diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 558293e1d..93ec8eb6c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.10", + "version": "8.0.5", "commands": [ "dotnet-ef" ] diff --git a/.idea/.idea.LexBox/.idea/.gitignore b/.idea/.idea.LexBox/.idea/.gitignore index 4ec73edd9..daa11e94f 100644 --- a/.idea/.idea.LexBox/.idea/.gitignore +++ b/.idea/.idea.LexBox/.idea/.gitignore @@ -11,3 +11,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/.idea.LexBox/.idea/dataSources.xml b/.idea/.idea.LexBox/.idea/dataSources.xml index eceae86ec..b6ad8028d 100644 --- a/.idea/.idea.LexBox/.idea/dataSources.xml +++ b/.idea/.idea.LexBox/.idea/dataSources.xml @@ -5,32 +5,17 @@ postgresql true org.postgresql.Driver - jdbc:postgresql://localhost/lexbox + jdbc:postgresql://localhost:5433/lexbox - + - $ProjectFileDir$ - - mariadb - true - redmine data - org.mariadb.jdbc.Driver - jdbc:mariadb://localhost:3306 - - - - - - - $ProjectFileDir$ - postgresql true @@ -65,5 +50,41 @@ $ProjectFileDir$ + + mongo + true + true + com.dbschema.MongoJdbcDriver + mongodb://localhost:27018 + $ProjectFileDir$ + + + mongo + true + true + com.dbschema.MongoJdbcDriver + mongodb://localhost:27017 + + + + + + $ProjectFileDir$ + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:C:\dev\LexBox\backend\LocalWebApp\sena-3.sqlite + + + + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar + + + - + \ No newline at end of file diff --git a/.idea/.idea.LexBox/.idea/indexLayout.xml b/.idea/.idea.LexBox/.idea/indexLayout.xml index 3a934820b..24b63bcb7 100644 --- a/.idea/.idea.LexBox/.idea/indexLayout.xml +++ b/.idea/.idea.LexBox/.idea/indexLayout.xml @@ -20,4 +20,4 @@ frontend/.svelte-kit/output - \ No newline at end of file + diff --git a/.idea/.idea.LexBox/.idea/vcs.xml b/.idea/.idea.LexBox/.idea/vcs.xml index 30edf8684..9041d4ebe 100644 --- a/.idea/.idea.LexBox/.idea/vcs.xml +++ b/.idea/.idea.LexBox/.idea/vcs.xml @@ -12,5 +12,6 @@ + - \ No newline at end of file + diff --git a/backend/LexBoxApi/Auth/AuthKernel.cs b/backend/LexBoxApi/Auth/AuthKernel.cs index 892deaa5c..38fae368b 100644 --- a/backend/LexBoxApi/Auth/AuthKernel.cs +++ b/backend/LexBoxApi/Auth/AuthKernel.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Text; @@ -5,12 +6,14 @@ using LexBoxApi.Auth.Requirements; using LexBoxApi.Controllers; using LexCore.Auth; +using LexData; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Logging; using Microsoft.OpenApi.Models; +using OpenIddict.Validation.AspNetCore; namespace LexBoxApi.Auth; @@ -66,6 +69,8 @@ public static void AddLexBoxAuth(IServiceCollection services, .BindConfiguration("Authentication:Jwt") .ValidateDataAnnotations() .ValidateOnStart(); + services.AddOptions() + .BindConfiguration("Authentication:OpenId"); services.AddAuthentication(options => { options.DefaultScheme = DefaultScheme; @@ -78,7 +83,13 @@ public static void AddLexBoxAuth(IServiceCollection services, { options.ForwardDefaultSelector = context => { - + if (context.Request.Headers.ContainsKey("Authorization") && + context.Request.Headers.Authorization.ToString().StartsWith("Bearer") && + context.RequestServices.GetService>()?.Value.Enable == true) + { + //fow now this will use oauth + return OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + } if (context.Request.IsJwtRequest()) { return JwtBearerDefaults.AuthenticationScheme; @@ -101,9 +112,9 @@ public static void AddLexBoxAuth(IServiceCollection services, .AddCookie(options => { configuration.Bind("Authentication:Cookie", options); - options.LoginPath = "/api/login"; + options.LoginPath = "/login"; options.Cookie.Name = AuthCookieName; - options.ForwardChallenge = JwtBearerDefaults.AuthenticationScheme; + // options.ForwardChallenge = JwtBearerDefaults.AuthenticationScheme; options.ForwardForbid = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => @@ -152,7 +163,8 @@ public static void AddLexBoxAuth(IServiceCollection services, context.HandleResponse(); var loginController = context.HttpContext.RequestServices.GetRequiredService(); loginController.ControllerContext.HttpContext = context.HttpContext; - var redirectTo = await loginController.CompleteGoogleLogin(context.Principal, context.Properties?.RedirectUri); + //using context.ReturnUri and not context.Properties.RedirectUri because the latter is null + var redirectTo = await loginController.CompleteGoogleLogin(context.Principal, context.ReturnUri); context.HttpContext.Response.Redirect(redirectTo); }; }); @@ -185,6 +197,65 @@ public static void AddLexBoxAuth(IServiceCollection services, } }); }); + + var openIdOptions = configuration.GetSection("Authentication:OpenId").Get(); + if (openIdOptions?.Enable == true) AddOpenId(services, environment); + } + + private static void AddOpenId(IServiceCollection services, IWebHostEnvironment environment) + { + services.Add(ScopeRequestFixer.Descriptor.ServiceDescriptor); + //openid server + services.AddOpenIddict() + .AddCore(options => + { + options.UseEntityFrameworkCore().UseDbContext(); + options.UseQuartz(); + }) + .AddServer(options => + { + options.RegisterScopes("openid", "profile", "email"); + //todo add application claims + options.RegisterClaims("aud", "email", "exp", "iss", "iat", "sub", "name"); + options.SetAuthorizationEndpointUris("api/oauth/open-id-auth"); + options.SetTokenEndpointUris("api/oauth/token"); + options.SetIntrospectionEndpointUris("api/oauth/introspect"); + options.SetUserinfoEndpointUris("api/oauth/userinfo"); + options.Configure(serverOptions => serverOptions.Handlers.Add(ScopeRequestFixer.Descriptor)); + + options.AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow(); + + options.RequireProofKeyForCodeExchange();//best practice to use PKCE with auth code flow and no implicit flow + + options.IgnoreResponseTypePermissions(); + options.IgnoreScopePermissions(); + if (environment.IsDevelopment()) + { + options.AddDevelopmentEncryptionCertificate(); + options.AddDevelopmentSigningCertificate(); + } + else + { + //see docs: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html + throw new NotImplementedException("need to implement loading keys from a file"); + } + + var aspnetCoreBuilder = options.UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableTokenEndpointPassthrough(); + if (environment.IsDevelopment()) + { + aspnetCoreBuilder.DisableTransportSecurityRequirement(); + } + }) + .AddValidation(options => + { + options.UseLocalServer(); + options.UseAspNetCore(); + options.AddAudiences(Enum.GetValues().Where(a => a != LexboxAudience.Unknown).Select(a => a.ToString()).ToArray()); + options.EnableAuthorizationEntryValidation(); + }); } public static AuthorizationPolicyBuilder RequireDefaultLexboxAuth(this AuthorizationPolicyBuilder builder) diff --git a/backend/LexBoxApi/Auth/OpenIdOptions.cs b/backend/LexBoxApi/Auth/OpenIdOptions.cs new file mode 100644 index 000000000..eeef9e5e2 --- /dev/null +++ b/backend/LexBoxApi/Auth/OpenIdOptions.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace LexBoxApi.Auth; + +public class OpenIdOptions +{ + [Required] + public required bool Enable { get; set; } +} diff --git a/backend/LexBoxApi/Auth/ScopeRequestFixer.cs b/backend/LexBoxApi/Auth/ScopeRequestFixer.cs new file mode 100644 index 000000000..9048091ca --- /dev/null +++ b/backend/LexBoxApi/Auth/ScopeRequestFixer.cs @@ -0,0 +1,28 @@ +using OpenIddict.Abstractions; +using OpenIddict.Server; + +namespace LexBoxApi.Auth; + +/// +/// the MSAL library makes requests with the scope parameter, which is invalid, this attempts to remove the scope before it's rejected +/// +public sealed class ScopeRequestFixer : IOpenIddictServerHandler +{ + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(OpenIddictServerHandlers.Exchange.ValidateResourceOwnerCredentialsParameters.Descriptor.Order + 1) + .SetType(OpenIddictServerHandlerType.Custom) + .Build(); + + public ValueTask HandleAsync(OpenIddictServerEvents.ValidateTokenRequestContext context) + { + if (!string.IsNullOrEmpty(context.Request.Scope) && (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsDeviceCodeGrantType())) + { + context.Request.Scope = null; + } + + return default; + } +} diff --git a/backend/LexBoxApi/Controllers/LoginController.cs b/backend/LexBoxApi/Controllers/LoginController.cs index 944ead716..34466ab10 100644 --- a/backend/LexBoxApi/Controllers/LoginController.cs +++ b/backend/LexBoxApi/Controllers/LoginController.cs @@ -8,13 +8,9 @@ using LexCore.Auth; using LexData; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Http; -using LexCore.Entities; using System.Security.Claims; using Microsoft.AspNetCore.Authentication.Google; @@ -28,8 +24,7 @@ public class LoginController( LoggedInContext loggedInContext, EmailService emailService, UserService userService, - TurnstileService turnstileService, - ProjectService projectService) + TurnstileService turnstileService) : ControllerBase { /// @@ -38,7 +33,6 @@ public class LoginController( /// [HttpGet("loginRedirect")] [AllowAnyAudience] - public async Task LoginRedirect( string jwt, // This is required because auth looks for a jwt in the query string string returnTo) @@ -53,6 +47,7 @@ public async Task LoginRedirect( return await EmailLinkExpired(); } } + await HttpContext.SignInAsync(User, new AuthenticationProperties { IsPersistent = true }); return Redirect(returnTo); @@ -87,6 +82,7 @@ public async Task CompleteGoogleLogin(ClaimsPrincipal? principal, string { (authUser, userEntity) = await lexAuthService.GetUser(googleEmail); } + if (authUser is null) { authUser = new LexAuthUser() @@ -102,19 +98,20 @@ public async Task CompleteGoogleLogin(ClaimsPrincipal? principal, string Locale = locale ?? LexCore.Entities.User.DefaultLocalizationCode, Locked = null, }; - var queryParams = new Dictionary() { - {"email", googleEmail}, - {"name", googleName}, - {"returnTo", returnTo}, + var queryParams = new Dictionary() + { + { "email", googleEmail }, { "name", googleName }, { "returnTo", returnTo }, }; var queryString = QueryString.Create(queryParams); returnTo = "/register" + queryString.ToString(); } + if (userEntity is not null && !foundGoogleId) { userEntity.GoogleId = googleId; await lexBoxDbContext.SaveChangesAsync(); } + await HttpContext.SignInAsync(authUser.GetPrincipal("google"), new AuthenticationProperties { IsPersistent = true }); return returnTo; @@ -158,6 +155,7 @@ public async Task> VerifyEmail( return Redirect(returnTo); } + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -219,7 +217,9 @@ public async Task ForgotPassword(ForgotPasswordInput input) return Ok(); } - public record ResetPasswordRequest([Required(AllowEmptyStrings = false)] string PasswordHash, int? PasswordStrength); + public record ResetPasswordRequest( + [Required(AllowEmptyStrings = false)] string PasswordHash, + int? PasswordStrength); [HttpPost("resetPassword")] [RequireAudience(LexboxAudience.ForgotPassword)] diff --git a/backend/LexBoxApi/Controllers/OauthController.cs b/backend/LexBoxApi/Controllers/OauthController.cs new file mode 100644 index 000000000..b7024a295 --- /dev/null +++ b/backend/LexBoxApi/Controllers/OauthController.cs @@ -0,0 +1,307 @@ +using System.Security.Claims; +using System.Text.Json; +using LexBoxApi.Auth; +using LexCore.Auth; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +namespace LexBoxApi.Controllers; + +[ApiController] +[Route("/api/oauth")] +public class OauthController( + LoggedInContext loggedInContext, + IOpenIddictApplicationManager applicationManager, + IOpenIddictAuthorizationManager authorizationManager +) : ControllerBase +{ + + [HttpGet("open-id-auth")] + [HttpPost("open-id-auth")] + [ProducesResponseType(400)] + [ProducesDefaultResponseType] + public async Task Authorize() + { + var request = HttpContext.GetOpenIddictServerRequest(); + if (request is null) + { + return BadRequest(); + } + + if (IsAcceptRequest()) + { + return await FinishSignIn(loggedInContext.User, request); + } + + // Retrieve the user principal stored in the authentication cookie. + // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. + var result = await HttpContext.AuthenticateAsync(); + var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; + if (!result.Succeeded || + lexAuthUser is null || + request.HasPrompt(OpenIddictConstants.Prompts.Login) || + IsExpired(request, result)) + { + // If the client application requested promptless authentication, + // return an error indicating that the user is not logged in. + if (request.HasPrompt(OpenIddictConstants.Prompts.None)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = + OpenIddictConstants.Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The user is not logged in." + })); + } + + // To avoid endless login -> authorization redirects, the prompt=login flag + // is removed from the authorization request payload before redirecting the user. + var prompt = string.Join(" ", request.GetPrompts().Remove(OpenIddictConstants.Prompts.Login)); + + var parameters = Request.HasFormContentType + ? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList() + : Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList(); + + parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt))); + + return Challenge( + authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) + }); + } + + var userId = lexAuthUser.Id.ToString(); + var requestClientId = request.ClientId; + ArgumentException.ThrowIfNullOrEmpty(requestClientId); + var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? + throw new InvalidOperationException( + "Details concerning the calling client application cannot be found."); + var applicationId = await applicationManager.GetIdAsync(application) ?? + throw new InvalidOperationException("The calling client application could not be found."); + + // Retrieve the permanent authorizations associated with the user and the calling client application. + var authorizations = await authorizationManager.FindAsync( + subject: userId, + client: applicationId, + status: OpenIddictConstants.Statuses.Valid, + type: OpenIddictConstants.AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + switch (await applicationManager.GetConsentTypeAsync(application)) + { + // If the consent is implicit or if an authorization was found, + // return an authorization response without displaying the consent form. + case OpenIddictConstants.ConsentTypes.Implicit: + case OpenIddictConstants.ConsentTypes.External when authorizations.Count is not 0: + case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(OpenIddictConstants.Prompts.Consent): + + return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + + // If the consent is external (e.g when authorizations are granted by a sysadmin), + // immediately return an error if no authorization can be found in the database. + case OpenIddictConstants.ConsentTypes.External when authorizations.Count is 0: + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + + // At this point, no authorization was found in the database and an error must be returned + // if the client application specified prompt=none in the authorization request. + case OpenIddictConstants.ConsentTypes.Explicit when request.HasPrompt(OpenIddictConstants.Prompts.None): + case OpenIddictConstants.ConsentTypes.Systematic when request.HasPrompt(OpenIddictConstants.Prompts.None): + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "Interactive user consent is required." + })); + + // In every other case, send user to consent page + default: + var parameters = Request.HasFormContentType + ? Request.Form.ToList() + : Request.Query.ToList(); + var data = JsonSerializer.Serialize(parameters.ToDictionary(pair => pair.Key, pair => pair.Value.ToString())); + var queryString = new QueryString() + .Add("appName", await applicationManager.GetDisplayNameAsync(application) ?? "Unknown app") + .Add("scope", request.Scope ?? "") + .Add("postback", data); + return Redirect($"/authorize{queryString.Value}"); + } + } + + private static bool IsExpired(OpenIddictRequest request, AuthenticateResult result) + { + // If a max_age parameter was provided, ensure that the cookie is not too old. + return (request.MaxAge != null && result.Properties?.IssuedUtc != null && + DateTimeOffset.UtcNow - result.Properties.IssuedUtc > + TimeSpan.FromSeconds(request.MaxAge.Value)); + } + + private bool IsAcceptRequest() + { + return Request.Method == "POST" && Request.Form.ContainsKey("submit.accept") && User.Identity?.IsAuthenticated == true; + } + + [HttpPost("token")] + [AllowAnonymous] + public async Task Exchange() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + // Retrieve the claims principal stored in the authorization code/refresh token. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; + if (!result.Succeeded || lexAuthUser is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The token is no longer valid." + })); + } + + return await FinishSignIn(lexAuthUser, request); + } + + private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request) + { + var requestClientId = request.ClientId; + ArgumentException.ThrowIfNullOrEmpty(requestClientId); + var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? + throw new InvalidOperationException( + "Details concerning the calling client application cannot be found."); + // Retrieve the permanent authorizations associated with the user and the calling client application. + var applicationId = await applicationManager.GetIdAsync(application) ?? throw new InvalidOperationException("The calling client application could not be found."); + var authorizations = await authorizationManager.FindAsync( + subject: lexAuthUser.Id.ToString(), + client: applicationId, + status: OpenIddictConstants.Statuses.Valid, + type: OpenIddictConstants.AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + //allow cors response for redirect hosts + var redirectUrisAsync = await applicationManager.GetRedirectUrisAsync(application); + Response.Headers.AccessControlAllowOrigin = redirectUrisAsync + .Select(uri => new Uri(uri).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray(); + + // Note: this check is here to ensure a malicious user can't abuse this POST-only endpoint and + // force it to return a valid response without the external authorization. + if (authorizations.Count is 0 && + await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + } + + return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + } + private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request, string applicationId, List authorizations) + { + var userId = lexAuthUser.Id.ToString(); + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: OpenIddictConstants.Claims.Name, + roleType: OpenIddictConstants.Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(OpenIddictConstants.Claims.Subject, userId) + .SetClaim(OpenIddictConstants.Claims.Email, lexAuthUser.Email) + .SetClaim(OpenIddictConstants.Claims.Name, lexAuthUser.Name) + .SetClaim(OpenIddictConstants.Claims.Role, lexAuthUser.Role.ToString()); + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + identity.SetScopes(request.GetScopes()); + identity.SetAudiences(LexboxAudience.LexboxApi.ToString()); + // identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + + // Automatically create a permanent authorization to avoid requiring explicit consent + // for future authorization or token requests containing the same scopes. + var authorization = authorizations.LastOrDefault(); + authorization ??= await authorizationManager.CreateAsync( + identity: identity, + subject : userId, + client : applicationId, + type : OpenIddictConstants.AuthorizationTypes.Permanent, + scopes : identity.GetScopes()); + + identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); + identity.SetDestinations(GetDestinations); + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + private static IEnumerable GetDestinations(Claim claim) + { + // Note: by default, claims are NOT automatically included in the access and identity tokens. + // To allow OpenIddict to serialize them, you must attach them a destination, that specifies + // whether they should be included in access tokens, in identity tokens or in both. + + var claimsIdentity = claim.Subject; + ArgumentNullException.ThrowIfNull(claimsIdentity); + switch (claim.Type) + { + case OpenIddictConstants.Claims.Name: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Profile)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Email: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Email)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Role: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Roles)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + // Never include the security stamp in the access and identity tokens, as it's a secret value. + case "AspNet.Identity.SecurityStamp": yield break; + + default: + yield return OpenIddictConstants.Destinations.AccessToken; + yield break; + } + } +} diff --git a/backend/LexBoxApi/LexBoxApi.csproj b/backend/LexBoxApi/LexBoxApi.csproj index deddc8b83..725215ac3 100644 --- a/backend/LexBoxApi/LexBoxApi.csproj +++ b/backend/LexBoxApi/LexBoxApi.csproj @@ -32,8 +32,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/backend/LexBoxApi/Program.cs b/backend/LexBoxApi/Program.cs index 31a98c0df..889b02293 100644 --- a/backend/LexBoxApi/Program.cs +++ b/backend/LexBoxApi/Program.cs @@ -171,6 +171,10 @@ .RequireAuthorization(new AdminRequiredAttribute()); // /api routes should never make it to this point, they should be handled by the controllers, so return 404 app.Map("/api/{**catch-all}", () => Results.NotFound()).AllowAnonymous(); + +//should normally be handled by svelte, but if it does reach this we need to return a 401, otherwise we'll get stuck in a redirect loop +app.Map("/login", Results.Unauthorized).AllowAnonymous(); + app.MapSyncProxy(AuthKernel.DefaultScheme); await app.RunAsync(); diff --git a/backend/LexBoxApi/appsettings.Development.json b/backend/LexBoxApi/appsettings.Development.json index ec867ac9d..077ee8967 100644 --- a/backend/LexBoxApi/appsettings.Development.json +++ b/backend/LexBoxApi/appsettings.Development.json @@ -7,7 +7,7 @@ } }, "DbConfig": { - "LexBoxConnectionString": "Host=localhost;Port=5433;Username=postgres;Password=972b722e63f549938d07bd8c4ee5086c;Database=lexbox;Include Error Detail=true", + "LexBoxConnectionString": "Host=localhost;Port=5433;Username=postgres;Password=972b722e63f549938d07bd8c4ee5086c;Database=lexbox;Include Error Detail=true" }, "LfClassicConfig": { "ConnectionString": "mongodb://localhost:27017", @@ -57,6 +57,9 @@ "Google": { "ClientId": "694338503380-rf3t82f3kqc7l0orjletk2r58lep0lvi.apps.googleusercontent.com", "ClientSecret": "__REPLACE__" + }, + "OpenId": { + "Enable": true } }, "Tus" : { diff --git a/backend/LexBoxApi/appsettings.json b/backend/LexBoxApi/appsettings.json index 526299b70..6f3e5ef2e 100644 --- a/backend/LexBoxApi/appsettings.json +++ b/backend/LexBoxApi/appsettings.json @@ -60,6 +60,9 @@ // properties declared in CookieAuthenticationOptions // 15 days, 0 hours "ExpireTimeSpan": "15.00:00" + }, + "OpenId": { + "Enable": false } }, "Otel": { diff --git a/backend/LexData/DataKernel.cs b/backend/LexData/DataKernel.cs index e89df4e26..90a9474d8 100644 --- a/backend/LexData/DataKernel.cs +++ b/backend/LexData/DataKernel.cs @@ -17,6 +17,7 @@ public static void AddLexData(this IServiceCollection services, options.EnableDetailedErrors(); options.UseNpgsql(serviceProvider.GetRequiredService>().Value.LexBoxConnectionString); options.UseProjectables(); + options.UseOpenIddict(); }, dbContextLifeTime); services.AddLogging(); services.AddHealthChecks() diff --git a/backend/LexData/LexData.csproj b/backend/LexData/LexData.csproj index 1ef9b465e..31a539006 100644 --- a/backend/LexData/LexData.csproj +++ b/backend/LexData/LexData.csproj @@ -10,17 +10,18 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + + diff --git a/backend/LexData/Migrations/20240515164328_AddOpenId.Designer.cs b/backend/LexData/Migrations/20240515164328_AddOpenId.Designer.cs new file mode 100644 index 000000000..95c2c35ef --- /dev/null +++ b/backend/LexData/Migrations/20240515164328_AddOpenId.Designer.cs @@ -0,0 +1,1271 @@ +// +using System; +using System.Collections.Generic; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240515164328_AddOpenId")] + partial class AddOpenId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("UserId", "OrgId") + .IsUnique(); + + b.ToTable("OrgMembers", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Orgs", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("LexData.Entities.CrdtCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.ComplexProperty>("HybridDateTime", "LexData.Entities.CrdtCommit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("bigint"); + + b1.Property("DateTime") + .HasColumnType("timestamp with time zone"); + }); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("CrdtCommits", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.HasOne("LexCore.Entities.Organization", "Organization") + .WithMany("Members") + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Organizations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("LexData.Entities.CrdtCommit", b => + { + b.HasOne("LexCore.Entities.FlexProjectMetadata", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Crdt.Core.ChangeEntity", "ChangeEntities", b1 => + { + b1.Property("CrdtCommitId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Change") + .HasColumnType("text"); + + b1.Property("CommitId") + .HasColumnType("uuid"); + + b1.Property("EntityId") + .HasColumnType("uuid"); + + b1.Property("Index") + .HasColumnType("integer"); + + b1.HasKey("CrdtCommitId", "Id"); + + b1.ToTable("CrdtCommits"); + + b1.ToJson("ChangeEntities"); + + b1.WithOwner() + .HasForeignKey("CrdtCommitId"); + }); + + b.Navigation("ChangeEntities"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Organizations"); + + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240515164328_AddOpenId.cs b/backend/LexData/Migrations/20240515164328_AddOpenId.cs new file mode 100644 index 000000000..952d9f453 --- /dev/null +++ b/backend/LexData/Migrations/20240515164328_AddOpenId.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class AddOpenId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ClientId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "text", nullable: true), + ClientType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + JsonWebKeySet = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text", nullable: true), + PostLogoutRedirectUris = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedirectUris = table.Column(type: "text", nullable: true), + Requirements = table.Column(type: "text", nullable: true), + Settings = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "text", nullable: true), + Descriptions = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Properties = table.Column(type: "text", nullable: true), + Resources = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + Properties = table.Column(type: "text", nullable: true), + Scopes = table.Column(type: "text", nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + AuthorizationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + Payload = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedemptionDate = table.Column(type: "timestamp with time zone", nullable: true), + ReferenceId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + } + } +} diff --git a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs index e0a9d006c..77c540a3e 100644 --- a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs +++ b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") - .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("ProductVersion", "8.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -773,6 +773,214 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + modelBuilder.Entity("LexData.Entities.CrdtCommit", b => { b.Property("Id") @@ -944,6 +1152,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("CreatedBy"); }); + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + modelBuilder.Entity("LexData.Entities.CrdtCommit", b => { b.HasOne("LexCore.Entities.FlexProjectMetadata", null) @@ -1022,6 +1254,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UsersICreated"); }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); #pragma warning restore 612, 618 } } diff --git a/backend/LexData/SeedingData.cs b/backend/LexData/SeedingData.cs index 2dae0edc6..0d4bb4994 100644 --- a/backend/LexData/SeedingData.cs +++ b/backend/LexData/SeedingData.cs @@ -4,10 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; namespace LexData; -public class SeedingData(LexBoxDbContext lexBoxDbContext, IOptions dbConfig, IHostEnvironment environment) +public class SeedingData(LexBoxDbContext lexBoxDbContext, IOptions dbConfig, IHostEnvironment environment, IOpenIddictApplicationManager? applicationManager = null) { public static readonly Guid TestAdminId = new("cf430ec9-e721-450a-b6a1-9a853212590b"); public static readonly Guid QaAdminId = new("99b00c58-0dc7-4fe4-b6f2-c27b828811e0"); @@ -16,17 +17,24 @@ public class SeedingData(LexBoxDbContext lexBoxDbContext, IOptions dbC public async Task SeedIfNoUsers(CancellationToken cancellationToken = default) { + await SeedOpenId(cancellationToken); if (await lexBoxDbContext.Users.CountAsync(cancellationToken) > 0) { return; } - await SeedDatabase(cancellationToken); + await SeedUserData(cancellationToken); + } + + public async Task SeedDatabase(CancellationToken cancellationToken = default) + { + await SeedOpenId(cancellationToken); + await SeedUserData(cancellationToken); } private const string PwSalt = "password-salt"; - public async Task SeedDatabase(CancellationToken cancellationToken = default) + private async Task SeedUserData(CancellationToken cancellationToken = default) { if (environment.IsProduction()) return; //NOTE: When seeding make sure you provide a constant Id like I have done here, @@ -167,6 +175,36 @@ public async Task SeedDatabase(CancellationToken cancellationToken = default) await lexBoxDbContext.SaveChangesAsync(cancellationToken); } + private async Task SeedOpenId(CancellationToken cancellationToken = default) + { + if (applicationManager is null) return; + const string clientId = "becf2856-0690-434b-b192-a4032b72067f"; + if (await applicationManager.FindByClientIdAsync(clientId, cancellationToken) is null) + { + await applicationManager.CreateAsync(new OpenIddictApplicationDescriptor + { + ClientId = clientId,//must be guid for MSAL + ClientType = OpenIddictConstants.ClientTypes.Public, + ApplicationType = OpenIddictConstants.ApplicationTypes.Web, + DisplayName = "Oidc Debugger", + //explicit requires the user to consent, Implicit does not, External requires an admin to approve, not currently supported + ConsentType = OpenIddictConstants.ConsentTypes.Explicit, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code, + OpenIddictConstants.Permissions.Scopes.Email, + OpenIddictConstants.Permissions.Scopes.Profile + }, + RedirectUris = { new Uri("https://oidcdebugger.com/debug")} + }, + cancellationToken); + } + } + public async Task CleanUpSeedData() { await lexBoxDbContext.Users.Where(u => u.Salt == PwSalt).ExecuteDeleteAsync(); diff --git a/backend/Testing/Fixtures/TestingServicesFixture.cs b/backend/Testing/Fixtures/TestingServicesFixture.cs index de8e19d15..df2a4e5d2 100644 --- a/backend/Testing/Fixtures/TestingServicesFixture.cs +++ b/backend/Testing/Fixtures/TestingServicesFixture.cs @@ -34,7 +34,7 @@ private static void ConfigureBaseServices(IServiceCollection services) EnvironmentName = Environments.Development }); services.AddSingleton(new ConfigurationManager()); - services.AddLexData(true, ServiceLifetime.Singleton); + services.AddLexData(true, dbContextLifeTime: ServiceLifetime.Singleton); } public ServiceProvider ConfigureServices(Action? configureServices = null) diff --git a/backend/Testing/Testing.csproj b/backend/Testing/Testing.csproj index 57b5d8306..d9a754155 100644 --- a/backend/Testing/Testing.csproj +++ b/backend/Testing/Testing.csproj @@ -38,7 +38,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/frontend/src/lib/components/SigninWithGoogleButton.svelte b/frontend/src/lib/components/SigninWithGoogleButton.svelte index 5be17e2da..fa341ef43 100644 --- a/frontend/src/lib/components/SigninWithGoogleButton.svelte +++ b/frontend/src/lib/components/SigninWithGoogleButton.svelte @@ -12,7 +12,7 @@ - commented out overflow: hidden, because it's unnecessary and makes text ugly on click (due to .btn) - added media query for dark mode using colors from the guidelines --> - +
diff --git a/frontend/src/lib/icons/Icon.svelte b/frontend/src/lib/icons/Icon.svelte index 34972528e..435f41abc 100644 --- a/frontend/src/lib/icons/Icon.svelte +++ b/frontend/src/lib/icons/Icon.svelte @@ -1,6 +1,6 @@ + + + + diff --git a/frontend/src/routes/(authenticated)/authorize/+page.svelte b/frontend/src/routes/(authenticated)/authorize/+page.svelte new file mode 100644 index 000000000..0e09dcff2 --- /dev/null +++ b/frontend/src/routes/(authenticated)/authorize/+page.svelte @@ -0,0 +1,73 @@ + + +
+
+
+
+
+
+ +
+

+ Authorize "{data.appName}" +

+
+ {data.appName} wants to access your account. +
+
+
+
+ {#each data.scope?.split(' ') ?? [] as scope} + {#if scope === 'profile'} +
+ +
+
Personal user data
+
Name, email address and/or username (read-only)
+
+
+ {/if} + {#if scope === 'profile'} +
+ +
+
Projects
+
Project membership and roles (read-only)
+
+
+ {/if} + {/each} +
+
+
+ {#each Object.entries(data.postback) as [key, value]} + + + {/each} +
+
+ {data.appName} is trusted by Language Depot + +
+
+ + +
+
+
+
+
diff --git a/frontend/src/routes/(authenticated)/authorize/+page.ts b/frontend/src/routes/(authenticated)/authorize/+page.ts new file mode 100644 index 000000000..e697d0d16 --- /dev/null +++ b/frontend/src/routes/(authenticated)/authorize/+page.ts @@ -0,0 +1,9 @@ +import type { PageLoadEvent } from './$types'; + +export function load(event: PageLoadEvent) { + return { + appName: event.url.searchParams.get('appName') as string, + scope: event.url.searchParams.get('scope'), + postback: JSON.parse(event.url.searchParams.get('postback') ?? '{}') as Record + }; +} diff --git a/frontend/src/routes/(unauthenticated)/login/+page.svelte b/frontend/src/routes/(unauthenticated)/login/+page.svelte index 977d7b6e5..0fe2c3fc4 100644 --- a/frontend/src/routes/(unauthenticated)/login/+page.svelte +++ b/frontend/src/routes/(unauthenticated)/login/+page.svelte @@ -1,25 +1,28 @@ @@ -55,7 +80,7 @@
@@ -111,8 +136,8 @@
- - + +
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 84f9c64e9..3879e3cbf 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config'; import { gqlOptions } from './gql-codegen'; // eslint-disable-next-line no-restricted-imports import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'; -import {searchForWorkspaceRoot} from 'vite'; +import {type ProxyOptions, searchForWorkspaceRoot} from 'vite'; import { sveltekit } from '@sveltejs/kit/vite'; @@ -12,6 +12,10 @@ import { sveltekit } from '@sveltejs/kit/vite'; const exposeServer = false; +const lexboxServer: ProxyOptions = { + target: 'http://localhost:5158', + secure: false +}; export default defineConfig({ build: { @@ -30,7 +34,7 @@ export default defineConfig({ codegen(gqlOptions), precompileIntl('src/lib/i18n/locales'), sveltekit(), - exposeServer ? basicSsl() : null, // crypto.subtle is only availble on secure connections + exposeServer ? basicSsl() : null, // crypto.subtle is only available on secure connections ], optimizeDeps: { }, @@ -47,15 +51,10 @@ export default defineConfig({ ] }, proxy: process.env['DockerDev'] ? undefined : { - '/v1/traces': { - target: 'http://localhost:4318' - }, - '/api': { - target: 'http://localhost:5158' - }, - '/hg': { - target: 'http://localhost:5158' - } + '/v1/traces': 'http://localhost:4318', + '/api': lexboxServer, + '/hg': lexboxServer, + '/.well-known': lexboxServer } }, });