From 86b26fdd12b68791db39fc496d7d155d638a1687 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 26 May 2017 20:40:45 +0200 Subject: [PATCH] More account controller features. --- src/Squidex.Core/Squidex.Core.csproj | 8 +- src/Squidex.Events/Squidex.Events.csproj | 2 +- .../Squidex.Infrastructure.Redis.csproj | 2 +- src/Squidex.Infrastructure/GravatarHelper.cs | 27 + .../Squidex.Infrastructure.csproj | 6 +- .../Squidex.Read.MongoDb.csproj | 4 +- .../Users/MongoUserRepository.cs | 74 +- src/Squidex.Read/Squidex.Read.csproj | 4 +- .../Users/Repositories/IUserRepository.cs | 4 + src/Squidex.Write/Squidex.Write.csproj | 2 +- .../Config/Identity/MicrosoftHandler.cs | 41 + .../Config/Identity/MicrosoftIdentityUsage.cs | 39 + .../Config/Identity/MyIdentityOptions.cs | 11 + src/Squidex/Config/Swagger/SwaggerServices.cs | 2 +- .../Swagger/XmlResponseTypesProcessor.cs | 15 +- .../Api/Users/Models/CreateUserDto.cs | 25 + .../Api/Users/Models/UpdateUserDto.cs | 24 + .../Api/Users/UserManagementController.cs | 22 + .../Generator/SchemasSwaggerGenerator.cs | 6 +- .../UI/Account/AccountController.cs | 66 +- .../Controllers/UI/Account/LoginModel.cs | 21 + src/Squidex/Controllers/UI/Account/LoginVM.cs | 8 + src/Squidex/Squidex.csproj | 47 +- src/Squidex/Startup.cs | 1 + src/Squidex/Views/Account/Login.cshtml | 109 +- src/Squidex/app/theme/_bootstrap.scss | 14 + src/Squidex/app/theme/_static.scss | 89 +- src/Squidex/app/theme/_vars.scss | 4 + .../app/theme/icomoon/demo-files/demo.css | 10 +- src/Squidex/app/theme/icomoon/demo.html | 1018 +++++++----- .../app/theme/icomoon/fonts/icomoon.eot | Bin 16504 -> 16972 bytes .../app/theme/icomoon/fonts/icomoon.svg | 3 + .../app/theme/icomoon/fonts/icomoon.ttf | Bin 16340 -> 16808 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 16416 -> 16884 bytes src/Squidex/app/theme/icomoon/selection.json | 1451 +++++++++-------- src/Squidex/app/theme/icomoon/style.css | 215 ++- src/Squidex/appsettings.json | 3 + tests/RunCoverage.ps1 | 2 +- .../Squidex.Core.Tests.csproj | 2 +- .../Squidex.Infrastructure.Tests.csproj | 6 +- .../Squidex.Read.Tests.csproj | 2 +- .../Squidex.Write.Tests.csproj | 2 +- 42 files changed, 2177 insertions(+), 1214 deletions(-) create mode 100644 src/Squidex.Infrastructure/GravatarHelper.cs create mode 100644 src/Squidex/Config/Identity/MicrosoftHandler.cs create mode 100644 src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs create mode 100644 src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs create mode 100644 src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs create mode 100644 src/Squidex/Controllers/UI/Account/LoginModel.cs diff --git a/src/Squidex.Core/Squidex.Core.csproj b/src/Squidex.Core/Squidex.Core.csproj index b9f4d0271..790b214c1 100644 --- a/src/Squidex.Core/Squidex.Core.csproj +++ b/src/Squidex.Core/Squidex.Core.csproj @@ -11,11 +11,11 @@ - + - - - + + + diff --git a/src/Squidex.Events/Squidex.Events.csproj b/src/Squidex.Events/Squidex.Events.csproj index fad168319..e9b4fd1df 100644 --- a/src/Squidex.Events/Squidex.Events.csproj +++ b/src/Squidex.Events/Squidex.Events.csproj @@ -12,6 +12,6 @@ - + diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj index 5e5b44588..6cb63c49e 100644 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -10,6 +10,6 @@ - + diff --git a/src/Squidex.Infrastructure/GravatarHelper.cs b/src/Squidex.Infrastructure/GravatarHelper.cs new file mode 100644 index 000000000..83e3f37ef --- /dev/null +++ b/src/Squidex.Infrastructure/GravatarHelper.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// GravatarHelper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Cryptography; +using System.Text; + +namespace Squidex.Infrastructure +{ + public static class GravatarHelper + { + public static string CreatePictureUrl(string email) + { + using (var md5 = MD5.Create()) + { + var gravatarHash = md5.ComputeHash(Encoding.UTF8.GetBytes(email.ToLowerInvariant().Trim())); + var gravatarUrl = $"https://www.gravatar.com/avatar/{gravatarHash}"; + + return gravatarUrl; + } + } + } +} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index b1b173ee4..db94e81dc 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -9,10 +9,10 @@ - - + + - + diff --git a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj index 836dbff66..6b616cd4f 100644 --- a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj +++ b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs b/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs index d282cf336..8f98a6a41 100644 --- a/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs +++ b/src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs @@ -9,13 +9,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.MongoDB; +using Squidex.Core.Identity; using Squidex.Infrastructure; using Squidex.Read.Users; using Squidex.Read.Users.Repositories; +// ReSharper disable ImplicitlyCapturedClosure // ReSharper disable InvertIf namespace Squidex.Read.MongoDb.Users @@ -52,6 +55,63 @@ namespace Squidex.Read.MongoDb.Users return user != null ? new MongoUserEntity(user) : null; } + public async Task CreateAsync(string email, string displayName, string password) + { + var pictureUrl = GravatarHelper.CreatePictureUrl(email); + + var user = new IdentityUser + { + Email = email, + Claims = new List + { + new IdentityUserClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl)), + new IdentityUserClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName)) + }, + UserName = email + }; + + await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); + + if (!string.IsNullOrWhiteSpace(password)) + { + await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user."); + } + + return user.Id; + } + + public async Task UpdateAsync(string id, string email, string displayName, string password) + { + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); + } + + if (!string.IsNullOrWhiteSpace(email)) + { + user.Email = user.UserName = email; + } + + if (!string.IsNullOrWhiteSpace(displayName)) + { + user.Claims.Find(x => x.Type == SquidexClaimTypes.SquidexDisplayName).Value = displayName; + } + + if (!string.IsNullOrWhiteSpace(password)) + { + user.PasswordHash = null; + } + + await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user."); + + if (!string.IsNullOrWhiteSpace(password)) + { + await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot update user."); + } + } + public async Task LockAsync(string id) { var user = await userManager.FindByIdAsync(id); @@ -61,7 +121,7 @@ namespace Squidex.Read.MongoDb.Users throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); } - await userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)); + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)), "Cannot lock user."); } public async Task UnlockAsync(string id) @@ -73,7 +133,17 @@ namespace Squidex.Read.MongoDb.Users throw new DomainObjectNotFoundException(id, typeof(IdentityUser)); } - await userManager.SetLockoutEndDateAsync(user, null); + await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null), "Cannot unlock user."); + } + + private static async Task DoChecked(Func> action, string message) + { + var result = await action(); + + if (!result.Succeeded) + { + throw new ValidationException(message, result.Errors.Select(x => new ValidationError(x.Description)).ToArray()); + } } private IQueryable QueryUsers(string email = null) diff --git a/src/Squidex.Read/Squidex.Read.csproj b/src/Squidex.Read/Squidex.Read.csproj index 35140eb7f..0efb70fad 100644 --- a/src/Squidex.Read/Squidex.Read.csproj +++ b/src/Squidex.Read/Squidex.Read.csproj @@ -13,7 +13,7 @@ - - + + diff --git a/src/Squidex.Read/Users/Repositories/IUserRepository.cs b/src/Squidex.Read/Users/Repositories/IUserRepository.cs index 94e2ee15c..e1c2b9386 100644 --- a/src/Squidex.Read/Users/Repositories/IUserRepository.cs +++ b/src/Squidex.Read/Users/Repositories/IUserRepository.cs @@ -17,6 +17,10 @@ namespace Squidex.Read.Users.Repositories Task FindUserByIdAsync(string id); + Task CreateAsync(string email, string displayName, string password); + + Task UpdateAsync(string id, string email, string displayName, string password); + Task LockAsync(string id); Task UnlockAsync(string id); diff --git a/src/Squidex.Write/Squidex.Write.csproj b/src/Squidex.Write/Squidex.Write.csproj index ffe61a370..6030e32b8 100644 --- a/src/Squidex.Write/Squidex.Write.csproj +++ b/src/Squidex.Write/Squidex.Write.csproj @@ -14,6 +14,6 @@ - + diff --git a/src/Squidex/Config/Identity/MicrosoftHandler.cs b/src/Squidex/Config/Identity/MicrosoftHandler.cs new file mode 100644 index 000000000..2fcd9a902 --- /dev/null +++ b/src/Squidex/Config/Identity/MicrosoftHandler.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// MicrosoftHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Core.Identity; + +// ReSharper disable InvertIf + +namespace Squidex.Config.Identity +{ + public sealed class MicrosoftHandler : OAuthEvents + { + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var displayName = context.User.Value("displayName"); + + if (!string.IsNullOrEmpty(displayName)) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName)); + } + + var id = context.User.Value("id"); + + if (!string.IsNullOrEmpty(id)) + { + var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; + + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl)); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs b/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs new file mode 100644 index 000000000..40088d10b --- /dev/null +++ b/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// MicrosoftIdentityUsage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +// ReSharper disable InvertIf + +namespace Squidex.Config.Identity +{ + public static class MicrosoftIdentityUsage + { + public static IApplicationBuilder UseMyMicrosoftAuthentication(this IApplicationBuilder app) + { + var options = app.ApplicationServices.GetService>().Value; + + if (options.IsMicrosoftAuthConfigured()) + { + var googleOptions = + new MicrosoftAccountOptions + { + ClientId = options.MicrosoftClient, + ClientSecret = options.MicrosoftSecret, + Events = new MicrosoftHandler() + }; + + app.UseMicrosoftAccountAuthentication(googleOptions); + } + + return app; + } + } +} diff --git a/src/Squidex/Config/Identity/MyIdentityOptions.cs b/src/Squidex/Config/Identity/MyIdentityOptions.cs index 9268e9ddf..10d0fc99a 100644 --- a/src/Squidex/Config/Identity/MyIdentityOptions.cs +++ b/src/Squidex/Config/Identity/MyIdentityOptions.cs @@ -22,10 +22,16 @@ namespace Squidex.Config.Identity public string GithubSecret { get; set; } + public string MicrosoftClient { get; set; } + + public string MicrosoftSecret { get; set; } + public bool EnforceAdmin { get; set; } public bool RequiresHttps { get; set; } + public bool AllowPasswordAuth { get; set; } + public bool LockAutomatically { get; set; } public bool IsAdminConfigured() @@ -42,5 +48,10 @@ namespace Squidex.Config.Identity { return !string.IsNullOrWhiteSpace(GoogleClient) && !string.IsNullOrWhiteSpace(GoogleSecret); } + + public bool IsMicrosoftAuthConfigured() + { + return !string.IsNullOrWhiteSpace(MicrosoftClient) && !string.IsNullOrWhiteSpace(MicrosoftSecret); + } } } diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Config/Swagger/SwaggerServices.cs index 8eb5836fc..f9cbc19ec 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Config/Swagger/SwaggerServices.cs @@ -87,7 +87,7 @@ namespace Squidex.Config.Swagger settings.DocumentProcessors.Add(new XmlTagProcessor()); settings.OperationProcessors.Add(new XmlTagProcessor()); - settings.OperationProcessors.Add(new XmlResponseTypesProcessor()); + settings.OperationProcessors.Add(new XmlResponseTypesProcessor(settings)); return settings; } diff --git a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs index b928fea69..42bc13427 100644 --- a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs @@ -15,6 +15,7 @@ using NJsonSchema.Infrastructure; using NSwag; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using NSwag.SwaggerGeneration.WebApi; using Squidex.Controllers.Api; // ReSharper disable UseObjectOrCollectionInitializer @@ -25,6 +26,13 @@ namespace Squidex.Config.Swagger { private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); + private readonly WebApiToSwaggerGeneratorSettings settings; + + public XmlResponseTypesProcessor(WebApiToSwaggerGeneratorSettings settings) + { + this.settings = settings; + } + public async Task ProcessAsync(OperationProcessorContext context) { var hasOkResponse = false; @@ -62,7 +70,7 @@ namespace Squidex.Config.Swagger return true; } - private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) + private async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) { if (operation.Responses.ContainsKey("500")) { @@ -70,11 +78,12 @@ namespace Squidex.Config.Swagger } var errorType = typeof(ErrorDto); - var errorSchema = JsonObjectTypeDescription.FromType(errorType, new Attribute[0], EnumHandling.String); + var errorContract = settings.ActualContractResolver.ResolveContract(errorType); + var errorScheme = JsonObjectTypeDescription.FromType(errorType, errorContract, new Attribute[0], EnumHandling.String); var response = new SwaggerResponse { Description = "Operation failed." }; - response.Schema = await context.SwaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); + response.Schema = await context.SwaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorScheme.IsNullable, null); operation.Responses.Add("500", response); } diff --git a/src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs b/src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs new file mode 100644 index 000000000..77a4167c4 --- /dev/null +++ b/src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// CreateUserDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Controllers.Api.Users.Models +{ + public sealed class CreateUserDto + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + public string DisplayName { get; set; } + + [Required] + public string Password { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs b/src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs new file mode 100644 index 000000000..2ff41be03 --- /dev/null +++ b/src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// UpdateUserDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Controllers.Api.Users.Models +{ + public sealed class UpdateUserDto + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + public string DisplayName { get; set; } + + public string Password { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Users/UserManagementController.cs b/src/Squidex/Controllers/Api/Users/UserManagementController.cs index abfb2ec0a..b4fa920d0 100644 --- a/src/Squidex/Controllers/Api/Users/UserManagementController.cs +++ b/src/Squidex/Controllers/Api/Users/UserManagementController.cs @@ -51,6 +51,28 @@ namespace Squidex.Controllers.Api.Users return Ok(model); } + [HttpPost] + [Route("user-management")] + [ApiCosts(0)] + public async Task Create([FromBody] CreateUserDto request) + { + var id = await userRepository.CreateAsync(request.Email, request.DisplayName, request.Password); + + var model = new EntityCreatedDto { Id = id }; + + return Ok(model); + } + + [HttpPut] + [Route("user-management/{id}")] + [ApiCosts(0)] + public async Task Update(string id, [FromBody] UpdateUserDto request) + { + await userRepository.UpdateAsync(id, request.Email, request.DisplayName, request.Password); + + return NoContent(); + } + [HttpPut] [Route("user-management/{id}/lock/")] [ApiCosts(0)] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 1f65b01f7..ad5277c14 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -34,6 +34,7 @@ namespace Squidex.Controllers.ContentApi.Generator { public sealed class SchemasSwaggerGenerator { + private readonly SwaggerOwinSettings swaggerSettings; private readonly SwaggerJsonSchemaGenerator schemaGenerator; private readonly SwaggerDocument document = new SwaggerDocument { Tags = new List() }; private readonly HttpContext context; @@ -59,6 +60,8 @@ namespace Squidex.Controllers.ContentApi.Generator schemaBodyDescription = SwaggerHelper.LoadDocs("schemabody"); schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); + + this.swaggerSettings = swaggerSettings; } public async Task Generate(IAppEntity targetApp, IEnumerable schemas) @@ -130,7 +133,8 @@ namespace Squidex.Controllers.ContentApi.Generator private async Task GenerateBasicSchemas() { var errorType = typeof(ErrorDto); - var errorSchema = JsonObjectTypeDescription.FromType(errorType, new Attribute[0], EnumHandling.String); + var errorContract = swaggerSettings.ActualContractResolver.ResolveContract(errorType); + var errorSchema = JsonObjectTypeDescription.FromType(errorType, errorContract, new Attribute[0], EnumHandling.String); errorDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); } diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index bbf97b9ba..a2fad3b24 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Config.Identity; using Squidex.Core.Identity; +using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Tasks; @@ -125,15 +126,64 @@ namespace Squidex.Controllers.UI.Account return RedirectToAction(nameof(LogoutCompleted)); } + [HttpGet] + [Route("account/signup/")] + public IActionResult Signup(string returnUrl = null) + { + return LoginView(returnUrl, false, false); + } + [HttpGet] [Route("account/login/")] public IActionResult Login(string returnUrl = null) { - var providers = + return LoginView(returnUrl, true, false); + } + + [HttpPost] + [Route("account/login/")] + public async Task Login(LoginModel model, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return LoginView(returnUrl, true, true); + } + + var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, true, true); + + if (!result.Succeeded) + { + return LoginView(returnUrl, true, true); + } + else if (!string.IsNullOrWhiteSpace(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return Redirect("~/../"); + } + } + + private IActionResult LoginView(string returnUrl, bool isLogin, bool isFailed) + { + var allowPasswordAuth = identityOptions.Value.AllowPasswordAuth; + + var providers = signInManager.GetExternalAuthenticationSchemes() .Select(x => new ExternalProvider(x.AuthenticationScheme, x.DisplayName)).ToList(); - return View(new LoginVM { ExternalProviders = providers, ReturnUrl = returnUrl }); + var vm = new LoginVM + { + ExternalProviders = providers, + IsLogin = isLogin, + IsFailed = isFailed, + HasPasswordAuth = allowPasswordAuth, + HasPasswordAndExternal = allowPasswordAuth && providers.Any(), + ReturnUrl = returnUrl + }; + + return View("Login", vm); } [HttpPost] @@ -204,7 +254,7 @@ namespace Squidex.Controllers.UI.Account } else { - return Redirect("~/"); + return Redirect("~/../"); } } @@ -249,6 +299,16 @@ namespace Squidex.Controllers.UI.Account { var user = new IdentityUser { Email = email, UserName = email }; + if (!externalLogin.Principal.HasClaim(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)) + { + user.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email))); + } + + if (!externalLogin.Principal.HasClaim(x => x.Type == SquidexClaimTypes.SquidexDisplayName)) + { + user.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, email)); + } + foreach (var squidexClaim in externalLogin.Principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix))) { user.AddClaim(squidexClaim); diff --git a/src/Squidex/Controllers/UI/Account/LoginModel.cs b/src/Squidex/Controllers/UI/Account/LoginModel.cs new file mode 100644 index 000000000..ca70d9aca --- /dev/null +++ b/src/Squidex/Controllers/UI/Account/LoginModel.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// LoginModel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Controllers.UI.Account +{ + public sealed class LoginModel + { + [Required] + public string Email { get; set; } + + [Required] + public string Password { get; set; } + } +} diff --git a/src/Squidex/Controllers/UI/Account/LoginVM.cs b/src/Squidex/Controllers/UI/Account/LoginVM.cs index cd08db172..b9899fdf1 100644 --- a/src/Squidex/Controllers/UI/Account/LoginVM.cs +++ b/src/Squidex/Controllers/UI/Account/LoginVM.cs @@ -14,6 +14,14 @@ namespace Squidex.Controllers.UI.Account { public string ReturnUrl { get; set; } + public bool IsLogin { get; set; } + + public bool IsFailed { get; set; } + + public bool HasPasswordAuth { get; set; } + + public bool HasPasswordAndExternal { get; set; } + public IEnumerable ExternalProviders { get; set; } } } diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 25aa87a68..a9a747069 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -39,36 +39,37 @@ - + - - + + - - - - - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + - + - - + + diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index cd072adf6..9cd4aae12 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -144,6 +144,7 @@ namespace Squidex identityApp.UseMyApiProtection(); identityApp.UseMyGoogleAuthentication(); identityApp.UseMyGithubAuthentication(); + identityApp.UseMyMicrosoftAuthentication(); identityApp.UseStaticFiles(); identityApp.MapWhen(x => IsIdentityRequest(x), mvcApp => diff --git a/src/Squidex/Views/Account/Login.cshtml b/src/Squidex/Views/Account/Login.cshtml index 2e5be9b77..74790ccf1 100644 --- a/src/Squidex/Views/Account/Login.cshtml +++ b/src/Squidex/Views/Account/Login.cshtml @@ -4,25 +4,106 @@ @model Squidex.Controllers.UI.Account.LoginVM @{ - ViewBag.Title = "Login"; + var type = Model.IsLogin ? "Login" : "Signup"; + + ViewBag.Title = type; } -
-
-

