diff --git a/Directory.Packages.props b/Directory.Packages.props index 90a35bf..f4732ee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,12 +7,16 @@ - + + + + + diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 8544947..34570d3 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 0af5c4a..cd97117 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -1,4 +1,5 @@ -using Hutopy.Application.Common.Models; +using Google.Apis.Oauth2.v2.Data; +using Hutopy.Application.Common.Models; namespace Hutopy.Application.Common.Interfaces; @@ -9,10 +10,12 @@ public interface IIdentityService Task FindUserByIdAsync(string id); Task GetCurrentUserAsync(); Task FindUserByEmailAsync(string id); + Task LoginAsync(string email, string password); Task GetUserByUserNameAsync(string userName); Task IsInRoleAsync(string userId, string role); Task AuthorizeAsync(string userId, string policyName); Task AddRoleAsync(string userId, string role); Task> GetCurrentUserRolesAsync(); + Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo); Task DeleteUserAsync(string userId); } diff --git a/src/Application/Users/Commands/Login.cs b/src/Application/Users/Commands/Login.cs new file mode 100644 index 0000000..f61f810 --- /dev/null +++ b/src/Application/Users/Commands/Login.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; +public record LoginCommand : IRequest +{ + public required string EmailAddress { get; init; } + public required string Password { get; init; } +} + +public class LoginCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IIdentityService _identityService; + + public LoginCommandHandler(IApplicationDbContext context, IIdentityService identityService) + { + _context = context; + _identityService = identityService; + } + + public async Task Handle(LoginCommand request, CancellationToken cancellationToken) + { + var jwt = await _identityService.LoginAsync(request.EmailAddress, request.Password); + + return jwt ?? "Invalid login credentials"; + } +} diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index efab6e8..56e216d 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 31b0b30..42ca0c3 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -48,13 +48,20 @@ public static class DependencyInjection .AddBearerToken(IdentityConstants.BearerScheme); services.AddAuthorizationBuilder(); - - // Might need to change and use AddIdentity() when we need to integrate connection via third party ( facebook, google ) - services - .AddIdentityCore() + + services.AddIdentityCore(options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 8; + }) .AddRoles() .AddEntityFrameworkStores() - .AddApiEndpoints(); + .AddApiEndpoints() + .AddSignInManager>() + .AddDefaultTokenProviders(); services.AddSingleton(TimeProvider.System); services.AddScoped(); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 3326217..02b5ce0 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,17 +1,22 @@ +using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; +using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; namespace Hutopy.Infrastructure.Identity; public class IdentityService( UserManager userManager, + SignInManager signInManager, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, IAuthorizationService authorizationService, - IHttpContextAccessor contextAccessor + IHttpContextAccessor contextAccessor, + IConfiguration configuration ) : IIdentityService { @@ -53,6 +58,23 @@ public class IdentityService( return (result.ToApplicationResult(), user.Id); } + public async Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo) + { + var user = new ApplicationUser + { + UserName = userInfo.Name, + Email = userInfo.Email, + FirstName = userInfo.GivenName, + LastName = userInfo.FamilyName + }; + + var password = Guid.NewGuid().ToString("N")[..32]; + + var result = await userManager.CreateAsync(user, password); + + return (result.ToApplicationResult(), user.Id); + } + public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { var applicationUser = new ApplicationUser @@ -187,4 +209,23 @@ public class IdentityService( return userRoles; } + + public async Task LoginAsync(string userName, string password) + { + var result = await signInManager.PasswordSignInAsync(userName, password, isPersistent: false, lockoutOnFailure: false); + + if (!result.Succeeded) + { + return null; + } + + var user = await GetUserByUserNameAsync(userName); + var token = JwtTokenHelper.GenerateJwtToken( + issuer: configuration["Jwt-Issuer"] ?? "", + audience: configuration["Jwt-Audience"] ?? "", + key: configuration["Jwt-Key"] ?? "", + userId: user?.Id ?? ""); + + return token; + } } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 2958bd6..108c958 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -4,6 +4,7 @@ Hutopy.Infrastructure + diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs new file mode 100644 index 0000000..9f01d6a --- /dev/null +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -0,0 +1,31 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Hutopy.Infrastructure.Utils; + +public static class JwtTokenHelper +{ + public static string GenerateJwtToken(string issuer, string audience, string key, string userId) + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.NameIdentifier, userId) + }; + + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} diff --git a/src/Infrastructure/Utils/RandomGenerator.cs b/src/Infrastructure/Utils/RandomGenerator.cs new file mode 100644 index 0000000..1e5a9a2 --- /dev/null +++ b/src/Infrastructure/Utils/RandomGenerator.cs @@ -0,0 +1,58 @@ +using System.Text; + +namespace Hutopy.Infrastructure.Utils; + +// If we need to add special characters we can alternate between 2 pools. +public class RandomGenerator +{ + // For the moment, numbers and special characters don't work because + // the random generator is designed to handle a single integer. + // We can modify this in the future. + private const string LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789" + + "!@#$%^&*()_+" + + "-=[];',./`~{}|:\"<>?"; + private const int LetterIdxBits = 6; + private const int LetterIdxMask = 1 << LetterIdxBits; + private const int LetterIdxMax = 64 / LetterIdxBits; + + private static readonly Random Src = new(); + + public static byte[] RandBytesMaskSrc(int n) + { + var b = new byte[n]; + + for (var i = n - 1; i >= 0;) + { + long cache = Src.NextInt64(); + int remain = LetterIdxMax; + + while (remain != 0) + { + if (i < 0) + break; + + if (cache == 0) + cache = Src.NextInt64(); + + var idx = (int)(cache & LetterIdxMask); + if (idx < LetterBytes.Length) + { + b[i] = (byte)LetterBytes[idx]; + i--; + } + + cache >>= LetterIdxBits; + remain--; + } + } + + return b; + } + + public static string RandomString(int length) + { + var bytes = RandBytesMaskSrc(length); + return Encoding.UTF8.GetString(bytes); // Equivalent for *(string*)(&bytes[0]) + } +} diff --git a/src/Web/Controllers/FacebookController.cs b/src/Web/Controllers/FacebookController.cs new file mode 100644 index 0000000..679502e --- /dev/null +++ b/src/Web/Controllers/FacebookController.cs @@ -0,0 +1,53 @@ +using System.Security.Claims; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Infrastructure.Utils; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Facebook; +using Microsoft.AspNetCore.Mvc; + +namespace Hutopy.Web.Controllers; + +public class FacebookController(IIdentityService identityService) : Controller +{ + [HttpGet("/api/facebook/sign-in")] + public async Task SignIn() + { + await HttpContext.ChallengeAsync(FacebookDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = Url.Action("Authorize") + }); + } + + public async Task Authorize() + { + var authenticateResult = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme); + + if (!authenticateResult.Succeeded) return BadRequest(); + + var claims = authenticateResult.Principal.Claims.ToList(); + + var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? ""; + var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? ""; + var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? ""; + var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? ""; + + var claimsIdentity = new ClaimsIdentity(new List + { + new(ClaimTypes.Name, name), + new(ClaimTypes.Email, email), + new(ClaimTypes.GivenName, givenName), + new(ClaimTypes.Surname, familyName) + }, CookieAuthenticationDefaults.AuthenticationScheme); + + if (await identityService.FindUserByEmailAsync(email) != null) + { + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + return Redirect("/"); + } + + await identityService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + return Redirect("/"); + } +} diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs new file mode 100644 index 0000000..6df233a --- /dev/null +++ b/src/Web/Controllers/GoogleController.cs @@ -0,0 +1,77 @@ +using System.Security.Claims; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Infrastructure.Utils; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace Hutopy.Web.Controllers; + +public class GoogleController(IIdentityService identityService, IHttpClientFactory httpClientFactory, IConfiguration configuration) : Controller +{ + [HttpPost("/api/google/sign-in")] + public async Task SignIn([FromBody] GoogleSignInRequest request) + { + var httpClient = httpClientFactory.CreateClient(); + // Verify the token with Google + var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}"); + if (!response.IsSuccessStatusCode) + { + return BadRequest("Invalid Google token."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var email = payload["email"]?.ToString() ?? ""; + var name = payload["name"]?.ToString() ?? ""; + var givenName = payload["given_name"]?.ToString() ?? ""; + var familyName = payload["family_name"]?.ToString() ?? ""; + + if (string.IsNullOrEmpty(email)) + { + return BadRequest("Google token did not contain an email."); + } + + // Check if user exists or create a new one + var user = await identityService.FindUserByEmailAsync(email); + if (user == null) + { + await identityService.CreateUserAsync(email, email, givenName, familyName, RandomGenerator.RandomString(24)); + user = await identityService.FindUserByEmailAsync(email); + } + + if (user?.Id is null) + { + return BadRequest("Unable to find or create the user."); + } + + // Sign in the user + var claims = new List + { + new(ClaimTypes.Name, name), + new(ClaimTypes.Email, email), + new(ClaimTypes.GivenName, givenName), + new(ClaimTypes.Surname, familyName) + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + + var issuer = configuration["Jwt-Issuer"] ?? + throw new ArgumentNullException("The Jwt issuer is missing."); + var audience = configuration["Jwt-Audience"] ?? + throw new ArgumentNullException("The Jwt audience is missing."); + var key = configuration["Jwt-Key"] ?? + throw new ArgumentNullException("The Jwt key is missing."); + + var jwtToken = JwtTokenHelper.GenerateJwtToken(issuer, audience, key, user.Id); + + return Ok(new { accessToken = jwtToken, email }); + } + + public class GoogleSignInRequest + { + public required string AccessToken { get; set; } + } +} diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index f8ede38..3a7eb61 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -1,8 +1,14 @@ -using Azure.Identity; +using System.Text; +using Azure.Identity; using Hutopy.Application.Common.Interfaces; using Hutopy.Infrastructure.Data; using Hutopy.Web.Services; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Facebook; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; using NSwag; using NSwag.Generation.Processors.Security; @@ -24,6 +30,8 @@ public static class DependencyInjection services.AddExceptionHandler(); services.AddRazorPages(); + + services.AddHttpClient(); // Customise default API behaviour services.Configure(options => @@ -62,4 +70,48 @@ public static class DependencyInjection return services; } + + public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, ConfigurationManager configuration) + { + services.AddAuthentication(options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie("Identity.Application", options => + { + options.LoginPath = "/api/Users/login"; + }) + .AddCookie() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => + { + jwtBearerOptions.Authority = "https://hutopy.com"; + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = configuration["Jwt-Issuer"], + ValidateAudience = true, + ValidAudience = configuration["Jwt-Audience"], + ValidateLifetime = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt-Key"] ?? + throw new ArgumentNullException("The Jwt Key is missing."))) + }; + }) + .AddGoogle(GoogleDefaults.AuthenticationScheme, options => + { + options.ClientId = configuration["Google-ClientId"] ?? + throw new ArgumentNullException("The Google ClientId is missing.");; + options.ClientSecret = configuration["Google-ClientSecret"] ?? + throw new ArgumentNullException("The Google ClientSecret is missing.");; + }) + .AddFacebook(FacebookDefaults.AuthenticationScheme, options => + { + options.ClientId = configuration["Facebook-ClientId"] ?? + throw new ArgumentNullException("The Facebook ClientId is missing."); + options.ClientSecret = configuration["Facebook-ClientSecret"] ?? + throw new ArgumentNullException("The Facebook ClientSecret is missing."); + }); + + return services; + } } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index b0b9c59..39f784a 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -1,6 +1,5 @@ using Hutopy.Application.Users.Commands; using Hutopy.Application.Users.Queries.GetMinimalUser; -using Hutopy.Infrastructure.Identity; namespace Hutopy.Web.Endpoints; @@ -10,9 +9,8 @@ public class Users : EndpointGroupBase { app.MapGroup(this) .MapPost(CreateUser) - .MapGet(GetMinimalUser) - .MapIdentityApi(); - + .MapPost(Login, "/login") + .MapGet(GetMinimalUser); } private static async Task CreateUser(ISender sender, CreateUserCommand command) @@ -24,5 +22,9 @@ public class Users : EndpointGroupBase { return await sender.Send(query); } - + + private static async Task Login(ISender sender, LoginCommand command) + { + return await sender.Send(command); + } } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 646e478..781bdc2 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -3,6 +3,7 @@ using Hutopy.Infrastructure; using Hutopy.Infrastructure.Data; using Hutopy.Web; using Azure.Identity; +using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); @@ -44,13 +45,22 @@ builder.Services.AddKeyVaultIfConfigured(builder.Configuration); builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); +builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); +builder.Services.AddControllers(); var app = builder.Build(); +app.UseForwardedHeaders( + new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto } +); + app.UseCors("AllowAll"); app.UseCors("AllowHutopyUi"); app.UseCors("AllowHutopyUiPreview"); +app.UseAuthentication(); +app.UseAuthorization(); + // Initialize and seed the db. await app.InitialiseDatabaseAsync(); @@ -75,8 +85,6 @@ app.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); -app.MapRazorPages(); - app.MapFallbackToFile("index.html"); app.UseExceptionHandler(options => { }); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index a5444e7..9193242 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -14,6 +14,9 @@ + + + diff --git a/src/Web/appsettings.Development.dist b/src/Web/appsettings.Development.dist index 58d6716..25369fa 100644 --- a/src/Web/appsettings.Development.dist +++ b/src/Web/appsettings.Development.dist @@ -7,23 +7,11 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "Google": { - "ClientId": "", - "ClientSecret": "", - "ProjectId": "", - "AuthUri": "", - "TokenUri": "", - "AuthProviderX509CertUrl": "", - "RedirectUris": [ - "https://hutopy.ca", - "https://hutopy.com", - "http://localhost" - ], - "JavascriptOrigins": [ - "https://hutopy.ca", - "https://hutopy.com", - "http://localhost" - ] - } -} - + "Google-ClientId": "", + "Google-ClientSecret": "", + "Facebook-ClientId": "", + "Facebook-ClientSecret": "", + "Jwt-Audience": "", + "Jwt-Issuer": "", + "Jwt-Key": "", +} \ No newline at end of file diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index ece282e..93ac3dc 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -281,75 +281,22 @@ } } }, - "/api/Users/register": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRegister", - "requestBody": { - "x-name": "registration", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, "/api/Users/login": { "post": { "tags": [ "Users" ], - "operationId": "PostApiUsersLogin", - "parameters": [ - { - "name": "useCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "useSessionCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 3 - } - ], + "operationId": "Login", "requestBody": { - "x-name": "login", + "x-name": "command", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LoginRequest" + "$ref": "#/components/schemas/LoginCommand" } } }, + "required": true, "x-position": 1 }, "responses": { @@ -358,7 +305,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" + "type": "string" } } } @@ -366,305 +313,6 @@ } } }, - "/api/Users/refresh": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRefresh", - "requestBody": { - "x-name": "refreshRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" - } - } - } - } - } - } - }, - "/api/Users/confirmEmail": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersConfirmEmail", - "parameters": [ - { - "name": "userId", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "code", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "changedEmail", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 3 - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/resendConfirmationEmail": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResendConfirmationEmail", - "requestBody": { - "x-name": "resendRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResendConfirmationEmailRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/forgotPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersForgotPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForgotPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/resetPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResetPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResetPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/manage/2fa": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersManage2fa", - "requestBody": { - "x-name": "tfaRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/Users/manage/info": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersManageInfo", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - }, - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersManageInfo", - "requestBody": { - "x-name": "infoRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, "/api/WeatherForecasts": { "get": { "tags": [ @@ -692,6 +340,52 @@ } ] } + }, + "/api/facebook/sign-in": { + "get": { + "tags": [ + "Facebook" + ], + "operationId": "Facebook_SignIn", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/api/google/sign-in": { + "post": { + "tags": [ + "Google" + ], + "operationId": "Google_SignIn", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GoogleSignInRequest" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } } }, "components": { @@ -968,6 +662,18 @@ } } }, + "LoginCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "emailAddress": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "MinimalUserDto": { "type": "object", "additionalProperties": false, @@ -983,231 +689,6 @@ } } }, - "HttpValidationProblemDetails": { - "allOf": [ - { - "$ref": "#/components/schemas/ProblemDetails" - }, - { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "errors": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - ] - }, - "ProblemDetails": { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - } - }, - "RegisterRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "AccessTokenResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "tokenType": { - "type": "string" - }, - "accessToken": { - "type": "string" - }, - "expiresIn": { - "type": "integer", - "format": "int64" - }, - "refreshToken": { - "type": "string" - } - } - }, - "LoginRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "twoFactorCode": { - "type": "string", - "nullable": true - }, - "twoFactorRecoveryCode": { - "type": "string", - "nullable": true - } - } - }, - "RefreshRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "refreshToken": { - "type": "string" - } - } - }, - "ResendConfirmationEmailRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ForgotPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ResetPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "resetCode": { - "type": "string" - }, - "newPassword": { - "type": "string" - } - } - }, - "TwoFactorResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "sharedKey": { - "type": "string" - }, - "recoveryCodesLeft": { - "type": "integer", - "format": "int32" - }, - "recoveryCodes": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "isTwoFactorEnabled": { - "type": "boolean" - }, - "isMachineRemembered": { - "type": "boolean" - } - } - }, - "TwoFactorRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "enable": { - "type": "boolean", - "nullable": true - }, - "twoFactorCode": { - "type": "string", - "nullable": true - }, - "resetSharedKey": { - "type": "boolean" - }, - "resetRecoveryCodes": { - "type": "boolean" - }, - "forgetMachine": { - "type": "boolean" - } - } - }, - "InfoResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "isEmailConfirmed": { - "type": "boolean" - } - } - }, - "InfoRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "newEmail": { - "type": "string", - "nullable": true - }, - "newPassword": { - "type": "string", - "nullable": true - }, - "oldPassword": { - "type": "string", - "nullable": true - } - } - }, "WeatherForecast": { "type": "object", "additionalProperties": false, @@ -1229,6 +710,15 @@ "nullable": true } } + }, + "GoogleSignInRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "accessToken": { + "type": "string" + } + } } }, "securitySchemes": {