diff --git a/OPServer/Config/IdentityServerIntegration.cs b/OPServer/Config/IdentityServerIntegration.cs index 5e10ee2..f87724d 100644 --- a/OPServer/Config/IdentityServerIntegration.cs +++ b/OPServer/Config/IdentityServerIntegration.cs @@ -32,7 +32,7 @@ out bool didSetupIdServer //customization // not sure these are needed, need to comment out and test xamarin app //services.AddTransient(); - services.AddTransient(); + //services.AddTransient(); var idsBuilder = services.AddIdentityServerConfiguredForCloudscribe(options => { diff --git a/OPServer/OPServer.csproj b/OPServer/OPServer.csproj index b56a56f..f673a20 100644 --- a/OPServer/OPServer.csproj +++ b/OPServer/OPServer.csproj @@ -32,7 +32,8 @@ - + + diff --git a/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/apiresource/api1.json b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/apiresource/api1.json new file mode 100644 index 0000000..6f7da0f --- /dev/null +++ b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/apiresource/api1.json @@ -0,0 +1 @@ +{"ApiSecrets":[{"Description":null,"Value":"K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=","Expiration":null,"Type":"SharedSecret"}],"Scopes":[{"Name":"api1","DisplayName":"api1","Description":"api1","Required":false,"Emphasize":false,"ShowInDiscoveryDocument":false,"UserClaims":[]}],"Enabled":true,"Name":"api1","DisplayName":"api1","Description":null,"UserClaims":["role"]} \ No newline at end of file diff --git a/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/client/vuejs.json b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/client/vuejs.json new file mode 100644 index 0000000..6b84586 --- /dev/null +++ b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/client/vuejs.json @@ -0,0 +1 @@ +{"Enabled":true,"ClientId":"vuejs","ProtocolType":"oidc","ClientSecrets":[],"RequireClientSecret":true,"ClientName":"vuejs","ClientUri":null,"LogoUri":null,"RequireConsent":true,"AllowRememberConsent":true,"AllowedGrantTypes":["implicit"],"RequirePkce":false,"AllowPlainTextPkce":false,"AllowAccessTokensViaBrowser":true,"RedirectUris":["http://localhost:5900"],"PostLogoutRedirectUris":["http://localhost:5900"],"FrontChannelLogoutUri":null,"FrontChannelLogoutSessionRequired":true,"BackChannelLogoutUri":null,"BackChannelLogoutSessionRequired":false,"AllowOfflineAccess":false,"AllowedScopes":["profile","openid","email","api1"],"AlwaysIncludeUserClaimsInIdToken":false,"IdentityTokenLifetime":300,"AccessTokenLifetime":3600,"AuthorizationCodeLifetime":300,"AbsoluteRefreshTokenLifetime":2592000,"SlidingRefreshTokenLifetime":1296000,"ConsentLifetime":null,"RefreshTokenUsage":0,"UpdateAccessTokenClaimsOnRefresh":false,"RefreshTokenExpiration":0,"AccessTokenType":0,"EnableLocalLogin":true,"IdentityProviderRestrictions":[],"IncludeJwtId":false,"Claims":[],"AlwaysSendClientClaims":false,"ClientClaimsPrefix":"client_","PairWiseSubjectSalt":null,"AllowedCorsOrigins":["http://localhost:5900"],"Properties":{}} \ No newline at end of file diff --git a/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/grantitem/9aca391e-e515-4520-9308-cf20b27750b3.json b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/grantitem/9aca391e-e515-4520-9308-cf20b27750b3.json new file mode 100644 index 0000000..a59d04d --- /dev/null +++ b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/grantitem/9aca391e-e515-4520-9308-cf20b27750b3.json @@ -0,0 +1 @@ +{"Id":"9aca391e-e515-4520-9308-cf20b27750b3","Key":"KZEUHjNwWQFSY0wB49bNJVUEvjYvUg8u9oiyXrVVrYs=","Type":"user_consent","SubjectId":"19d0f112-34ef-4266-b1b4-d9c2ead8349a","ClientId":"vuejs","CreationTime":"2018-04-16T16:02:17Z","Expiration":null,"Data":"{\"SubjectId\":\"19d0f112-34ef-4266-b1b4-d9c2ead8349a\",\"ClientId\":\"vuejs\",\"Scopes\":[\"openid\",\"profile\",\"api1\"],\"CreationTime\":\"2018-04-16T16:02:17Z\",\"Expiration\":null}"} \ No newline at end of file diff --git a/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/siteuser/19d0f112-34ef-4266-b1b4-d9c2ead8349a.json b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/siteuser/19d0f112-34ef-4266-b1b4-d9c2ead8349a.json index 122db0c..3890694 100644 --- a/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/siteuser/19d0f112-34ef-4266-b1b4-d9c2ead8349a.json +++ b/OPServer/nodb_storage/projects/9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c/siteuser/19d0f112-34ef-4266-b1b4-d9c2ead8349a.json @@ -1 +1 @@ -{"AuthorBio":"","Comment":"","NormalizedEmail":"ADMIN@ADMIN.COM","NormalizedUserName":"ADMIN","EmailConfirmed":true,"EmailConfirmSentUtc":null,"AgreementAcceptedUtc":null,"LockoutEndDateUtc":null,"NewEmail":"","NewEmailApproved":false,"LastPasswordChangeUtc":"2017-08-27T13:08:47.4368302Z","MustChangePwd":false,"PasswordHash":"AQAAAAEAACcQAAAAELsEmNPOTbhrsV8v+siQO9rtzlXkj3QvNVDtlaLZuo0XKTvKKlHx1ZCKgd/xnX4UuA==","CanAutoLockout":false,"AccessFailedCount":0,"RolesChanged":false,"SecurityStamp":"3687b9b5-4aed-447d-a674-cf1d5edc0a87","Signature":"","TwoFactorEnabled":false,"Id":"19d0f112-34ef-4266-b1b4-d9c2ead8349a","SiteId":"9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c","Email":"admin@admin.com","UserName":"admin","DisplayName":"Admin","FirstName":"","LastName":"","IsDeleted":false,"Trusted":false,"AvatarUrl":"","DateOfBirth":null,"CreatedUtc":"2017-08-23T19:27:35.0835603Z","LastModifiedUtc":"2017-08-23T19:27:35.0835607Z","DisplayInMemberList":true,"Gender":"","IsLockedOut":false,"LastLoginUtc":"2018-04-11T19:02:49.9303707Z","PhoneNumber":"","PhoneNumberConfirmed":false,"AccountApproved":true,"TimeZoneId":"","WebSiteUrl":""} \ No newline at end of file +{"AuthorBio":"","Comment":"","NormalizedEmail":"ADMIN@ADMIN.COM","NormalizedUserName":"ADMIN","EmailConfirmed":true,"EmailConfirmSentUtc":null,"AgreementAcceptedUtc":null,"LockoutEndDateUtc":null,"NewEmail":"","NewEmailApproved":false,"LastPasswordChangeUtc":"2017-08-27T13:08:47.4368302Z","MustChangePwd":false,"PasswordHash":"AQAAAAEAACcQAAAAELsEmNPOTbhrsV8v+siQO9rtzlXkj3QvNVDtlaLZuo0XKTvKKlHx1ZCKgd/xnX4UuA==","CanAutoLockout":false,"AccessFailedCount":0,"RolesChanged":false,"SecurityStamp":"3687b9b5-4aed-447d-a674-cf1d5edc0a87","Signature":"","TwoFactorEnabled":false,"Id":"19d0f112-34ef-4266-b1b4-d9c2ead8349a","SiteId":"9c1cd70e-455c-43e6-b7f4-bb4ed60cf70c","Email":"admin@admin.com","UserName":"admin","DisplayName":"Admin","FirstName":"","LastName":"","IsDeleted":false,"Trusted":false,"AvatarUrl":"","DateOfBirth":null,"CreatedUtc":"2017-08-23T19:27:35.0835603Z","LastModifiedUtc":"2017-08-23T19:27:35.0835607Z","DisplayInMemberList":true,"Gender":"","IsLockedOut":false,"LastLoginUtc":"2018-04-21T12:34:12.4164807Z","PhoneNumber":"","PhoneNumberConfirmed":false,"AccountApproved":true,"TimeZoneId":"","WebSiteUrl":""} \ No newline at end of file diff --git a/README.md b/README.md index d06d5f8..d25c3fc 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ If you are new to cloudscribe please see the [Introduction](https://www.cloudscr [![Build Status](https://travis-ci.org/cloudscribe/sample-idserver.svg?branch=master)](https://travis-ci.org/cloudscribe/sample-idserver) -# Using cloudscribe Core with IdentityServer4 and NoDb +# Using cloudscribe Core with IdentityServer4 -cloudscribe Core and IdentityServer4 integration provides a compelling solution that makes it easy to provision new OP (OpenId Connect Provider) Servers each with their own Users, Roles, Claims, Clients, and Scopes. It includes a UI for managing all the needed data including role and claim assignments for users. +cloudscribe Core and IdentityServer4 integration provides a compelling solution that makes it easy to provision new OpenId Connect Provider server endpoints each with their own Users, Roles, Claims, Clients, and Scopes. It includes a UI for managing all the needed data including role and claim assignments for users, api resources, identity resources and api clients. There are 2 mutually exclusive multi-tenancy configuration options. Tenants can be based on host names, or tenants can be based on the first folder segment of the url. This sample uses the folder segment approach. Folder tenants eare easier to provision than host name tenants because there are no additional DNS records needed and no additional SSL certificate is needed, you create new tenants from the UI and they work immediately. diff --git a/Tenant1Api/Controllers/IdentityController.cs b/Tenant1Api/Controllers/IdentityController.cs new file mode 100644 index 0000000..b87ae31 --- /dev/null +++ b/Tenant1Api/Controllers/IdentityController.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [Route("api/[controller]")] + [Authorize] + public class IdentityController : ControllerBase + { + + public IdentityController() + { + } + [HttpGet()] + public IActionResult Get() + { + return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); + } + } +} diff --git a/Tenant1Api/Program.cs b/Tenant1Api/Program.cs new file mode 100644 index 0000000..34e81a8 --- /dev/null +++ b/Tenant1Api/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Api +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/Tenant1Api/Properties/launchSettings.json b/Tenant1Api/Properties/launchSettings.json new file mode 100644 index 0000000..31c12e4 --- /dev/null +++ b/Tenant1Api/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5901", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HTTPS_PORT": "5901" + } + }, + "Api": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5901" + } + } + } +} \ No newline at end of file diff --git a/Tenant1Api/Startup.cs b/Tenant1Api/Startup.cs new file mode 100644 index 0000000..0d726da --- /dev/null +++ b/Tenant1Api/Startup.cs @@ -0,0 +1,51 @@ +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Api +{ + public class Startup + { + public IConfiguration Configuration { get; } + public IHostingEnvironment Environment { get; } + public Startup(IConfiguration configuration, IHostingEnvironment environment) + { + Configuration = configuration; + Environment = environment; + } + public void ConfigureServices(IServiceCollection services) + { + services + .AddMvcCore() + .AddJsonFormatters() + .AddAuthorization() + ; + + services.AddCors(); + + services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) + .AddIdentityServerAuthentication(options => + { + options.Authority = "http://localhost:50405" ; + options.RequireHttpsMetadata = false; + options.ApiName = "api1"; + options.ApiSecret = "secret"; + }); + + } + + public void Configure(IApplicationBuilder app) + { + app.UseCors(policy => + { + policy.AllowAnyOrigin(); + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + }); + app.UseAuthentication(); + app.UseMvc(); + } + } +} diff --git a/Tenant1Api/Tenant1Api.csproj b/Tenant1Api/Tenant1Api.csproj new file mode 100644 index 0000000..7acbfc1 --- /dev/null +++ b/Tenant1Api/Tenant1Api.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tenant1Api/appsettings.json b/Tenant1Api/appsettings.json new file mode 100644 index 0000000..09cf536 --- /dev/null +++ b/Tenant1Api/appsettings.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/Tenant1SpaVueJs/.babelrc b/Tenant1SpaVueJs/.babelrc new file mode 100644 index 0000000..6681edd --- /dev/null +++ b/Tenant1SpaVueJs/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["es2015", "stage-2"], + "plugins": [ + "transform-runtime", + "transform-async-to-generator" + ], + "comments": false +} diff --git a/Tenant1SpaVueJs/.editorconfig b/Tenant1SpaVueJs/.editorconfig new file mode 100644 index 0000000..e291365 --- /dev/null +++ b/Tenant1SpaVueJs/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/Tenant1SpaVueJs/.eslintrc.js b/Tenant1SpaVueJs/.eslintrc.js new file mode 100644 index 0000000..8e6549e --- /dev/null +++ b/Tenant1SpaVueJs/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style + extends: 'standard', + // required to lint *.vue files + plugins: [ + 'html' + ], + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/Tenant1SpaVueJs/.gitignore b/Tenant1SpaVueJs/.gitignore new file mode 100644 index 0000000..c69f59a --- /dev/null +++ b/Tenant1SpaVueJs/.gitignore @@ -0,0 +1,253 @@ +.DS_Store +node_modules/ +npm-debug.log + +/Properties/launchSettings.json + +package-lock.json + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +bin/ +Bin/ +obj/ +Obj/ + +# Visual Studio 2015 cache/options directory +.vs/ +/wwwroot/dist/** + +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +!/wwwroot/dist/_placeholder.txt + +/yarn.lock + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +orleans.codegen.cs + +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ + +.vscode/ diff --git a/Tenant1SpaVueJs/ClientApp/app.js b/Tenant1SpaVueJs/ClientApp/app.js new file mode 100644 index 0000000..b723c73 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/app.js @@ -0,0 +1,36 @@ +import Vue from 'vue' +import axios from 'axios' +import router from './router' +import store from './store' +import { sync } from 'vuex-router-sync' +import App from 'components/app-root' +import VueAxios from 'vue-axios' +Vue.use(VueAxios, axios) + +// Add a request interceptor +axios.interceptors.request.use((config) => { + // Do something before request is sent + if (store.state.auth.user.access_token) { + config.headers['Authorization'] = [store.state.auth.user.token_type, store.state.auth.user.access_token].join(' ') + } + else { + delete config.headers['Authorization'] + } + return config + }, (error) => { + return Promise.reject(error) + }) + +sync(store, router) + +const app = new Vue({ + store, + router, + ...App +}) + +export { + app, + router, + store +} \ No newline at end of file diff --git a/Tenant1SpaVueJs/ClientApp/boot-app.js b/Tenant1SpaVueJs/ClientApp/boot-app.js new file mode 100644 index 0000000..6ba5a52 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/boot-app.js @@ -0,0 +1,7 @@ +import './css/site.css' +import 'core-js/es6/promise' +import 'core-js/es6/array' + +import { app } from './app' + +app.$mount('#app') diff --git a/Tenant1SpaVueJs/ClientApp/boot-server.js b/Tenant1SpaVueJs/ClientApp/boot-server.js new file mode 100644 index 0000000..6da8e58 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/boot-server.js @@ -0,0 +1,12 @@ +var prerendering = require('aspnet-prerendering'); + +module.exports = prerendering.createServerRenderer(function (params) { + return new Promise(function (resolve, reject) { + var result = '