- @foreach (var provider in Model.ExternalProviders) +

+ +@if (!Model.HasPasswordAuth && Model.ExternalProviders.Count() == 1) +{ + +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 952f8b0ff..3ee261c18 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -1,6 +1,8 @@ @import '_mixins'; @import '_vars'; +@import './../../node_modules/bootstrap/scss/mixins/_buttons'; + body { background: $color-background; } @@ -167,6 +169,18 @@ body { } .btn { + &-github { + @include button-variant($color-dark-foreground, $color-extern-github, $color-extern-github); + } + + &-google { + @include button-variant($color-dark-foreground, $color-extern-google, $color-extern-google); + } + + &-microsoft { + @include button-variant($color-dark-foreground, $color-extern-microsoft, $color-extern-microsoft); + } + &-cancel { padding: .4rem; font-size: 1.1rem; diff --git a/src/Squidex/app/theme/_static.scss b/src/Squidex/app/theme/_static.scss index dcfaae418..4e33af0cf 100644 --- a/src/Squidex/app/theme/_static.scss +++ b/src/Squidex/app/theme/_static.scss @@ -1,6 +1,87 @@ @import '_mixins'; @import '_vars'; +noscript { + display: block; + color: $color-theme-error; + font-size: 30px; + font-weight: lighter; + margin-bottom: 20px; +} + +.login { + & { + max-width: 20rem; + margin: 0 auto; + padding: 1rem 2rem; + } + + &-headline { + & { + color: $color-empty; + display: block; + margin-bottom: 1rem; + font-size: 1.3rem; + font-weight: normal; + text-align: center; + text-transform: uppercase; + } + + &.active { + color: $color-text; + cursor: pointer; + pointer-events: none; + } + } + + &-logo { + @include fixed(1rem, auto, auto, 1rem); + } + + &-password-signup { + color: $color-empty; + } + + &-footer { + font-size: .8rem; + font-weight: normal; + margin-top: 2rem; + } + + &-separator { + & { + text-align: center; + margin-bottom: 2.5rem; + margin-top: 1.5rem; + border-bottom: 1px solid $color-border; + } + + &-text { + color: darken($color-border, 15%); + display: inline-block; + background: $color-background; + border: 0; + bottom: -.7rem; + position: relative; + padding: 0 1rem; + } + } + + .icon-microsoft { + &::before { + color: $color-dark-foreground; + } + } + + .btn-external { + text-align: left; + } + + .form-group { + margin-bottom: .5rem; + } +} + .splash { &-h1, &-text { @@ -19,12 +100,4 @@ font-size: 30px; font-weight: lighter; } -} - -noscript { - display: block; - color: $color-theme-error; - font-size: 30px; - font-weight: lighter; - margin-bottom: 20px; } \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index e1484f4e7..19793fd42 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -12,6 +12,10 @@ $color-control: rgba(0, 0, 0, .15); $color-input: #dbe4eb; $color-disabled: #eef1f4; +$color-extern-google: #d34836; +$color-extern-microsoft: #0063b1; +$color-extern-github: #3c4146; + $color-theme-blue: #438cef; $color-theme-blue-dark: #3d7dd5; $color-theme-blue-light: #9ebeea; diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.css b/src/Squidex/app/theme/icomoon/demo-files/demo.css index 579e46d57..f2a23ed9f 100644 --- a/src/Squidex/app/theme/icomoon/demo-files/demo.css +++ b/src/Squidex/app/theme/icomoon/demo-files/demo.css @@ -147,19 +147,19 @@ p { font-size: 16px; } .fs1 { - font-size: 24px; + font-size: 32px; } .fs2 { - font-size: 28px; + font-size: 32px; } .fs3 { - font-size: 20px; + font-size: 24px; } .fs4 { - font-size: 32px; + font-size: 28px; } .fs5 { - font-size: 32px; + font-size: 20px; } .fs6 { font-size: 32px; diff --git a/src/Squidex/app/theme/icomoon/demo.html b/src/Squidex/app/theme/icomoon/demo.html index 9d09974f4..3f8bc85a9 100644 --- a/src/Squidex/app/theme/icomoon/demo.html +++ b/src/Squidex/app/theme/icomoon/demo.html @@ -9,20 +9,20 @@
-

Font Name: icomoon (Glyphs: 63)

+

Font Name: icomoon (Glyphs: 66)

-

Grid Size: 24

+

Grid Size: Unknown

- + - icon-download + icon-brand2
- - + +
liga: @@ -31,1133 +31,1405 @@
- + - icon-control-RichText + icon-github
- - + +
liga:
-
-
-

Grid Size: 14

-
+
- + - icon-bug + icon-activity
- - + +
liga:
-
+
- + - icon-control-Markdown + icon-history
- - + +
liga:
-
+
- + - icon-control-Date + icon-time
- - + +
liga:
-
+
- + - icon-control-DateTime + icon-add
- - + +
liga:
-
+
- + - icon-angle-right + icon-plus
- - + +
liga:
-
+
- + - icon-user-o + icon-check-circle
- - + +
liga:
-
+
- + - icon-caret-right + icon-check-circle-filled
- - + +
liga:
-
+
- + - icon-caret-left + icon-close
- - + +
liga:
-
+
- + - icon-caret-up + icon-content
- - + +
liga:
-
+
- + - icon-caret-down + icon-control-Checkbox
- - + +
liga:
-
+
- + - icon-angle-up + icon-control-Dropdown
- - + +
liga:
-
+
- + - icon-angle-down + icon-control-Input
- - + +
liga:
-
+
- + - icon-angle-left + icon-control-Radio
- - + +
liga:
-
-
-

Grid Size: 20

-
+
- + - icon-info + icon-control-TextArea
- - + +
liga:
-
-
-

Grid Size: 16

-
+
- + - icon-google + icon-control-Toggle
- - + +
liga:
-
+
- + - icon-unlocked + icon-copy
- - + +
liga:
-
+
- + - icon-lock + icon-dashboard
- - + +
liga:
-
+
- + - icon-reset + icon-delete
- - + +
liga:
-
+
- + - icon-pause + icon-bin
- - + +
liga:
-
+
- + - icon-play + icon-delete-filled
- - + +
liga:
-
+
- + - icon-settings2 + icon-document-delete
- - + +
liga:
-
+
- + - icon-bin2 + icon-document-disable
- - + +
liga:
-
-
-

Grid Size: 32

-
+
- + - icon-control-Stars + icon-document-publish
- - + +
liga:
-
+
- + - icon-browser + icon-drag
- - + +
liga:
-
-
-

Grid Size: Unknown

-
+
- + - icon-activity + icon-filter
- - + +
liga:
-
+
- + - icon-history + icon-help
- - + +
liga:
-
+
- + - icon-time + icon-type-Json
- - + +
liga:
-
+
- + - icon-add + icon-json
- - + +
liga:
-
+
- + - icon-plus + icon-location
- - + +
liga:
-
+
- + - icon-check-circle + icon-control-Map
- - + +
liga:
-
+
- + - icon-check-circle-filled + icon-type-Geolocation
- - + +
liga:
-
+
- + - icon-close + icon-logo
- - + +
liga:
-
+
- + - icon-content + icon-media
- - + +
liga:
-
+
- + - icon-control-Checkbox + icon-type-Assets
- - + +
liga:
-
+
- + - icon-control-Dropdown + icon-more
- - + +
liga:
-
+
- + - icon-control-Input + icon-dots
- - + +
liga:
-
+
- + - icon-control-Radio + icon-pencil
- - + +
liga:
-
+
- + - icon-control-TextArea + icon-reference
- - + +
liga:
-
+
- + - icon-control-Toggle + icon-schemas
- - + +
liga:
-
+
- + - icon-copy + icon-search
- - + +
liga:
-
+
- + - icon-dashboard + icon-settings
- - + +
liga:
-
+
- + - icon-delete + icon-type-Boolean
- - + +
liga:
-
+
- + - icon-bin + icon-type-DateTime
- - + +
liga:
-
+
- + - icon-delete-filled + icon-type-Number
- - + +
liga:
-
+
- + - icon-document-delete + icon-type-String
- - + + +
+
+ liga: + +
+
+
+
+ + + + icon-user +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: 16

+
+
+ + + + icon-microsoft +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-google +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-unlocked +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-lock +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-reset +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-pause +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-play +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-settings2 +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-bin2 +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: 24

+
+
+ + + + icon-download +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-RichText +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Date +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-DateTime +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-angle-right +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-user-o +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-caret-right +
+
+ +
liga:
-
+
- + - icon-document-disable + icon-caret-left
- - + +
liga:
-
+
- + - icon-document-publish + icon-caret-up
- - + +
liga:
-
+
+
+

Grid Size: 14

+
- + - icon-drag + icon-bug
- - + +
liga:
-
+
- + - icon-filter + icon-control-Markdown
- - + +
liga:
-
+
- + - icon-help + icon-control-Date
- - + +
liga:
-
+
- + - icon-type-Json + icon-control-DateTime
- - + +
liga:
-
+
- + - icon-json + icon-angle-right
- - + +
liga:
-
+
- + - icon-location + icon-user-o
- - + +
liga:
-
+
- + - icon-control-Map + icon-caret-right
- - + +
liga:
-
+
- + - icon-type-Geolocation + icon-caret-left
- - + +
liga:
-
+
-
- - + +
liga:
-
+
- + - icon-media + icon-caret-down
- - + +
liga:
-
+
- + - icon-type-Assets + icon-angle-up
- - + +
liga:
-
+
- + - icon-more + icon-angle-down
- - + +
liga:
-
+
- + - icon-dots + icon-angle-left
- - + +
liga:
-
+
+
+

Grid Size: 20

+
- + - icon-pencil + icon-info
- - + +
liga:
-
+
- + - icon-reference + icon-unlocked
- - + +
liga:
-
+
- + - icon-schemas + icon-lock
- - + +
liga:
-
+
- + - icon-search + icon-reset
- - + +
liga:
-
+
- + - icon-settings + icon-pause
- - + +
liga:
-
+
- + - icon-type-Boolean + icon-play
- - + +
liga:
-
+
- + - icon-type-DateTime + icon-settings2
- - + +
liga:
-
+
- + - icon-type-Number + icon-bin2
- - + +
liga:
+
+
+

Grid Size: 32

- + - icon-type-String + icon-control-Stars
- - + +
liga: @@ -1166,14 +1438,14 @@
- + - icon-user + icon-browser
- - + +
liga: diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index 35c52055ebd6a4754e7075ff974bb14e2eb97245..96383b86715998cc56016ee9e2f7a9e0cb8769c7 100644 GIT binary patch delta 642 zcmYLHO=uHQ5T4nWq{*fxO|sb}O&ZhmCut+0Nt0}C)9ogfYGRNcQWde$kQPcYwqTKX zN(v%EMXc+^o)x_Jpim3()`MOwUc`eZ@t`0`K`m^Ztp^8YzW2R1-@JLl`_;m?chOYJ z1H8y>nlc9ePk&5(BMY15nj;DTugS01>eW@x#rJ!gdx54D`}zd{8vxmbrN!!k?N9jt zVDb~?WtM2e!Wqv3$O7^HrTSV!tJzD$$4KM7u~MrZe*Z?7P0iBz!+N!`3iEh{_-Epw zo7MW_)1u`NKp~3vR#$GV0iz}4?o$W>W4Nm>;1uT6XLufS+dpuBT-`W*r`a8K0T=*| z+N{l5BwhD+2+*v7eqAmB&>!o1NZ zRf@yYe!h@qNsHNRNwRc-_fHQOE0WJ>9qAt}o#34YU!}P;eUCOVW0DlsrH>;dA0|1=1mpwNGxFr* zCnp}zJlDm*VA}$e@5oK8C}4e~wPi%S@ofievC zTR`#*%*>3tCZ{mkGulmF!syLtxA_C(Wh+IH0)}Ne@6_Y@ZN4&avw- eC3fbF&YSPp@i0#QV&B2WzzCFZ-rVJIh!FslVMOQv diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg index 95b4fcd2f..3bbec56c1 100644 --- a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg +++ b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg @@ -70,4 +70,7 @@ + + + \ No newline at end of file diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index ff432315ef0d7ec5f0c2d5cbebf102faaab6c2bf..8388e87c8c63560339aea86fc06bed1603c0d203 100644 GIT binary patch delta 639 zcmXw%UrbVQ6vxlGKR~z`$iE8+5~2c1LqXs=MYxc&AXaW+wH6dZlQBs)UBf4EZLQ7L zN_xrnw%&bd(>8kRxj!$hhn}j3dTPGpcJ17@&h|Usea@fX&hPB^vHj`+wgCY^4;^4I zb>-@0)M(ZODRM;TPPI{K+ONHT2Y_zk;#y_1NjgBRsCI3A`>q)J@r}3zkX)&)R#psu zipK!+UnrlbQ9#G3F94*h#G|#wR!goL3&bdaZXv% z>1P$N9$cq6dD^>B6}5l>=3`Rr>ft&kUN|4 z4re^M^R|BGaF1t;A%hJEtc9I&Y-#tf&U%Vp-fx!&S>4R*!e0BWa~_A5MdviU)5oiw zVmUvt;No*}7S(CBx+sh1c-O*2zAQS``l-?B!WrJIa+cfs%gQv$l9jJP7IOV|-8LqV zK~F0s`*c}5`(L|KQpClBR%L7}4klAC F@h{Mtg^mCK delta 288 zcmZ3{%y^}~o`I2pfq|Q$fq|L9z&}{uh+l|J2`I7$h!c`?6AQ${E*)iHV3Yy!L(&tA z3xKo$kPo6c(sL@)4(@v_2;^5VFjy_gNKH)PTgGJ1z+eMXZ!o|N0P7ZU8t5;0kBL~fn&S$;oVYcjeD=D*8ZhyX?f!&d3yk{8ywm9?Q40l zMC-VhGEt84SK**cCVAJX9_Hu-^|^9&y++iIYukS-ZTw3jmc&nFJQ9u6hBTj zL#H?DJzg__3gD>C+N=Q#I;aDk0OszIZu3a@fSQz%0yYu?D8NZ54QyZ=Plc5ycsr*g z5=!Dc)|7`4oZ@{v`l15XhRbxKL8D-iT!PISjl|HSaR>Yua7P0!3|OOb9Ob&7MZ!_p zkMe}o7ROMK&jG8Z!-W>fC>)E%B3b1)<@~~s0F~XX+3@^k-rR+il*u#XaeD{nttyk= zS4fTaI#dQJojsZCA4s~gXDtS1?;TAQd`1g)N%_rUcy?>I!rFYE+o>0OSj8mj{Qb78 z<1V|F1*bHkV^CB(S7Sw6Ax*&^WMQ3^}SCk!U{lsu6e?l~=9L4(196t@U z_s{DA@Hu}vuXV-_Ku;~D@p(o&`A?hMF&Q(hy_f;D^i|}E=KC|-#=ggWuUtW@sCAHO I72|L5H#dTb5&!@I delta 337 zcmey;%($R|QKa18&5ePP0SFuv7`Q>S{gufN9fc=dq%s-K8)UscAIA~ zUba#MDPdTq^G-dU-{va=Hw&15Rr%h15CcR{_OUZ(bl%)z$HO>zj(vv!IIx@ - + diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 26f183ffa..ebd9d0a3e 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -9,10 +9,10 @@ - - + + - + diff --git a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj index b1ccd6f2d..1f2872f46 100644 --- a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj +++ b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj index fc82fe580..b54978d65 100644 --- a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj +++ b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj @@ -14,7 +14,7 @@ - +