Loading...

' + + '

Current time in Node is: ' + new Date() + '

' + + '

Request path is: ' + params.location.path + '

' + + '

Absolute URL is: ' + params.absoluteUrl + '

'; + + resolve({ html: result }); + }); +}); \ No newline at end of file diff --git a/Tenant1SpaVueJs/ClientApp/components/app-root.vue b/Tenant1SpaVueJs/ClientApp/components/app-root.vue new file mode 100644 index 0000000..cdc58fa --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/components/app-root.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/Tenant1SpaVueJs/ClientApp/components/home-page.vue b/Tenant1SpaVueJs/ClientApp/components/home-page.vue new file mode 100644 index 0000000..0ca6fc5 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/components/home-page.vue @@ -0,0 +1,44 @@ + + + + diff --git a/Tenant1SpaVueJs/ClientApp/components/nav-menu.vue b/Tenant1SpaVueJs/ClientApp/components/nav-menu.vue new file mode 100644 index 0000000..e0ddc54 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/components/nav-menu.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/Tenant1SpaVueJs/ClientApp/config.js b/Tenant1SpaVueJs/ClientApp/config.js new file mode 100644 index 0000000..0f7bedc --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/config.js @@ -0,0 +1,21 @@ +var configDev = { + sts: 'http://localhost:50405/', + authority: 'http://localhost:50405/', + client_id: 'vuejs', + redirect_uri: 'http://localhost:5900', + post_logout_redirect_uri: 'http://localhost:5900', + response_type: 'id_token token', + scope: 'openid profile api1', + filterProtocolClaims: true, + loadUserInfo: true, + apiUrl:"http://localhost:5901/api/identity" +} + + +export default configDev + +function GetRootUrl() { + return window.location.host.substring(window.location.host.lastIndexOf('.', window.location.host.lastIndexOf('.') - 1) + 1) +} + + diff --git a/Tenant1SpaVueJs/ClientApp/css/site.css b/Tenant1SpaVueJs/ClientApp/css/site.css new file mode 100644 index 0000000..803247c --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/css/site.css @@ -0,0 +1,66 @@ +.main-nav li .glyphicon { + margin-right: 10px; +} + +/* Highlighting rules for nav menu items */ +.main-nav li a.active, +.main-nav li a.active:hover, +.main-nav li a.active:focus { + background-color: #4189C7; + color: white; +} + +/* Keep the nav menu independent of scrolling and on top of other items */ +.main-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1; +} + +@media (max-width: 767px) { + /* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */ + body { + padding-top: 50px; + } +} + +@media (min-width: 768px) { + /* On small screens, convert the nav menu to a vertical sidebar */ + .main-nav { + height: 100%; + width: calc(25% - 20px); + } + .main-nav .navbar { + border-radius: 0px; + border-width: 0px; + height: 100%; + } + .main-nav .navbar-header { + float: none; + } + .main-nav .navbar-collapse { + border-top: 1px solid #444; + padding: 0px; + } + .main-nav .navbar ul { + float: none; + } + .main-nav .navbar li { + float: none; + font-size: 15px; + margin: 6px; + } + .main-nav .navbar li a { + padding: 10px 16px; + border-radius: 4px; + } + .main-nav .navbar a { + /* If a menu item's text is too long, truncate it */ + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} \ No newline at end of file diff --git a/Tenant1SpaVueJs/ClientApp/router.js b/Tenant1SpaVueJs/ClientApp/router.js new file mode 100644 index 0000000..e3609af --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/router.js @@ -0,0 +1,13 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +import { routes } from './routes' + +Vue.use(VueRouter); + +let router = new VueRouter({ + mode: 'history', + routes +}) + +export default router diff --git a/Tenant1SpaVueJs/ClientApp/routes.js b/Tenant1SpaVueJs/ClientApp/routes.js new file mode 100644 index 0000000..24bff1b --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/routes.js @@ -0,0 +1,5 @@ +import HomePage from 'components/home-page' + +export const routes = [ + { path: '/', component: HomePage, display: 'Home', style: 'glyphicon glyphicon-home' } +] diff --git a/Tenant1SpaVueJs/ClientApp/store/auth.js b/Tenant1SpaVueJs/ClientApp/store/auth.js new file mode 100644 index 0000000..1f4d798 --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/store/auth.js @@ -0,0 +1,22 @@ +// import { UserManager } from 'oidc-client' +// import config from '../config' + +export default { + state: { + previousLocation: '/', + user: {} + }, + getters: { + }, + mutations: { + updatePreviousLocation(state, payload) { + state.previousLocation = payload + }, + user(state, payload) { + state.user = payload.user + } + }, + + actions: { + } +} diff --git a/Tenant1SpaVueJs/ClientApp/store/index.js b/Tenant1SpaVueJs/ClientApp/store/index.js new file mode 100644 index 0000000..a9bec8e --- /dev/null +++ b/Tenant1SpaVueJs/ClientApp/store/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import createPersistedState from 'vuex-persistedstate' +Vue.use(Vuex) + +import auth from './auth.js' + +const store = new Vuex.Store({ + plugins: [createPersistedState({ + storage: window.sessionStorage, + reducer: state => ({ // so provide here the states you want to persist + previousLocation: state.auth.previousLocation + }) + })], + strict: true, + modules: { + auth + } +}) + +export default store diff --git a/Tenant1SpaVueJs/Controllers/HomeController.cs b/Tenant1SpaVueJs/Controllers/HomeController.cs new file mode 100644 index 0000000..625db4d --- /dev/null +++ b/Tenant1SpaVueJs/Controllers/HomeController.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Tenant1SpaVueJs.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public IActionResult Error() + { + return View(); + } + } +} \ No newline at end of file diff --git a/Tenant1SpaVueJs/LICENSE.md b/Tenant1SpaVueJs/LICENSE.md new file mode 100644 index 0000000..34a8ce2 --- /dev/null +++ b/Tenant1SpaVueJs/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Mark Pieszak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Tenant1SpaVueJs/Program.cs b/Tenant1SpaVueJs/Program.cs new file mode 100644 index 0000000..21be4da --- /dev/null +++ b/Tenant1SpaVueJs/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; + +namespace Tenant1SpaVueJs +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/Tenant1SpaVueJs/README.md b/Tenant1SpaVueJs/README.md new file mode 100644 index 0000000..407f3f6 --- /dev/null +++ b/Tenant1SpaVueJs/README.md @@ -0,0 +1,30 @@ +# Based on Asp.NETCore 2.0 Vue 2 Starter - by [DevHelp.Online](http://www.DevHelp.Online) +https://github.com/MarkPieszak/aspnetcore-Vue-starter + +This VueJs sample client was contributed by [Paul Van Bladel](https://github.com/paulvanbladel) + +# Installation + +The Vuejs project should start without prior installation, it has webpack hot module middleware that will automatically install the dependencies and generate the output files in wwwroot/dist. + +Changes can be made while the sample is running and the hot module middleware will automatically re-generate the dist files + +# How to run the Vuejs project in visual studio + +1. Start the OPServer project by right click the project and choose view in browser +2. Right click the Tenant1Api project and choose view in browser, since this one is just an api project there is no UI and you will see a blank page for this in the browser, that is ok. +3. Right click the Tenant1SpaVueJs project and choose view in browser. This one is the VueSp sample, it can authenticate against the OPserver project and it calls the Api in the Tenant1Api project which is protected by the OPServer as its authority. + +# How to run from the command line + +1. Open a command window (or pwoershell) on the OPServer project and enter the commands dotnet restore, then dotnet build, then dotnet run +2. You can optionally view the OPserver in the browser at http://localhost:50405/ but best to use a different web browser than the Vue Sample if you intend to login directly to the OPServer project so that no auth cookie is shared between projects (which would happen since both are on localhost). +3. Open a command window on the Tenant1Api folder then enter the commands dotnet restore, dotnet build, dotnet run - that will get the api running at http://localhost:5901/ +4. Open a command window on the Tenant1SpaVueJs project folder and enter the commands dotnet restore, dotnet build, dotnet run +5. Open a web browser (ideally not the same browser at you have the OPServer open) at http://localhost:5900/ + +# Login Credentials + +See the main README.md in the root of the solution for information about the existing Tenant1 user account credentials you can use to test the client. + + diff --git a/Tenant1SpaVueJs/Startup.cs b/Tenant1SpaVueJs/Startup.cs new file mode 100644 index 0000000..8346caa --- /dev/null +++ b/Tenant1SpaVueJs/Startup.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SpaServices.Webpack; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Tenant1SpaVueJs +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions + { + HotModuleReplacement = true + }); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseStaticFiles(); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapSpaFallbackRoute( + name: "spa-fallback", + defaults: new { controller = "Home", action = "Index" }); + }); + } + } +} diff --git a/Tenant1SpaVueJs/Tenant1SpaVueJs.csproj b/Tenant1SpaVueJs/Tenant1SpaVueJs.csproj new file mode 100644 index 0000000..bffcc4c --- /dev/null +++ b/Tenant1SpaVueJs/Tenant1SpaVueJs.csproj @@ -0,0 +1,33 @@ + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + + + + + %(DistFiles.Identity) + PreserveNewest + + + + diff --git a/Tenant1SpaVueJs/Views/Home/Index.cshtml b/Tenant1SpaVueJs/Views/Home/Index.cshtml new file mode 100644 index 0000000..6f46bcd --- /dev/null +++ b/Tenant1SpaVueJs/Views/Home/Index.cshtml @@ -0,0 +1,7 @@ +@{ ViewData["Title"] = "Home Page"; } + +
+ +@section scripts { + +} diff --git a/Tenant1SpaVueJs/Views/Shared/Error.cshtml b/Tenant1SpaVueJs/Views/Shared/Error.cshtml new file mode 100644 index 0000000..473b35d --- /dev/null +++ b/Tenant1SpaVueJs/Views/Shared/Error.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

diff --git a/Tenant1SpaVueJs/Views/Shared/_Layout.cshtml b/Tenant1SpaVueJs/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..4fa3a21 --- /dev/null +++ b/Tenant1SpaVueJs/Views/Shared/_Layout.cshtml @@ -0,0 +1,22 @@ + + + + + + + @ViewData["Title"] - aspnetcore_Vue_starter + + + + + + + + + @RenderBody() + + + @RenderSection("scripts", required: false) + + + \ No newline at end of file diff --git a/Tenant1SpaVueJs/Views/_ViewImports.cshtml b/Tenant1SpaVueJs/Views/_ViewImports.cshtml new file mode 100644 index 0000000..06f22d7 --- /dev/null +++ b/Tenant1SpaVueJs/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Tenant1SpaVueJs +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" +@addTagHelper "*, Microsoft.AspNetCore.SpaServices" diff --git a/Tenant1SpaVueJs/Views/_ViewStart.cshtml b/Tenant1SpaVueJs/Views/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/Tenant1SpaVueJs/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/Tenant1SpaVueJs/appsettings.json b/Tenant1SpaVueJs/appsettings.json new file mode 100644 index 0000000..dbf2ac5 --- /dev/null +++ b/Tenant1SpaVueJs/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": {}, + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Tenant1SpaVueJs/global.json b/Tenant1SpaVueJs/global.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/Tenant1SpaVueJs/global.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/Tenant1SpaVueJs/package.json b/Tenant1SpaVueJs/package.json new file mode 100644 index 0000000..a9a0bc3 --- /dev/null +++ b/Tenant1SpaVueJs/package.json @@ -0,0 +1,49 @@ +{ + "name": "aspnetcore-vuejs", + "description": "ASP.NET Core & VueJS Starter project", + "author": "Mark Pieszak", + "scripts": { + "dev": "cross-env ASPNETCORE_ENVIRONMENT=Development NODE_ENV=development dotnet run", + "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", + "install": "webpack --config webpack.config.vendor.js" + }, + "dependencies": { + "axios": "^0.15.3", + "core-js": "^2.4.1", + "font-awesome": "^4.6.3", + "oidc-client": "^1.4.1", + "vue": "^2.1.8", + "vue-axios": "^2.1.1", + "vue-router": "^2.1.1", + "vue-server-renderer": "^2.1.8", + "vue-template-compiler": "^2.1.8", + "vuex": "^2.1.1", + "vuex-persistedstate": "^2.5.2", + "vuex-router-sync": "^4.0.1" + }, + "devDependencies": { + "aspnet-webpack": "^2.0.1", + "babel-core": "^6.21.0", + "babel-loader": "^6.2.10", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-stage-2": "^6.18.0", + "babel-register": "^6.18.0", + "bootstrap": "^3.3.6", + "cross-env": "^3.1.3", + "css-loader": "^0.26.1", + "event-source-polyfill": "^0.0.7", + "extract-text-webpack-plugin": "^2.0.0-rc", + "file-loader": "^0.9.0", + "jquery": "^2.2.1", + "node-sass": "^4.1.0", + "optimize-css-assets-webpack-plugin": "^1.3.1", + "sass-loader": "^4.1.0", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "vue-loader": "^10.0.2", + "webpack": "^2.2.0", + "webpack-hot-middleware": "^2.12.2" + } +} diff --git a/Tenant1SpaVueJs/web.config b/Tenant1SpaVueJs/web.config new file mode 100644 index 0000000..d579a1e --- /dev/null +++ b/Tenant1SpaVueJs/web.config @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Tenant1SpaVueJs/webpack.config.js b/Tenant1SpaVueJs/webpack.config.js new file mode 100644 index 0000000..ffdd81a --- /dev/null +++ b/Tenant1SpaVueJs/webpack.config.js @@ -0,0 +1,51 @@ +const path = require('path'); +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const bundleOutputDir = './wwwroot/dist'; + +module.exports = (env) => { + const isDevBuild = !(env && env.prod); + return [{ + stats: { modules: false }, + entry: { 'main': './ClientApp/boot-app.js' }, + resolve: { + extensions: ['.js', '.vue'], + alias: { + 'vue$': 'vue/dist/vue', + 'components': path.resolve(__dirname, './ClientApp/components'), + 'views': path.resolve(__dirname, './ClientApp/views'), + 'utils': path.resolve(__dirname, './ClientApp/utils'), + 'api': path.resolve(__dirname, './ClientApp/store/api') + } + }, + output: { + path: path.join(__dirname, bundleOutputDir), + filename: '[name].js', + publicPath: '/dist/' + }, + module: { + rules: [ + { test: /\.vue$/, include: /ClientApp/, use: 'vue-loader' }, + { test: /\.js$/, include: /ClientApp/, use: 'babel-loader' }, + { test: /\.css$/, use: isDevBuild ? ['style-loader', 'css-loader'] : ExtractTextPlugin.extract({ use: 'css-loader' }) }, + { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' } + ] + }, + plugins: [ + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require('./wwwroot/dist/vendor-manifest.json') + }) + ].concat(isDevBuild ? [ + // Plugins that apply in development builds only + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', // Remove this line if you prefer inline source maps + moduleFilenameTemplate: path.relative(bundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk + }) + ] : [ + // Plugins that apply in production builds only + new webpack.optimize.UglifyJsPlugin(), + new ExtractTextPlugin('site.css') + ]) + }]; +}; diff --git a/Tenant1SpaVueJs/webpack.config.vendor.js b/Tenant1SpaVueJs/webpack.config.vendor.js new file mode 100644 index 0000000..b8c9341 --- /dev/null +++ b/Tenant1SpaVueJs/webpack.config.vendor.js @@ -0,0 +1,49 @@ +const path = require('path'); +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); + +module.exports = (env) => { + const extractCSS = new ExtractTextPlugin('vendor.css'); + const isDevBuild = !(env && env.prod); + return [{ + stats: { modules: false }, + resolve: { + extensions: ['.js'] + }, + module: { + rules: [ + { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' }, + { test: /\.css(\?|$)/, use: extractCSS.extract(['css-loader']) } + ] + }, + entry: { + vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'event-source-polyfill', 'vue', 'vuex', 'axios', 'vue-router', 'jquery'], + }, + output: { + path: path.join(__dirname, 'wwwroot', 'dist'), + publicPath: '/dist/', + filename: '[name].js', + library: '[name]_[hash]', + }, + plugins: [ + extractCSS, + // Compress extracted CSS. + new OptimizeCSSPlugin({ + cssProcessorOptions: { + safe: true + } + }), + new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) + new webpack.DllPlugin({ + path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), + name: '[name]_[hash]' + }), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': isDevBuild ? '"development"' : '"production"' + }) + ].concat(isDevBuild ? [] : [ + new webpack.optimize.UglifyJsPlugin() + ]) + }]; +}; diff --git a/cloudscribe-idserver-nodb.sln b/cloudscribe-idserver-nodb.sln index 2169420..03d751c 100644 --- a/cloudscribe-idserver-nodb.sln +++ b/cloudscribe-idserver-nodb.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tenant2SpaPolymer", "Tenant EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tenant1SpaLocalApi", "Tenant1SpaLocalApi\Tenant1SpaLocalApi.csproj", "{56B452A9-BB1A-4179-8E05-7F1600C7899C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tenant1SpaVueJs", "Tenant1SpaVueJs\Tenant1SpaVueJs.csproj", "{FD64BB18-E45F-465C-9D41-FDC637805DFD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tenant1Api", "Tenant1Api\Tenant1Api.csproj", "{2FA1CB1E-A9B4-4A13-927E-5076DF46C0E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {56B452A9-BB1A-4179-8E05-7F1600C7899C}.Debug|Any CPU.Build.0 = Debug|Any CPU {56B452A9-BB1A-4179-8E05-7F1600C7899C}.Release|Any CPU.ActiveCfg = Release|Any CPU {56B452A9-BB1A-4179-8E05-7F1600C7899C}.Release|Any CPU.Build.0 = Release|Any CPU + {FD64BB18-E45F-465C-9D41-FDC637805DFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD64BB18-E45F-465C-9D41-FDC637805DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD64BB18-E45F-465C-9D41-FDC637805DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD64BB18-E45F-465C-9D41-FDC637805DFD}.Release|Any CPU.Build.0 = Release|Any CPU + {2FA1CB1E-A9B4-4A13-927E-5076DF46C0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FA1CB1E-A9B4-4A13-927E-5076DF46C0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FA1CB1E-A9B4-4A13-927E-5076DF46C0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FA1CB1E-A9B4-4A13-927E-5076DF46C0E3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE