diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fef987976..0d2be20a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: run: dotnet build BDMS.sln -c Release /warnaserror - name: Start db and api's - run: docker compose up --wait minio db api-legacy api + run: docker compose up --wait minio db api-legacy api oidc-server - working-directory: ./src/client run: npm ci diff --git a/.gitignore b/.gitignore index d6c9c18ef..c5bf1f423 100644 --- a/.gitignore +++ b/.gitignore @@ -412,6 +412,9 @@ src/client/cypress/screenshots # Dependencies /src/client/node_modules +# Generated debug files +src/client/public/env.js + # Testing src/client/coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8a241fd..bdb0be621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Updated layer management to use the .NET API. - Update stratigraphy management to use the .NET API. - Hide outer ring for disabled radio buttons. +- Handle Authentication with a OpenID Connect. ## v2.0.506 - 2023-12-21 diff --git a/config/oidc-mock-clients.json b/config/oidc-mock-clients.json new file mode 100644 index 000000000..845ecc99e --- /dev/null +++ b/config/oidc-mock-clients.json @@ -0,0 +1,26 @@ +[{ + "ClientId": "bdms-client", + "Description": "Client for Authorization Code flow with PKCE", + "RequireClientSecret": false, + "AlwaysIncludeUserClaimsInIdToken": true, + "AllowedGrantTypes": [ + "authorization_code" + ], + "AllowedResponseTypes": [ + "code", + "id_token" + ], + "AllowAccessTokensViaBrowser": true, + "RedirectUris": [ + "http://localhost:3000" + ], + "AllowedScopes": [ + "openid", + "profile", + "email" + ], + "AccessTokenType": "JWT", + "IdentityTokenLifetime": 3600, + "AccessTokenLifetime": 3600 +} +] diff --git a/config/oidc-mock-users.json b/config/oidc-mock-users.json new file mode 100644 index 000000000..860ae0976 --- /dev/null +++ b/config/oidc-mock-users.json @@ -0,0 +1,66 @@ +[ + { + "SubjectId":"sub_admin", + "Username":"admin", + "Password":"swissforages", + "Claims": [ + { + "Type": "name", + "Value": "Admin User", + "ValueType": "string" + }, + { + "Type": "family_name", + "Value": "User", + "ValueType": "string" + }, + { + "Type": "given_name", + "Value": "Admin", + "ValueType": "string" + }, + { + "Type": "email", + "Value": "admin.user@local.dev", + "ValueType": "string" + }, + { + "Type": "email_verified", + "Value": "true", + "ValueType": "boolean" + } + ] + }, + { + "SubjectId":"sub_editor", + "Username":"editor", + "Password":"swissforages", + "Claims": [ + { + "Type": "name", + "Value": "Editor User", + "ValueType": "string" + }, + { + "Type": "family_name", + "Value": "User", + "ValueType": "string" + }, + { + "Type": "given_name", + "Value": "Editor", + "ValueType": "string" + }, + { + "Type": "email", + "Value": "editor.user@local.dev", + "ValueType": "string" + }, + { + "Type": "email_verified", + "Value": "true", + "ValueType": "boolean" + } + ] + } +] diff --git a/docker-compose.yml b/docker-compose.yml index aa4f049e8..22165bcc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,8 @@ services: condition: service_healthy environment: REACT_APP_PROXY_HOST_API: http://api:5000/ + REACT_APP_CLIENT_AUTHORITY: http://localhost:4011/ + REACT_APP_CLIENT_CLIENT_ID: bdms-client WATCHPACK_POLLING: 'true' api-legacy: build: @@ -118,5 +120,28 @@ services: DOTNET_USE_POLLING_FILE_WATCHER: 1 CONNECTIONSTRINGS__BdmsContext: Host=db;Username=SPAWNPLOW;Password=YELLOWSPATULA;Database=bdms;CommandTimeout=300 ReverseProxy__Clusters__pythonApi__Destinations__legacyApi__Address: "http://api-legacy:8888/" + Auth__Authority: http://oidc-server S3__ENDPOINT: http://minio:9000 S3__SECURE: 1 + oidc-server: + image: soluto/oidc-server-mock + ports: + - "4011:80" + environment: + CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json + USERS_CONFIGURATION_PATH: /tmp/config/users-config.json + SERVER_OPTIONS_INLINE: | + { + "IssuerUri": "http://localhost:4011", + "AccessTokenJwtType": "JWT", + "Discovery": { + "ShowKeySet": true + }, + "Authentication": { + "CookieSameSiteMode": "Lax", + "CheckSessionCookieSameSiteMode": "Lax" + } + } + volumes: + - ./config/oidc-mock-clients.json:/tmp/config/clients-config.json:ro + - ./config/oidc-mock-users.json:/tmp/config/users-config.json:ro diff --git a/src/api-legacy/v1/basehandler.py b/src/api-legacy/v1/basehandler.py index 1cee19418..dc6c94cb6 100644 --- a/src/api-legacy/v1/basehandler.py +++ b/src/api-legacy/v1/basehandler.py @@ -28,16 +28,16 @@ def __init__(self, *args, **kwargs): async def prepare(self): auth_header = self.request.headers.get('Authorization') - + if auth_header is None: self.set_header('WWW-Authenticate', 'Basic realm=BDMS') self.set_status(401) self.finish() return - username = auth_header + subject_id = auth_header - async with self.pool.acquire() as conn: + async with self.pool.acquire() as conn: val = await conn.fetchval(""" SELECT row_to_json(t) @@ -167,11 +167,11 @@ async def prepare(self): ON w.id_usr_fk = id_usr WHERE - username = $1 + subject_id = $1 AND disabled_usr IS NULL ) as t - """, username) + """, subject_id) if val is None: self.set_status(401) diff --git a/src/api-legacy/v1/user/handler.py b/src/api-legacy/v1/user/handler.py index dca9ecf1e..6080d7086 100644 --- a/src/api-legacy/v1/user/handler.py +++ b/src/api-legacy/v1/user/handler.py @@ -20,9 +20,9 @@ async def execute(self, request): for workgroup in self.user['workgroups']: if workgroup['disabled'] is not None: workgroup['roles'] = ['VIEW'] - + workgroups.append(workgroup) - + for role in workgroup['roles']: if role not in roles: roles.append(role) @@ -33,6 +33,7 @@ async def execute(self, request): "name": self.user['name'], "roles": roles, "terms": self.user['terms'], + "id": self.user['id'], "username": self.user['username'], "viewer": self.user['viewer'], "workgroups": workgroups diff --git a/src/api/Authentication/BasicAuthenticationHandler.cs b/src/api/Authentication/BasicAuthenticationHandler.cs deleted file mode 100644 index 8ead2d82f..000000000 --- a/src/api/Authentication/BasicAuthenticationHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using Npgsql; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; - -namespace BDMS.Authentication; - -public class BasicAuthenticationHandler : AuthenticationHandler -{ - private readonly BdmsContext dbContext; - - /// - /// Initializes a new instance of the class. - /// - /// The EF database context containing data for the BDMS application. - /// The monitor for the options instance. - /// The . - /// The . - /// If is null. - public BasicAuthenticationHandler(BdmsContext dbContext, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) - : base(options, logger, encoder) - { - this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - } - - /// - protected override Task HandleAuthenticateAsync() - { - var authorizationHeader = Request.Headers.Authorization.ToString(); - if (authorizationHeader != null && authorizationHeader.StartsWith("basic ", StringComparison.OrdinalIgnoreCase)) - { - // Get username and password from base64 encoded authorization header - var token = authorizationHeader[6..].Trim(); - var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token)); - var credentials = credentialstring.Split(':', 2); - - // Get authenticated user from database - var authenticatedUser = dbContext.Users - .FromSqlRaw( - "SELECT * FROM bdms.users WHERE username = @username AND password = crypt(@password, password)", - new NpgsqlParameter("@username", credentials[0]), - new NpgsqlParameter("@password", credentials[1])) - .SingleOrDefault(); - - // Handle invalid or disabled user - if (authenticatedUser == null || authenticatedUser.IsDisabled) - { - return Task.FromResult(AuthenticateResult.Fail("No valid authentication credentials have been provided or the specified user has been disabled in the backend.")); - } - - var claimsIdentity = new ClaimsIdentity(); - claimsIdentity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, "Basic")); - claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, authenticatedUser.Name)); - if (authenticatedUser.IsAdmin) claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, PolicyNames.Admin)); - else if (authenticatedUser.IsViewer) claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, PolicyNames.Viewer)); - - return Task.FromResult(AuthenticateResult.Success( - new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), "BasicAuthentication"))); - } - - return Task.FromResult(AuthenticateResult.Fail("No valid authentication credentials has been provided.")); - } -} diff --git a/src/api/Authentication/DatabaseAuthenticationClaimsTransformation.cs b/src/api/Authentication/DatabaseAuthenticationClaimsTransformation.cs new file mode 100644 index 000000000..094f418d9 --- /dev/null +++ b/src/api/Authentication/DatabaseAuthenticationClaimsTransformation.cs @@ -0,0 +1,49 @@ +using BDMS.Models; +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; + +namespace BDMS.Authentication; + +public class DatabaseAuthenticationClaimsTransformation : IClaimsTransformation +{ + private readonly BdmsContext dbContext; + + /// + /// Initializes a new instance of the class. + /// + /// The EF database context containing data for the BDMS application. + /// If is null. + public DatabaseAuthenticationClaimsTransformation(BdmsContext dbContext) + { + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + /// + public async Task TransformAsync(ClaimsPrincipal principal) + { + var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + if (userId is null) return principal; + + var authenticatedUser = dbContext.Users.FirstOrDefault(u => u.SubjectId == userId.Value) + ?? new User + { + SubjectId = userId.Value, + Password = "Undefined", // TODO: Remove with #911 + }; + if (authenticatedUser.IsDisabled) return principal; + + authenticatedUser.FirstName = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? authenticatedUser.FirstName; + authenticatedUser.LastName = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? authenticatedUser.LastName; + authenticatedUser.Name = $"{authenticatedUser.FirstName[0]}. {authenticatedUser.LastName}"; + dbContext.Update(authenticatedUser); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + var claimsIdentity = new ClaimsIdentity(); + claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, authenticatedUser.Name)); + if (authenticatedUser.IsAdmin) claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, PolicyNames.Admin)); + else if (authenticatedUser.IsViewer) claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, PolicyNames.Viewer)); + + principal.AddIdentity(claimsIdentity); + return principal; + } +} diff --git a/src/api/Authentication/LegacyApiAuthenticationMiddleware.cs b/src/api/Authentication/LegacyApiAuthenticationMiddleware.cs index c69e517a7..27ecd2c7c 100644 --- a/src/api/Authentication/LegacyApiAuthenticationMiddleware.cs +++ b/src/api/Authentication/LegacyApiAuthenticationMiddleware.cs @@ -15,14 +15,14 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (context.Request.Path.StartsWithSegments(new PathString("/api/v1"), StringComparison.OrdinalIgnoreCase)) { - var userName = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name); - if (userName is not null) + var subjectId = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + if (subjectId is not null) { - context.Request.Headers.Authorization = userName.Value; + context.Request.Headers.Authorization = subjectId.Value; await next.Invoke(context).ConfigureAwait(false); - logger.LogInformation("Authorized user <{UserName}> for legacy api accessing route <{Route}>", userName.Value, context.Request.Path); + logger.LogInformation("Authorized user with subject_id <{SubjectId}> for legacy api accessing route <{Route}>", subjectId.Value, context.Request.Path); return; } } diff --git a/src/api/BDMS.csproj b/src/api/BDMS.csproj index fcd48ab96..e3d4d4a7b 100644 --- a/src/api/BDMS.csproj +++ b/src/api/BDMS.csproj @@ -23,6 +23,7 @@ + diff --git a/src/api/BdmsContext.cs b/src/api/BdmsContext.cs index f8d90ec5a..ed5221792 100644 --- a/src/api/BdmsContext.cs +++ b/src/api/BdmsContext.cs @@ -52,12 +52,12 @@ public BdmsContext(DbContextOptions options) /// public async Task UpdateChangeInformationAndSaveChangesAsync(HttpContext httpContext) { - var userName = httpContext?.User.FindFirst(ClaimTypes.Name)?.Value; + var subjectId = httpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var entities = ChangeTracker.Entries(); var user = await Users .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == userName) + .SingleOrDefaultAsync(u => u.SubjectId == subjectId) .ConfigureAwait(false); foreach (var entity in entities) diff --git a/src/api/BoreholeFileUploadService.cs b/src/api/BoreholeFileUploadService.cs index a3fb7490d..a1322177f 100644 --- a/src/api/BoreholeFileUploadService.cs +++ b/src/api/BoreholeFileUploadService.cs @@ -51,14 +51,14 @@ public async Task UploadFileAndLinkToBorehole(IFormFile file, int boreholeId) using var transaction = context.Database.CurrentTransaction == null ? await context.Database.BeginTransactionAsync().ConfigureAwait(false) : null; try { - var userName = httpContextAccessor.HttpContext?.GetUserName(); + var subjectId = httpContextAccessor.HttpContext?.GetUserSubjectId(); var user = await context.Users .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == userName) + .SingleOrDefaultAsync(u => u.SubjectId == subjectId) .ConfigureAwait(false); - if (user == null || userName == null) throw new InvalidOperationException($"No user with username <{userName}> found."); + if (user == null || subjectId == null) throw new InvalidOperationException($"No user with subject_id <{subjectId}> found."); // If file does not exist on storage, upload it and create file in database. if (fileId == null) diff --git a/src/api/BoreholeLockService.cs b/src/api/BoreholeLockService.cs index a7318b39c..58cc5a160 100644 --- a/src/api/BoreholeLockService.cs +++ b/src/api/BoreholeLockService.cs @@ -12,13 +12,13 @@ public class BoreholeLockService(BdmsContext context, ILogger - public async Task IsBoreholeLockedAsync(int? boreholeId, string? userName) + public async Task IsBoreholeLockedAsync(int? boreholeId, string? subjectId) { var user = await context.Users .Include(u => u.WorkgroupRoles) .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == userName) - .ConfigureAwait(false) ?? throw new InvalidOperationException($"Current user with name <{userName}> does not exist."); + .SingleOrDefaultAsync(u => u.SubjectId == subjectId) + .ConfigureAwait(false) ?? throw new InvalidOperationException($"Current user with subjectId <{subjectId}> does not exist."); var borehole = await context.Boreholes .Include(b => b.Workflows) @@ -33,14 +33,14 @@ public async Task IsBoreholeLockedAsync(int? boreholeId, string? userName) if (!user.WorkgroupRoles.Any(x => x.WorkgroupId == borehole.WorkgroupId && userWorkflowRoles.Contains(x.Role))) { - logger.LogWarning("Current user with name <{UserName}> does not have the required role to create a stratigraphy for borehole with id <{BoreholeId}>.", userName, boreholeId); + logger.LogWarning("Current user with subject_id <{SubjectId}> does not have the required role to create a stratigraphy for borehole with id <{BoreholeId}>.", subjectId, boreholeId); throw new UnauthorizedAccessException(); } if (borehole.Locked.HasValue && borehole.Locked.Value.AddMinutes(LockTimeoutInMinutes) > timeProvider.GetUtcNow() && borehole.LockedById != user.Id) { var lockedUserFullName = $"{borehole.LockedBy?.FirstName} {borehole.LockedBy?.LastName}"; - logger.LogWarning("Current user with name <{UserName}> tried to create a stratigraphy for borehole with id <{BoreholeId}>, but the borehole is locked by user <{LockedByUserName}>.", userName, boreholeId, lockedUserFullName); + logger.LogWarning("Current user with subject_id <{SubjectId}> tried to create a stratigraphy for borehole with id <{BoreholeId}>, but the borehole is locked by user <{LockedByUserName}>.", subjectId, boreholeId, lockedUserFullName); return true; } diff --git a/src/api/Controllers/BoreholeController.cs b/src/api/Controllers/BoreholeController.cs index 8a4c2111e..22ef4480f 100644 --- a/src/api/Controllers/BoreholeController.cs +++ b/src/api/Controllers/BoreholeController.cs @@ -35,7 +35,7 @@ public async Task> CopyAsync([Required] int id, [Required] int var user = await context.Users .Include(u => u.WorkgroupRoles) .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == HttpContext.GetUserName()) + .SingleOrDefaultAsync(u => u.SubjectId == HttpContext.GetUserSubjectId()) .ConfigureAwait(false); if (user == null || !user.WorkgroupRoles.Any(w => w.WorkgroupId == workgroupId && w.Role == Role.Editor)) diff --git a/src/api/Controllers/StratigraphyController.cs b/src/api/Controllers/StratigraphyController.cs index c97f72c50..6c7271718 100644 --- a/src/api/Controllers/StratigraphyController.cs +++ b/src/api/Controllers/StratigraphyController.cs @@ -78,7 +78,7 @@ public async Task> CopyAsync([Required] int id) var user = await Context.Users .Include(u => u.WorkgroupRoles) .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == HttpContext.GetUserName()) + .SingleOrDefaultAsync(u => u.SubjectId == HttpContext.GetUserSubjectId()) .ConfigureAwait(false); if (user == null || !user.WorkgroupRoles.Any(w => w.Role == Role.Editor)) @@ -180,8 +180,8 @@ public override async Task> CreateAsync(Stratigraphy try { // Check if associated borehole is locked - var userName = HttpContext.GetUserName(); - if (await boreholeLockService.IsBoreholeLockedAsync(entity.BoreholeId, userName).ConfigureAwait(false)) + var subjectId = HttpContext.GetUserSubjectId(); + if (await boreholeLockService.IsBoreholeLockedAsync(entity.BoreholeId, subjectId).ConfigureAwait(false)) { return Problem("The borehole is locked by another user."); } @@ -226,8 +226,8 @@ public async Task> AddBedrockLayerAsync([Required] int id) try { // Check if associated borehole is locked - var userName = HttpContext.GetUserName(); - if (await boreholeLockService.IsBoreholeLockedAsync(stratigraphy.BoreholeId, userName).ConfigureAwait(false)) + var subjectId = HttpContext.GetUserSubjectId(); + if (await boreholeLockService.IsBoreholeLockedAsync(stratigraphy.BoreholeId, subjectId).ConfigureAwait(false)) { return Problem("The borehole is locked by another user."); } @@ -273,7 +273,7 @@ public override async Task> EditAsync(Stratigraphy en try { // Check if associated borehole is locked - var userName = HttpContext.GetUserName(); + var userName = HttpContext.GetUserSubjectId(); if (await boreholeLockService.IsBoreholeLockedAsync(entity.BoreholeId, userName).ConfigureAwait(false)) { return Problem("The borehole is locked by another user."); diff --git a/src/api/Controllers/UploadController.cs b/src/api/Controllers/UploadController.cs index 31e8a2c13..c50ea91a2 100644 --- a/src/api/Controllers/UploadController.cs +++ b/src/api/Controllers/UploadController.cs @@ -92,11 +92,11 @@ public async Task> UploadFileAsync(int workgroupId, IFormFile return ValidationProblem(statusCode: (int)HttpStatusCode.BadRequest); } - var userName = HttpContext.GetUserName(); + var subjectId = HttpContext.GetUserSubjectId(); var user = await context.Users .AsNoTracking() - .SingleOrDefaultAsync(u => u.Name == userName) + .SingleOrDefaultAsync(u => u.SubjectId == subjectId) .ConfigureAwait(false); // Map to Borehole type diff --git a/src/api/Controllers/UserController.cs b/src/api/Controllers/UserController.cs index 926b5c023..89fb9c9e8 100644 --- a/src/api/Controllers/UserController.cs +++ b/src/api/Controllers/UserController.cs @@ -27,7 +27,7 @@ public UserController(BdmsContext context) [HttpGet("self")] [Authorize(Policy = PolicyNames.Viewer)] public async Task> GetUserInformationAsync() => - await context.Users.SingleOrDefaultAsync(u => u.Name == HttpContext.GetUserName()).ConfigureAwait(false); + await context.Users.SingleOrDefaultAsync(u => u.SubjectId == HttpContext.GetUserSubjectId()).ConfigureAwait(false); /// /// Gets the user list. diff --git a/src/api/HttpContextExtensions.cs b/src/api/HttpContextExtensions.cs index c584b19cf..03a8427c8 100644 --- a/src/api/HttpContextExtensions.cs +++ b/src/api/HttpContextExtensions.cs @@ -8,10 +8,10 @@ namespace BDMS; internal static class HttpContextExtensions { /// - /// Gets the name of the user from the . + /// Gets the SubjectId of the user from the . /// /// The current . - /// The username or null, if the username cannot be read. - internal static string GetUserName(this HttpContext httpContext) - => httpContext.User.FindFirst(ClaimTypes.Name).Value; + /// The SubjectId or null, if the users SubjectId cannot be read. + internal static string GetUserSubjectId(this HttpContext httpContext) + => httpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; } diff --git a/src/api/IBoreholeLockService.cs b/src/api/IBoreholeLockService.cs index 882039c2b..d8657d9a1 100644 --- a/src/api/IBoreholeLockService.cs +++ b/src/api/IBoreholeLockService.cs @@ -11,9 +11,9 @@ public interface IBoreholeLockService /// Checks whether the borehole with the specified is locked. /// /// The to check locks for. - /// The current user name. + /// The of the current user. /// true if the borehole is locked by another user; otherwise, false. /// Provided user or does not exist. /// Current user is not allowed to create the given . - Task IsBoreholeLockedAsync(int? boreholeId, string? userName); + Task IsBoreholeLockedAsync(int? boreholeId, string? subjectId); } diff --git a/src/api/Migrations/20240109150510_AddUserSubjectId.Designer.cs b/src/api/Migrations/20240109150510_AddUserSubjectId.Designer.cs new file mode 100644 index 000000000..851e28699 --- /dev/null +++ b/src/api/Migrations/20240109150510_AddUserSubjectId.Designer.cs @@ -0,0 +1,2672 @@ +// +using System; +using BDMS; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BDMS.Migrations +{ + [DbContext(typeof(BdmsContext))] + [Migration("20240109150510_AddUserSubjectId")] + partial class AddUserSubjectId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("bdms") + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BDMS.Models.Borehole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_bho"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlternateName") + .HasColumnType("text") + .HasColumnName("alternate_name_bho"); + + b.Property("Canton") + .HasColumnType("text") + .HasColumnName("canton_bho"); + + b.Property("ChronostratigraphyId") + .HasColumnType("integer") + .HasColumnName("chronostrat_id_cli"); + + b.Property("Country") + .HasColumnType("text") + .HasColumnName("country_bho"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_bho"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_bho"); + + b.Property("CuttingsId") + .HasColumnType("integer") + .HasColumnName("cuttings_id_cli"); + + b.Property("DrillingDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("drilling_date_bho"); + + b.Property("DrillingDiameter") + .HasColumnType("double precision") + .HasColumnName("drilling_diameter_bho"); + + b.Property("DrillingMethodId") + .HasColumnType("integer") + .HasColumnName("drilling_method_id_cli"); + + b.Property("ElevationZ") + .HasColumnType("double precision") + .HasColumnName("elevation_z_bho"); + + b.Property("Geometry") + .HasColumnType("geometry") + .HasColumnName("geom_bho"); + + b.Property("HasGroundwater") + .HasColumnType("boolean") + .HasColumnName("groundwater_bho"); + + b.Property("HrsId") + .HasColumnType("integer") + .HasColumnName("hrs_id_cli"); + + b.Property("Inclination") + .HasColumnType("double precision") + .HasColumnName("inclination_bho"); + + b.Property("InclinationDirection") + .HasColumnType("double precision") + .HasColumnName("inclination_direction_bho"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("public_bho"); + + b.Property("KindId") + .HasColumnType("integer") + .HasColumnName("kind_id_cli"); + + b.Property("LithologyTopBedrockId") + .HasColumnType("integer") + .HasColumnName("lithology_top_bedrock_id_cli"); + + b.Property("LithostratigraphyId") + .HasColumnType("integer") + .HasColumnName("lithostrat_id_cli"); + + b.Property("LocationX") + .HasColumnType("double precision") + .HasColumnName("location_x_bho"); + + b.Property("LocationXLV03") + .HasColumnType("double precision") + .HasColumnName("location_x_lv03_bho"); + + b.Property("LocationY") + .HasColumnType("double precision") + .HasColumnName("location_y_bho"); + + b.Property("LocationYLV03") + .HasColumnType("double precision") + .HasColumnName("location_y_lv03_bho"); + + b.Property("Locked") + .HasColumnType("timestamp with time zone") + .HasColumnName("locked_bho"); + + b.Property("LockedById") + .HasColumnType("integer") + .HasColumnName("locked_by_bho"); + + b.Property("Municipality") + .HasColumnType("text") + .HasColumnName("municipality_bho"); + + b.Property("NationalInterest") + .HasColumnType("boolean") + .HasColumnName("national_interest"); + + b.Property("OriginalName") + .HasColumnType("text") + .HasColumnName("original_name_bho"); + + b.Property("OriginalReferenceSystem") + .HasColumnType("integer") + .HasColumnName("srs_id_cli"); + + b.Property("ProjectName") + .HasColumnType("text") + .HasColumnName("project_name_bho"); + + b.Property("PurposeId") + .HasColumnType("integer") + .HasColumnName("purpose_id_cli"); + + b.Property("QtDepthId") + .HasColumnType("integer") + .HasColumnName("qt_depth_id_cli"); + + b.Property("QtElevationId") + .HasColumnType("integer") + .HasColumnName("qt_elevation_id_cli"); + + b.Property("QtInclinationDirectionId") + .HasColumnType("integer") + .HasColumnName("qt_inclination_direction_id_cli"); + + b.Property("QtLocationId") + .HasColumnType("integer") + .HasColumnName("qt_location_id_cli"); + + b.Property("QtReferenceElevationId") + .HasColumnType("integer") + .HasColumnName("qt_reference_elevation_id_cli"); + + b.Property("QtTopBedrock") + .HasColumnType("double precision") + .HasColumnName("qt_top_bedrock"); + + b.Property("QtTopBedrockTvd") + .HasColumnType("double precision") + .HasColumnName("qt_top_bedrock_tvd"); + + b.Property("QtTotalDepthTvdId") + .HasColumnType("integer") + .HasColumnName("qt_total_depth_tvd_id_cli"); + + b.Property("ReferenceElevation") + .HasColumnType("double precision") + .HasColumnName("reference_elevation_bho"); + + b.Property("ReferenceElevationTypeId") + .HasColumnType("integer") + .HasColumnName("reference_elevation_type_id_cli"); + + b.Property("Remarks") + .HasColumnType("text") + .HasColumnName("remarks_bho"); + + b.Property("RestrictionId") + .HasColumnType("integer") + .HasColumnName("restriction_id_cli"); + + b.Property("RestrictionUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("restriction_until_bho"); + + b.Property("SpudDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("spud_date_bho"); + + b.Property("StatusId") + .HasColumnType("integer") + .HasColumnName("status_id_cli"); + + b.Property("TopBedrock") + .HasColumnType("double precision") + .HasColumnName("top_bedrock_bho"); + + b.Property("TopBedrockTvd") + .HasColumnType("double precision") + .HasColumnName("top_bedrock_tvd_bho"); + + b.Property("TotalDepth") + .HasColumnType("double precision") + .HasColumnName("total_depth_bho"); + + b.Property("TotalDepthTvd") + .HasColumnType("double precision") + .HasColumnName("total_depth_tvd_bho"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_bho"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_bho"); + + b.Property("WorkgroupId") + .HasColumnType("integer") + .HasColumnName("id_wgp_fk"); + + b.HasKey("Id"); + + b.HasIndex("ChronostratigraphyId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("CuttingsId"); + + b.HasIndex("DrillingMethodId"); + + b.HasIndex("HrsId"); + + b.HasIndex("KindId"); + + b.HasIndex("LithologyTopBedrockId"); + + b.HasIndex("LithostratigraphyId"); + + b.HasIndex("LockedById"); + + b.HasIndex("PurposeId"); + + b.HasIndex("QtDepthId"); + + b.HasIndex("QtElevationId"); + + b.HasIndex("QtInclinationDirectionId"); + + b.HasIndex("QtLocationId"); + + b.HasIndex("QtReferenceElevationId"); + + b.HasIndex("QtTotalDepthTvdId"); + + b.HasIndex("ReferenceElevationTypeId"); + + b.HasIndex("RestrictionId"); + + b.HasIndex("StatusId"); + + b.HasIndex("UpdatedById"); + + b.HasIndex("WorkgroupId"); + + b.ToTable("borehole", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.BoreholeCodelist", b => + { + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("id_bho_fk"); + + b.Property("CodelistId") + .HasColumnType("integer") + .HasColumnName("id_cli_fk"); + + b.Property("SchemaName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code_cli"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value_bco"); + + b.HasKey("BoreholeId", "CodelistId"); + + b.HasIndex("CodelistId"); + + b.ToTable("borehole_codelist", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.BoreholeFile", b => + { + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("id_bho_fk"); + + b.Property("FileId") + .HasColumnType("integer") + .HasColumnName("id_fil_fk"); + + b.Property("Attached") + .HasColumnType("timestamp with time zone") + .HasColumnName("attached_bfi"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_bfi"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_bfi"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description_bfi"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public_bfi"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update_bfi"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater_bfi"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id_usr_fk"); + + b.HasKey("BoreholeId", "FileId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("FileId"); + + b.HasIndex("UpdatedById"); + + b.HasIndex("UserId"); + + b.ToTable("borehole_files", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.ChronostratigraphyLayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_chr"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChronostratigraphyId") + .HasColumnType("integer") + .HasColumnName("chronostratigraphy_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("depth_from"); + + b.Property("IsLast") + .HasColumnType("boolean") + .HasColumnName("is_last"); + + b.Property("StratigraphyId") + .HasColumnType("integer") + .HasColumnName("id_sty_fk"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("depth_to"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("ChronostratigraphyId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("StratigraphyId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("chronostratigraphy", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Codelist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_cli"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code_cli"); + + b.Property("Conf") + .HasColumnType("text") + .HasColumnName("conf_cli"); + + b.Property("De") + .HasColumnType("text") + .HasColumnName("text_cli_de"); + + b.Property("DescriptionDe") + .HasColumnType("text") + .HasColumnName("description_cli_de"); + + b.Property("DescriptionEn") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description_cli_en"); + + b.Property("DescriptionFr") + .HasColumnType("text") + .HasColumnName("description_cli_fr"); + + b.Property("DescriptionIt") + .HasColumnType("text") + .HasColumnName("description_cli_it"); + + b.Property("DescriptionRo") + .HasColumnType("text") + .HasColumnName("description_cli_ro"); + + b.Property("En") + .IsRequired() + .HasColumnType("text") + .HasColumnName("text_cli_en"); + + b.Property("Fr") + .HasColumnType("text") + .HasColumnName("text_cli_fr"); + + b.Property("Geolcode") + .HasColumnType("integer") + .HasColumnName("geolcode"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasColumnName("default_cli"); + + b.Property("It") + .HasColumnType("text") + .HasColumnName("text_cli_it"); + + b.Property("Order") + .HasColumnType("integer") + .HasColumnName("order_cli"); + + b.Property("Path") + .HasColumnType("ltree") + .HasColumnName("path_cli"); + + b.Property("Ro") + .HasColumnType("text") + .HasColumnName("text_cli_ro"); + + b.Property("Schema") + .HasColumnType("text") + .HasColumnName("schema_cli"); + + b.HasKey("Id"); + + b.ToTable("codelist", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Completion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbandonDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("abandon_date"); + + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("borehole_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("KindId") + .HasColumnType("integer") + .HasColumnName("kind_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("BoreholeId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("KindId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("completion", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Config", b => + { + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name_cfg"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value_cfg"); + + b.HasKey("Name"); + + b.ToTable("config", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_cnt"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Creation") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_cnt"); + + b.Property("Expired") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_cnt"); + + b.Property("IsDraft") + .HasColumnType("boolean") + .HasColumnName("draft_cnt"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name_cnt"); + + b.Property("TextDe") + .HasColumnType("text") + .HasColumnName("text_cnt_de"); + + b.Property("TextEn") + .HasColumnType("text") + .HasColumnName("text_cnt_en"); + + b.Property("TextFr") + .HasColumnType("text") + .HasColumnName("text_cnt_fr"); + + b.Property("TextIt") + .HasColumnType("text") + .HasColumnName("text_cnt_it"); + + b.Property("TextRo") + .HasColumnType("text") + .HasColumnName("text_cnt_ro"); + + b.Property("TitelRo") + .HasColumnType("text") + .HasColumnName("title_cnt_ro"); + + b.Property("TitleDe") + .HasColumnType("text") + .HasColumnName("title_cnt_de"); + + b.Property("TitleEn") + .HasColumnType("text") + .HasColumnName("title_cnt_en"); + + b.Property("TitleFr") + .HasColumnType("text") + .HasColumnName("title_cnt_fr"); + + b.Property("TitleIt") + .HasColumnType("text") + .HasColumnName("title_cnt_it"); + + b.HasKey("Id"); + + b.ToTable("contents", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.FaciesDescription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_fac"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("depth_from"); + + b.Property("IsLast") + .HasColumnType("boolean") + .HasColumnName("is_last"); + + b.Property("QtDescriptionId") + .HasColumnType("integer") + .HasColumnName("qt_description_id"); + + b.Property("StratigraphyId") + .HasColumnType("integer") + .HasColumnName("id_sty_fk"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("depth_to"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("QtDescriptionId"); + + b.HasIndex("StratigraphyId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("facies_description", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_feb"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_feb"); + + b.Property("IsFrw") + .HasColumnType("boolean") + .HasColumnName("frw_feb"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message_feb"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag_feb"); + + b.Property("User") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_feb"); + + b.HasKey("Id"); + + b.ToTable("feedbacks", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_fil"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("uploaded_fil"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("id_usr_fk"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash_fil"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name_fil"); + + b.Property("NameUuid") + .HasColumnType("text") + .HasColumnName("name_uuid_fil"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type_fil"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_fil"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_fil"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("UpdatedById"); + + b.ToTable("files", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.HydrotestCodelist", b => + { + b.Property("HydrotestId") + .HasColumnType("integer") + .HasColumnName("id_ht_fk"); + + b.Property("CodelistId") + .HasColumnType("integer") + .HasColumnName("id_cli_fk"); + + b.HasKey("HydrotestId", "CodelistId"); + + b.HasIndex("CodelistId"); + + b.ToTable("hydrotest_codelist", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.HydrotestResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("HydrotestId") + .HasColumnType("integer") + .HasColumnName("hydrotest_id"); + + b.Property("MaxValue") + .HasColumnType("double precision") + .HasColumnName("max_value"); + + b.Property("MinValue") + .HasColumnType("double precision") + .HasColumnName("min_value"); + + b.Property("ParameterId") + .HasColumnType("integer") + .HasColumnName("parameter"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.Property("Value") + .HasColumnType("double precision") + .HasColumnName("value"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("HydrotestId"); + + b.HasIndex("ParameterId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("hydrotest_result", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Instrumentation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletionId") + .HasColumnType("integer") + .HasColumnName("completion_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("from_depth"); + + b.Property("KindId") + .HasColumnType("integer") + .HasColumnName("kind_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes"); + + b.Property("StatusId") + .HasColumnType("integer") + .HasColumnName("status_id"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("to_depth"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("CompletionId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("KindId"); + + b.HasIndex("StatusId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("instrumentation", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Layer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_lay"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlterationId") + .HasColumnType("integer") + .HasColumnName("alteration_id_cli"); + + b.Property("Casing") + .HasColumnType("text") + .HasColumnName("casng_id"); + + b.Property("CasingDateFinish") + .HasColumnType("timestamp with time zone") + .HasColumnName("casng_date_finish_lay"); + + b.Property("CasingDateSpud") + .HasColumnType("timestamp with time zone") + .HasColumnName("casng_date_spud_lay"); + + b.Property("CasingInnerDiameter") + .HasColumnType("double precision") + .HasColumnName("casng_inner_diameter_lay"); + + b.Property("CasingKindId") + .HasColumnType("integer") + .HasColumnName("casng_kind_id_cli"); + + b.Property("CasingMaterialId") + .HasColumnType("integer") + .HasColumnName("casng_material_id_cli"); + + b.Property("CasingOuterDiameter") + .HasColumnType("double precision") + .HasColumnName("casng_outer_diameter_lay"); + + b.Property("CohesionId") + .HasColumnType("integer") + .HasColumnName("cohesion_id_cli"); + + b.Property("CompactnessId") + .HasColumnType("integer") + .HasColumnName("compactness_id_cli"); + + b.Property("ConsistanceId") + .HasColumnType("integer") + .HasColumnName("consistance_id_cli"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_lay"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator_lay"); + + b.Property("FillKindId") + .HasColumnType("integer") + .HasColumnName("fill_kind_id_cli"); + + b.Property("FillMaterialId") + .HasColumnType("integer") + .HasColumnName("fill_material_id_cli"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("depth_from_lay"); + + b.Property("GradationId") + .HasColumnType("integer") + .HasColumnName("gradation_id_cli"); + + b.Property("GrainSize1Id") + .HasColumnType("integer") + .HasColumnName("grain_size_1_id_cli"); + + b.Property("GrainSize2Id") + .HasColumnType("integer") + .HasColumnName("grain_size_2_id_cli"); + + b.Property("HumidityId") + .HasColumnType("integer") + .HasColumnName("humidity_id_cli"); + + b.Property("Instrument") + .HasColumnType("text") + .HasColumnName("instr_id"); + + b.Property("InstrumentCasingId") + .HasColumnType("integer") + .HasColumnName("instr_id_sty_fk"); + + b.Property("InstrumentCasingLayerId") + .HasColumnType("integer") + .HasColumnName("instr_id_lay_fk"); + + b.Property("InstrumentKindId") + .HasColumnType("integer") + .HasColumnName("instr_kind_id_cli"); + + b.Property("InstrumentStatusId") + .HasColumnType("integer") + .HasColumnName("instr_status_id_cli"); + + b.Property("IsLast") + .HasColumnType("boolean") + .HasColumnName("last_lay"); + + b.Property("IsStriae") + .HasColumnType("boolean") + .HasColumnName("striae_lay"); + + b.Property("IsUndefined") + .HasColumnType("boolean") + .HasColumnName("undefined_lay"); + + b.Property("LithologyId") + .HasColumnType("integer") + .HasColumnName("lithology_id_cli"); + + b.Property("LithologyTopBedrockId") + .HasColumnType("integer") + .HasColumnName("lithology_top_bedrock_id_cli"); + + b.Property("LithostratigraphyId") + .HasColumnType("integer") + .HasColumnName("lithostratigraphy_id_cli"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes_lay"); + + b.Property("OriginalLithology") + .HasColumnType("text") + .HasColumnName("original_lithology"); + + b.Property("OriginalUscs") + .HasColumnType("text") + .HasColumnName("uscs_original_lay"); + + b.Property("PlasticityId") + .HasColumnType("integer") + .HasColumnName("plasticity_id_cli"); + + b.Property("QtDescriptionId") + .HasColumnType("integer") + .HasColumnName("qt_description_id_cli"); + + b.Property("StratigraphyId") + .HasColumnType("integer") + .HasColumnName("id_sty_fk"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("depth_to_lay"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update_lay"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater_lay"); + + b.Property("Uscs1Id") + .HasColumnType("integer") + .HasColumnName("uscs_1_id_cli"); + + b.Property("Uscs2Id") + .HasColumnType("integer") + .HasColumnName("uscs_2_id_cli"); + + b.Property("UscsDeterminationId") + .HasColumnType("integer") + .HasColumnName("uscs_determination_id_cli"); + + b.HasKey("Id"); + + b.HasIndex("AlterationId"); + + b.HasIndex("CasingKindId"); + + b.HasIndex("CasingMaterialId"); + + b.HasIndex("CohesionId"); + + b.HasIndex("CompactnessId"); + + b.HasIndex("ConsistanceId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("FillKindId"); + + b.HasIndex("FillMaterialId"); + + b.HasIndex("GradationId"); + + b.HasIndex("GrainSize1Id"); + + b.HasIndex("GrainSize2Id"); + + b.HasIndex("HumidityId"); + + b.HasIndex("InstrumentCasingId"); + + b.HasIndex("InstrumentKindId"); + + b.HasIndex("InstrumentStatusId"); + + b.HasIndex("LithologyId"); + + b.HasIndex("LithologyTopBedrockId"); + + b.HasIndex("LithostratigraphyId"); + + b.HasIndex("PlasticityId"); + + b.HasIndex("QtDescriptionId"); + + b.HasIndex("StratigraphyId"); + + b.HasIndex("UpdatedById"); + + b.HasIndex("Uscs1Id"); + + b.HasIndex("Uscs2Id"); + + b.HasIndex("UscsDeterminationId"); + + b.ToTable("layer", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.LayerCodelist", b => + { + b.Property("LayerId") + .HasColumnType("integer") + .HasColumnName("id_lay_fk"); + + b.Property("CodelistId") + .HasColumnType("integer") + .HasColumnName("id_cli_fk"); + + b.Property("SchemaName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("code_cli"); + + b.HasKey("LayerId", "CodelistId"); + + b.HasIndex("CodelistId"); + + b.ToTable("layer_codelist", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.LithologicalDescription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_ldp"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("depth_from"); + + b.Property("IsLast") + .HasColumnType("boolean") + .HasColumnName("is_last"); + + b.Property("QtDescriptionId") + .HasColumnType("integer") + .HasColumnName("qt_description_id"); + + b.Property("StratigraphyId") + .HasColumnType("integer") + .HasColumnName("id_sty_fk"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("depth_to"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("QtDescriptionId"); + + b.HasIndex("StratigraphyId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("lithological_description", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.LithostratigraphyLayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("FromDepth") + .HasColumnType("double precision") + .HasColumnName("depth_from"); + + b.Property("IsLast") + .HasColumnType("boolean") + .HasColumnName("is_last"); + + b.Property("LithostratigraphyId") + .HasColumnType("integer") + .HasColumnName("lithostratigraphy_id"); + + b.Property("StratigraphyId") + .HasColumnType("integer") + .HasColumnName("stratigraphy_id"); + + b.Property("ToDepth") + .HasColumnType("double precision") + .HasColumnName("depth_to"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("LithostratigraphyId"); + + b.HasIndex("StratigraphyId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("lithostratigraphy", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Observation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("borehole_id"); + + b.Property("CasingId") + .HasColumnType("integer") + .HasColumnName("casing"); + + b.Property("Comment") + .HasColumnType("text") + .HasColumnName("comment"); + + b.Property("CompletionFinished") + .HasColumnType("boolean") + .HasColumnName("completion_finished"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("creator"); + + b.Property("Duration") + .HasColumnType("double precision") + .HasColumnName("duration"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time"); + + b.Property("FromDepthM") + .HasColumnType("double precision") + .HasColumnName("from_depth_m"); + + b.Property("FromDepthMasl") + .HasColumnType("double precision") + .HasColumnName("from_depth_masl"); + + b.Property("ReliabilityId") + .HasColumnType("integer") + .HasColumnName("reliability"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.Property("ToDepthM") + .HasColumnType("double precision") + .HasColumnName("to_depth_m"); + + b.Property("ToDepthMasl") + .HasColumnType("double precision") + .HasColumnName("to_depth_masl"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("observation_type"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater"); + + b.HasKey("Id"); + + b.HasIndex("BoreholeId"); + + b.HasIndex("CasingId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReliabilityId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("observation", "bdms"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("BDMS.Models.Stratigraphy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_sty"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("id_bho_fk"); + + b.Property("Casing") + .HasColumnType("text") + .HasColumnName("casng_id"); + + b.Property("CasingDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("casng_date_abd_sty"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_sty"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("author_sty"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_sty"); + + b.Property("FillCasingId") + .HasColumnType("integer") + .HasColumnName("fill_casng_id_sty_fk"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("primary_sty"); + + b.Property("KindId") + .HasColumnType("integer") + .HasColumnName("kind_id_cli"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name_sty"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes_sty"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("update_sty"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updater_sty"); + + b.HasKey("Id"); + + b.HasIndex("BoreholeId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("FillCasingId"); + + b.HasIndex("KindId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("stratigraphy", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Term", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_tes"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Creation") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_tes"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_tes"); + + b.Property("IsDraft") + .HasColumnType("boolean") + .HasColumnName("draft_tes"); + + b.Property("TextDe") + .HasColumnType("text") + .HasColumnName("text_tes_de"); + + b.Property("TextEn") + .IsRequired() + .HasColumnType("text") + .HasColumnName("text_tes_en"); + + b.Property("TextFr") + .HasColumnType("text") + .HasColumnName("text_tes_fr"); + + b.Property("TextIt") + .HasColumnType("text") + .HasColumnName("text_tes_it"); + + b.Property("TextRo") + .HasColumnType("text") + .HasColumnName("text_tes_ro"); + + b.HasKey("Id"); + + b.ToTable("terms", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_usr"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("disabled_usr"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("firstname"); + + b.Property("IsAdmin") + .HasColumnType("boolean") + .HasColumnName("admin_usr"); + + b.Property("IsViewer") + .HasColumnType("boolean") + .HasColumnName("viewer_usr"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("lastname"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.Property("subject_id") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("users", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.UserEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_evs"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_evs"); + + b.Property("Payload") + .HasColumnType("jsonb") + .HasColumnName("payload_evs"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text") + .HasColumnName("topic_evs"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id_usr_fk"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("events", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.UserWorkgroupRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id_usr_fk"); + + b.Property("WorkgroupId") + .HasColumnType("integer") + .HasColumnName("id_wgp_fk"); + + b.Property("Role") + .HasColumnType("int") + .HasColumnName("id_rol_fk"); + + b.HasKey("UserId", "WorkgroupId", "Role"); + + b.ToTable("users_roles", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Workflow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_wkf"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoreholeId") + .HasColumnType("integer") + .HasColumnName("id_bho_fk"); + + b.Property("Finished") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_wkf"); + + b.Property("Notes") + .HasColumnType("text") + .HasColumnName("notes_wkf"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("id_rol_fk"); + + b.Property("Started") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_wkf"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("id_usr_fk"); + + b.HasKey("Id"); + + b.HasIndex("BoreholeId"); + + b.HasIndex("UserId"); + + b.ToTable("workflow", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Workgroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id_wgp"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_wgp"); + + b.Property("Disabled") + .HasColumnType("timestamp with time zone") + .HasColumnName("disabled_wgp"); + + b.Property("IsSupplier") + .HasColumnType("boolean") + .HasColumnName("supplier_wgp"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name_wgp"); + + b.Property("Settings") + .HasColumnType("json") + .HasColumnName("settings_wgp"); + + b.HasKey("Id"); + + b.ToTable("workgroups", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.FieldMeasurement", b => + { + b.HasBaseType("BDMS.Models.Observation"); + + b.Property("ParameterId") + .HasColumnType("integer") + .HasColumnName("parameter"); + + b.Property("SampleTypeId") + .HasColumnType("integer") + .HasColumnName("sample_type"); + + b.Property("Value") + .HasColumnType("double precision") + .HasColumnName("value"); + + b.HasIndex("ParameterId"); + + b.HasIndex("SampleTypeId"); + + b.ToTable("field_measurement", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.GroundwaterLevelMeasurement", b => + { + b.HasBaseType("BDMS.Models.Observation"); + + b.Property("KindId") + .HasColumnType("integer") + .HasColumnName("kind"); + + b.Property("LevelM") + .HasColumnType("double precision") + .HasColumnName("level_m"); + + b.Property("LevelMasl") + .HasColumnType("double precision") + .HasColumnName("level_masl"); + + b.HasIndex("KindId"); + + b.ToTable("groundwater_level_measurement", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Hydrotest", b => + { + b.HasBaseType("BDMS.Models.Observation"); + + b.ToTable("hydrotest", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.WaterIngress", b => + { + b.HasBaseType("BDMS.Models.Observation"); + + b.Property("ConditionsId") + .HasColumnType("integer") + .HasColumnName("conditions"); + + b.Property("QuantityId") + .HasColumnType("integer") + .HasColumnName("quantity"); + + b.HasIndex("ConditionsId"); + + b.HasIndex("QuantityId"); + + b.ToTable("water_ingress", "bdms"); + }); + + modelBuilder.Entity("BDMS.Models.Borehole", b => + { + b.HasOne("BDMS.Models.Codelist", "Chronostratigraphy") + .WithMany() + .HasForeignKey("ChronostratigraphyId"); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "Cuttings") + .WithMany() + .HasForeignKey("CuttingsId"); + + b.HasOne("BDMS.Models.Codelist", "DrillingMethod") + .WithMany() + .HasForeignKey("DrillingMethodId"); + + b.HasOne("BDMS.Models.Codelist", "Hrs") + .WithMany() + .HasForeignKey("HrsId"); + + b.HasOne("BDMS.Models.Codelist", "Kind") + .WithMany() + .HasForeignKey("KindId"); + + b.HasOne("BDMS.Models.Codelist", "LithologyTopBedrock") + .WithMany() + .HasForeignKey("LithologyTopBedrockId"); + + b.HasOne("BDMS.Models.Codelist", "Lithostratigraphy") + .WithMany() + .HasForeignKey("LithostratigraphyId"); + + b.HasOne("BDMS.Models.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.HasOne("BDMS.Models.Codelist", "Purpose") + .WithMany() + .HasForeignKey("PurposeId"); + + b.HasOne("BDMS.Models.Codelist", "QtDepth") + .WithMany() + .HasForeignKey("QtDepthId"); + + b.HasOne("BDMS.Models.Codelist", "QtElevation") + .WithMany() + .HasForeignKey("QtElevationId"); + + b.HasOne("BDMS.Models.Codelist", "QtInclinationDirection") + .WithMany() + .HasForeignKey("QtInclinationDirectionId"); + + b.HasOne("BDMS.Models.Codelist", "QtLocation") + .WithMany() + .HasForeignKey("QtLocationId"); + + b.HasOne("BDMS.Models.Codelist", "QtReferenceElevation") + .WithMany() + .HasForeignKey("QtReferenceElevationId"); + + b.HasOne("BDMS.Models.Codelist", "QtTotalDepthTvd") + .WithMany() + .HasForeignKey("QtTotalDepthTvdId"); + + b.HasOne("BDMS.Models.Codelist", "ReferenceElevationType") + .WithMany() + .HasForeignKey("ReferenceElevationTypeId"); + + b.HasOne("BDMS.Models.Codelist", "Restriction") + .WithMany() + .HasForeignKey("RestrictionId"); + + b.HasOne("BDMS.Models.Codelist", "Status") + .WithMany() + .HasForeignKey("StatusId"); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.HasOne("BDMS.Models.Workgroup", "Workgroup") + .WithMany("Boreholes") + .HasForeignKey("WorkgroupId"); + + b.Navigation("Chronostratigraphy"); + + b.Navigation("CreatedBy"); + + b.Navigation("Cuttings"); + + b.Navigation("DrillingMethod"); + + b.Navigation("Hrs"); + + b.Navigation("Kind"); + + b.Navigation("LithologyTopBedrock"); + + b.Navigation("Lithostratigraphy"); + + b.Navigation("LockedBy"); + + b.Navigation("Purpose"); + + b.Navigation("QtDepth"); + + b.Navigation("QtElevation"); + + b.Navigation("QtInclinationDirection"); + + b.Navigation("QtLocation"); + + b.Navigation("QtReferenceElevation"); + + b.Navigation("QtTotalDepthTvd"); + + b.Navigation("ReferenceElevationType"); + + b.Navigation("Restriction"); + + b.Navigation("Status"); + + b.Navigation("UpdatedBy"); + + b.Navigation("Workgroup"); + }); + + modelBuilder.Entity("BDMS.Models.BoreholeCodelist", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany("BoreholeCodelists") + .HasForeignKey("BoreholeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "Codelist") + .WithMany("BoreholeCodelists") + .HasForeignKey("CodelistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borehole"); + + b.Navigation("Codelist"); + }); + + modelBuilder.Entity("BDMS.Models.BoreholeFile", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany("BoreholeFiles") + .HasForeignKey("BoreholeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.File", "File") + .WithMany("BoreholeFiles") + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.HasOne("BDMS.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Borehole"); + + b.Navigation("CreatedBy"); + + b.Navigation("File"); + + b.Navigation("UpdatedBy"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BDMS.Models.ChronostratigraphyLayer", b => + { + b.HasOne("BDMS.Models.Codelist", "Chronostratigraphy") + .WithMany() + .HasForeignKey("ChronostratigraphyId"); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Stratigraphy", "Stratigraphy") + .WithMany("ChronostratigraphyLayers") + .HasForeignKey("StratigraphyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Chronostratigraphy"); + + b.Navigation("CreatedBy"); + + b.Navigation("Stratigraphy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.Completion", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany() + .HasForeignKey("BoreholeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "Kind") + .WithMany() + .HasForeignKey("KindId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Borehole"); + + b.Navigation("CreatedBy"); + + b.Navigation("Kind"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.FaciesDescription", b => + { + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "QtDescription") + .WithMany() + .HasForeignKey("QtDescriptionId"); + + b.HasOne("BDMS.Models.Stratigraphy", "Stratigraphy") + .WithMany("FaciesDescriptions") + .HasForeignKey("StratigraphyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("QtDescription"); + + b.Navigation("Stratigraphy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.File", b => + { + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.HydrotestCodelist", b => + { + b.HasOne("BDMS.Models.Codelist", "Codelist") + .WithMany("HydrotestCodelists") + .HasForeignKey("CodelistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Hydrotest", "Hydrotest") + .WithMany("HydrotestCodelists") + .HasForeignKey("HydrotestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Codelist"); + + b.Navigation("Hydrotest"); + }); + + modelBuilder.Entity("BDMS.Models.HydrotestResult", b => + { + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Hydrotest", "Hydrotest") + .WithMany("HydrotestResults") + .HasForeignKey("HydrotestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "Parameter") + .WithMany() + .HasForeignKey("ParameterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("Hydrotest"); + + b.Navigation("Parameter"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.Instrumentation", b => + { + b.HasOne("BDMS.Models.Completion", "Completion") + .WithMany() + .HasForeignKey("CompletionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "Kind") + .WithMany() + .HasForeignKey("KindId"); + + b.HasOne("BDMS.Models.Codelist", "Status") + .WithMany() + .HasForeignKey("StatusId"); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Completion"); + + b.Navigation("CreatedBy"); + + b.Navigation("Kind"); + + b.Navigation("Status"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.Layer", b => + { + b.HasOne("BDMS.Models.Codelist", "Alteration") + .WithMany() + .HasForeignKey("AlterationId"); + + b.HasOne("BDMS.Models.Codelist", "CasingKind") + .WithMany() + .HasForeignKey("CasingKindId"); + + b.HasOne("BDMS.Models.Codelist", "CasingMaterial") + .WithMany() + .HasForeignKey("CasingMaterialId"); + + b.HasOne("BDMS.Models.Codelist", "Cohesion") + .WithMany() + .HasForeignKey("CohesionId"); + + b.HasOne("BDMS.Models.Codelist", "Compactness") + .WithMany() + .HasForeignKey("CompactnessId"); + + b.HasOne("BDMS.Models.Codelist", "Consistance") + .WithMany() + .HasForeignKey("ConsistanceId"); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "FillKind") + .WithMany() + .HasForeignKey("FillKindId"); + + b.HasOne("BDMS.Models.Codelist", "FillMaterial") + .WithMany() + .HasForeignKey("FillMaterialId"); + + b.HasOne("BDMS.Models.Codelist", "Gradation") + .WithMany() + .HasForeignKey("GradationId"); + + b.HasOne("BDMS.Models.Codelist", "GrainSize1") + .WithMany() + .HasForeignKey("GrainSize1Id"); + + b.HasOne("BDMS.Models.Codelist", "GrainSize2") + .WithMany() + .HasForeignKey("GrainSize2Id"); + + b.HasOne("BDMS.Models.Codelist", "Humidity") + .WithMany() + .HasForeignKey("HumidityId"); + + b.HasOne("BDMS.Models.Stratigraphy", "InstrumentCasing") + .WithMany() + .HasForeignKey("InstrumentCasingId"); + + b.HasOne("BDMS.Models.Codelist", "InstrumentKind") + .WithMany() + .HasForeignKey("InstrumentKindId"); + + b.HasOne("BDMS.Models.Codelist", "InstrumentStatus") + .WithMany() + .HasForeignKey("InstrumentStatusId"); + + b.HasOne("BDMS.Models.Codelist", "Lithology") + .WithMany() + .HasForeignKey("LithologyId"); + + b.HasOne("BDMS.Models.Codelist", "LithologyTopBedrock") + .WithMany() + .HasForeignKey("LithologyTopBedrockId"); + + b.HasOne("BDMS.Models.Codelist", "Lithostratigraphy") + .WithMany() + .HasForeignKey("LithostratigraphyId"); + + b.HasOne("BDMS.Models.Codelist", "Plasticity") + .WithMany() + .HasForeignKey("PlasticityId"); + + b.HasOne("BDMS.Models.Codelist", "QtDescription") + .WithMany() + .HasForeignKey("QtDescriptionId"); + + b.HasOne("BDMS.Models.Stratigraphy", "Stratigraphy") + .WithMany("Layers") + .HasForeignKey("StratigraphyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.HasOne("BDMS.Models.Codelist", "Uscs1") + .WithMany() + .HasForeignKey("Uscs1Id"); + + b.HasOne("BDMS.Models.Codelist", "Uscs2") + .WithMany() + .HasForeignKey("Uscs2Id"); + + b.HasOne("BDMS.Models.Codelist", "UscsDetermination") + .WithMany() + .HasForeignKey("UscsDeterminationId"); + + b.Navigation("Alteration"); + + b.Navigation("CasingKind"); + + b.Navigation("CasingMaterial"); + + b.Navigation("Cohesion"); + + b.Navigation("Compactness"); + + b.Navigation("Consistance"); + + b.Navigation("CreatedBy"); + + b.Navigation("FillKind"); + + b.Navigation("FillMaterial"); + + b.Navigation("Gradation"); + + b.Navigation("GrainSize1"); + + b.Navigation("GrainSize2"); + + b.Navigation("Humidity"); + + b.Navigation("InstrumentCasing"); + + b.Navigation("InstrumentKind"); + + b.Navigation("InstrumentStatus"); + + b.Navigation("Lithology"); + + b.Navigation("LithologyTopBedrock"); + + b.Navigation("Lithostratigraphy"); + + b.Navigation("Plasticity"); + + b.Navigation("QtDescription"); + + b.Navigation("Stratigraphy"); + + b.Navigation("UpdatedBy"); + + b.Navigation("Uscs1"); + + b.Navigation("Uscs2"); + + b.Navigation("UscsDetermination"); + }); + + modelBuilder.Entity("BDMS.Models.LayerCodelist", b => + { + b.HasOne("BDMS.Models.Codelist", "Codelist") + .WithMany("LayerCodelists") + .HasForeignKey("CodelistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Layer", "Layer") + .WithMany("LayerCodelists") + .HasForeignKey("LayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Codelist"); + + b.Navigation("Layer"); + }); + + modelBuilder.Entity("BDMS.Models.LithologicalDescription", b => + { + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "QtDescription") + .WithMany() + .HasForeignKey("QtDescriptionId"); + + b.HasOne("BDMS.Models.Stratigraphy", "Stratigraphy") + .WithMany("LithologicalDescriptions") + .HasForeignKey("StratigraphyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("QtDescription"); + + b.Navigation("Stratigraphy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.LithostratigraphyLayer", b => + { + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "Lithostratigraphy") + .WithMany() + .HasForeignKey("LithostratigraphyId"); + + b.HasOne("BDMS.Models.Stratigraphy", "Stratigraphy") + .WithMany("LithostratigraphyLayers") + .HasForeignKey("StratigraphyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("Lithostratigraphy"); + + b.Navigation("Stratigraphy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.Observation", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany() + .HasForeignKey("BoreholeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Stratigraphy", "Casing") + .WithMany() + .HasForeignKey("CasingId"); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Codelist", "Reliability") + .WithMany() + .HasForeignKey("ReliabilityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Borehole"); + + b.Navigation("Casing"); + + b.Navigation("CreatedBy"); + + b.Navigation("Reliability"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.Stratigraphy", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany("Stratigraphies") + .HasForeignKey("BoreholeId"); + + b.HasOne("BDMS.Models.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("BDMS.Models.Stratigraphy", "FillCasing") + .WithMany() + .HasForeignKey("FillCasingId"); + + b.HasOne("BDMS.Models.Codelist", "Kind") + .WithMany() + .HasForeignKey("KindId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Borehole"); + + b.Navigation("CreatedBy"); + + b.Navigation("FillCasing"); + + b.Navigation("Kind"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("BDMS.Models.UserEvent", b => + { + b.HasOne("BDMS.Models.User", "User") + .WithMany("BoringEvents") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BDMS.Models.UserWorkgroupRole", b => + { + b.HasOne("BDMS.Models.User", null) + .WithMany("WorkgroupRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BDMS.Models.Workflow", b => + { + b.HasOne("BDMS.Models.Borehole", "Borehole") + .WithMany("Workflows") + .HasForeignKey("BoreholeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borehole"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BDMS.Models.FieldMeasurement", b => + { + b.HasOne("BDMS.Models.Observation", null) + .WithOne() + .HasForeignKey("BDMS.Models.FieldMeasurement", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "Parameter") + .WithMany() + .HasForeignKey("ParameterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "SampleType") + .WithMany() + .HasForeignKey("SampleTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Parameter"); + + b.Navigation("SampleType"); + }); + + modelBuilder.Entity("BDMS.Models.GroundwaterLevelMeasurement", b => + { + b.HasOne("BDMS.Models.Observation", null) + .WithOne() + .HasForeignKey("BDMS.Models.GroundwaterLevelMeasurement", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "Kind") + .WithMany() + .HasForeignKey("KindId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kind"); + }); + + modelBuilder.Entity("BDMS.Models.Hydrotest", b => + { + b.HasOne("BDMS.Models.Observation", null) + .WithOne() + .HasForeignKey("BDMS.Models.Hydrotest", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BDMS.Models.WaterIngress", b => + { + b.HasOne("BDMS.Models.Codelist", "Conditions") + .WithMany() + .HasForeignKey("ConditionsId"); + + b.HasOne("BDMS.Models.Observation", null) + .WithOne() + .HasForeignKey("BDMS.Models.WaterIngress", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BDMS.Models.Codelist", "Quantity") + .WithMany() + .HasForeignKey("QuantityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Conditions"); + + b.Navigation("Quantity"); + }); + + modelBuilder.Entity("BDMS.Models.Borehole", b => + { + b.Navigation("BoreholeCodelists"); + + b.Navigation("BoreholeFiles"); + + b.Navigation("Stratigraphies"); + + b.Navigation("Workflows"); + }); + + modelBuilder.Entity("BDMS.Models.Codelist", b => + { + b.Navigation("BoreholeCodelists"); + + b.Navigation("HydrotestCodelists"); + + b.Navigation("LayerCodelists"); + }); + + modelBuilder.Entity("BDMS.Models.File", b => + { + b.Navigation("BoreholeFiles"); + }); + + modelBuilder.Entity("BDMS.Models.Layer", b => + { + b.Navigation("LayerCodelists"); + }); + + modelBuilder.Entity("BDMS.Models.Stratigraphy", b => + { + b.Navigation("ChronostratigraphyLayers"); + + b.Navigation("FaciesDescriptions"); + + b.Navigation("Layers"); + + b.Navigation("LithologicalDescriptions"); + + b.Navigation("LithostratigraphyLayers"); + }); + + modelBuilder.Entity("BDMS.Models.User", b => + { + b.Navigation("BoringEvents"); + + b.Navigation("WorkgroupRoles"); + }); + + modelBuilder.Entity("BDMS.Models.Workgroup", b => + { + b.Navigation("Boreholes"); + }); + + modelBuilder.Entity("BDMS.Models.Hydrotest", b => + { + b.Navigation("HydrotestCodelists"); + + b.Navigation("HydrotestResults"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/Migrations/20240109150510_AddUserSubjectId.cs b/src/api/Migrations/20240109150510_AddUserSubjectId.cs new file mode 100644 index 000000000..063417b2e --- /dev/null +++ b/src/api/Migrations/20240109150510_AddUserSubjectId.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BDMS.Migrations; + +/// +public partial class AddUserSubjectId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "subject_id", + schema: "bdms", + table: "users", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.DropUniqueConstraint( + name: "users_username_key", + table: "users", + schema: "bdms"); + + migrationBuilder.Sql(@" + UPDATE bdms.users + SET + subject_id = CONCAT('sub_', username), + username = CONCAT(SUBSTRING(firstname, 1, 1), '. ', lastname); + "); + + migrationBuilder.AddUniqueConstraint( + name: "users_subject_id_unique", + table: "users", + column: "subject_id", + schema: "bdms"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "users_subject_id_unique", + table: "users", + schema: "bdms"); + + migrationBuilder.Sql(@" + UPDATE bdms.users + SET username = SUBSTRING(subject_id, 5);"); + + migrationBuilder.AddUniqueConstraint( + name: "users_username_key", + table: "users", + column: "username", + schema: "bdms"); + + migrationBuilder.DropColumn( + name: "subject_id", + schema: "bdms", + table: "users"); + } +} diff --git a/src/api/Migrations/BdmsContextModelSnapshot.cs b/src/api/Migrations/BdmsContextModelSnapshot.cs index 81c4911f0..f44933bc9 100644 --- a/src/api/Migrations/BdmsContextModelSnapshot.cs +++ b/src/api/Migrations/BdmsContextModelSnapshot.cs @@ -1597,6 +1597,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("username"); + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("SubjectId") + .IsRequired() + .HasColumnType("text"); + b.HasKey("Id"); b.ToTable("users", "bdms"); diff --git a/src/api/Models/User.cs b/src/api/Models/User.cs index 087d19ca2..e240b7898 100644 --- a/src/api/Models/User.cs +++ b/src/api/Models/User.cs @@ -20,6 +20,12 @@ public class User : IIdentifyable [Column("username")] public string Name { get; set; } + /// + /// Gets or sets the s subject id provided by oidc. + /// + [Column("subject_id")] + public string SubjectId { get; set; } + /// /// Gets or sets the s firstname. /// @@ -71,6 +77,10 @@ public class User : IIdentifyable [NotMapped] public bool? Deletable { get; set; } + // TODO: Remove with #911 + [Column("password")] + public string Password { get; set; } + /// public override string ToString() => Name; } diff --git a/src/api/Program.cs b/src/api/Program.cs index 5d989df93..564227825 100644 --- a/src/api/Program.cs +++ b/src/api/Program.cs @@ -3,8 +3,8 @@ using BDMS; using BDMS.Authentication; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using System.Reflection; @@ -21,6 +21,11 @@ }); builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => builder.Configuration.Bind("Auth", options)); + +builder.Services.AddTransient(); + builder.Services.AddAuthorization(options => { options.AddPolicy(PolicyNames.Admin, options => options.RequireRole(PolicyNames.Admin)); @@ -35,9 +40,6 @@ options.FallbackPolicy = options.DefaultPolicy; }); -builder.Services.AddAuthentication("BasicAuthentication") - .AddScheme("BasicAuthentication", null); - builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpClient(); @@ -62,11 +64,10 @@ Version = "v2", Title = "BDMS REST API v2", }); - options.AddSecurityDefinition("Basic", new OpenApiSecurityScheme + options.AddSecurityDefinition("OpenIdConnect", new OpenApiSecurityScheme { In = ParameterLocation.Header, - Type = SecuritySchemeType.Http, - Scheme = nameof(AuthenticationSchemes.Basic), + Type = SecuritySchemeType.OpenIdConnect, }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { @@ -76,7 +77,7 @@ Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, - Id = nameof(AuthenticationSchemes.Basic), + Id = nameof(SecuritySchemeType.OpenIdConnect), }, }, Array.Empty() diff --git a/src/api/appsettings.Development.json b/src/api/appsettings.Development.json index 2c102edfd..2ab8aaa39 100644 --- a/src/api/appsettings.Development.json +++ b/src/api/appsettings.Development.json @@ -15,6 +15,11 @@ "SECRET_KEY": "YELLOWMONKEY", "SECURE": "0" }, + "Auth": { + "Authority": "http://localhost:4011", + "Audience": "bdms-client", + "RequireHttpsMetadata": "false" + }, "ReverseProxy": { "Clusters": { "pythonApi": { diff --git a/src/client/.env b/src/client/.env index 2c4c17571..880790c6b 100644 --- a/src/client/.env +++ b/src/client/.env @@ -1 +1,3 @@ REACT_APP_PROXY_HOST_API=http://localhost:5000/ +REACT_APP_CLIENT_AUTHORITY=http://localhost:4011/ +REACT_APP_CLIENT_CLIENT_ID=bdms-client diff --git a/src/client/Dockerfile b/src/client/Dockerfile index e2f744f97..493b96ec6 100644 --- a/src/client/Dockerfile +++ b/src/client/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get -y install git vim curl htop python3 python3-pip RUN mkdir -p /app/src/client WORKDIR /app/src/client -COPY ./package.json ./package-lock.json ./server.js ./ +COPY ./package.json ./package-lock.json ./ RUN npm install # Build docs @@ -30,7 +30,6 @@ WORKDIR /app COPY --from=development /app/src/client/node_modules /app/node_modules COPY --from=development /app/src/client/public/help /app/public/help -COPY --from=development /app/src/client/server.js /app/server.js COPY . . diff --git a/src/client/cypress/e2e/admin/aboutSettings.cy.js b/src/client/cypress/e2e/admin/aboutSettings.cy.js index b24df7783..c390ef11b 100644 --- a/src/client/cypress/e2e/admin/aboutSettings.cy.js +++ b/src/client/cypress/e2e/admin/aboutSettings.cy.js @@ -1,9 +1,10 @@ -import { login } from "../testHelpers"; +import { loginAsAdmin } from "../testHelpers"; import license from "../../fixtures/license.json"; describe("Admin about page tests", () => { it("shows version information linking the corresponding release on GitHub.", () => { - login("/setting/about"); + loginAsAdmin(); + cy.visit("/setting/about"); cy.get('[data-cy="version"]') .should("contain", "0.0.99+dev") @@ -16,7 +17,8 @@ describe("Admin about page tests", () => { it("shows license information (with fixtures)", () => { cy.intercept("/license.json", license); - login("/setting/about"); + loginAsAdmin(); + cy.visit("/setting/about"); cy.get('[data-cy^="credits-"]').should("have.length", 2); cy.get('[data-cy="credits-example-js@0.0.999"]').should( @@ -30,7 +32,8 @@ describe("Admin about page tests", () => { }); it("shows license information (without fixtures)", () => { - login("/setting/about"); + loginAsAdmin(); + cy.visit("/setting/about"); cy.get('[data-cy^="credits-"]').should("have.length.above", 0); }); }); diff --git a/src/client/cypress/e2e/admin/loginScreen.cy.js b/src/client/cypress/e2e/admin/loginScreen.cy.js index 4af52443b..2720da6ce 100644 --- a/src/client/cypress/e2e/admin/loginScreen.cy.js +++ b/src/client/cypress/e2e/admin/loginScreen.cy.js @@ -1,8 +1,9 @@ -import { login } from "../testHelpers"; +import { loginAsAdmin } from "../testHelpers"; describe("Admin login preview tests", () => { it("displays correct message when publishing a new welcome message.", () => { - login("/setting/login"); + loginAsAdmin(); + cy.visit("/setting/login"); // Initial button state cy.get('[data-cy="save-welcome-message-button"]').should("not.be.visible"); diff --git a/src/client/cypress/e2e/admin/userAdministration.cy.js b/src/client/cypress/e2e/admin/userAdministration.cy.js index cde07f54c..372549170 100644 --- a/src/client/cypress/e2e/admin/userAdministration.cy.js +++ b/src/client/cypress/e2e/admin/userAdministration.cy.js @@ -2,14 +2,15 @@ import { loginAsAdmin } from "../testHelpers"; describe("Admin settings test", () => { beforeEach(() => { - loginAsAdmin("/setting/admin"); + loginAsAdmin(); + cy.visit("/setting/admin"); cy.get('[data-cy="user-list-table-body"]') .children() .should("have.length", 7); }); - it("displays correct message when enabling user.", () => { + it.skip("displays correct message when enabling user.", () => { // add user cy.get('[placeholder="Username"]').type("Testuser"); cy.get('[placeholder="Password"]').type("123456"); @@ -72,7 +73,7 @@ describe("Admin settings test", () => { cy.get('.modal [data-cy="permanently-delete-user-button"]').click(); }); - it("can add user with admin role.", () => { + it.skip("can add user with admin role.", () => { // add admin user cy.get('[data-cy="admin-checkbox"]').click(); @@ -148,7 +149,7 @@ describe("Admin settings test", () => { let filesUser = cy .get('[data-cy="user-list-table-body"]') .children() - .contains("tr", "filesUser"); + .contains("tr", "has_files"); cy.contains("user_that_only"); cy.contains("has_files"); @@ -168,7 +169,7 @@ describe("Admin settings test", () => { let deletableUser = cy .get('[data-cy="user-list-table-body"]') .children() - .contains("tr", "deletableUser"); + .contains("tr", "be_deleted"); cy.contains("user_that_can"); cy.contains("be_deleted"); @@ -243,7 +244,7 @@ describe("Admin settings test", () => { // is reloaded and assert role assignments cy.get('[data-cy="user-list-table-body"]') .children() - .contains("td", "admin") + .contains("td", "Admin") .click(); cy.get('[data-cy="user-list-table-body"]') diff --git a/src/client/cypress/e2e/app.cy.js b/src/client/cypress/e2e/app.cy.js index 1ca6ed332..c32cffb51 100644 --- a/src/client/cypress/e2e/app.cy.js +++ b/src/client/cypress/e2e/app.cy.js @@ -3,6 +3,7 @@ import { loginAsAdmin, loginAsEditorInViewerMode } from "./testHelpers"; describe("General app tests", () => { it("Displays the login page in the correct language", () => { // default is english + cy.session("logged out", () => cy.visit("/")); cy.visit("/"); cy.contains("Sign in"); cy.contains("Welcome to"); @@ -25,12 +26,14 @@ describe("General app tests", () => { it("Displays the current host as app title", () => { loginAsEditorInViewerMode(); + cy.visit("/"); cy.get('[data-cy="app-title"]').contains("localhost"); }); it("Correctly navigates back and forth from settings", () => { // correctly navigate back to viewer mode loginAsAdmin(); + cy.visit("/"); cy.get('[data-cy="menu"]').click(); cy.get('[data-cy="settings-list-item"]').click(); cy.contains("h3", "Done").click(); diff --git a/src/client/cypress/e2e/boreholeList.cy.js b/src/client/cypress/e2e/boreholeList.cy.js index b1e204ee1..cc2dcc14b 100644 --- a/src/client/cypress/e2e/boreholeList.cy.js +++ b/src/client/cypress/e2e/boreholeList.cy.js @@ -4,6 +4,7 @@ describe("Borehole list tests", () => { it("Boreholes are displayed in correct order with editor login", () => { cy.intercept("/api/v1/borehole").as("borehole"); loginAsEditorInViewerMode(); + cy.visit("/"); cy.get("div[id=map]").should("be.visible"); @@ -80,7 +81,8 @@ describe("Borehole list tests", () => { it("Boreholes are displayed in correct order with admin login", () => { cy.intercept("/api/v1/borehole").as("borehole"); cy.intercept("/api/v1/borehole/edit").as("editorBorehole"); - loginAsAdmin("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.wait("@editorBorehole"); cy.get("div[id=map]").should("be.visible"); @@ -123,14 +125,14 @@ describe("Borehole list tests", () => { } }); - cy.get("tbody").children().eq(0).contains("td", "publisher"); - cy.get("tbody").children().eq(1).contains("td", "publisher"); - cy.get("tbody").children().eq(2).contains("td", "publisher"); + cy.get("tbody").children().eq(0).contains("td", "p. user"); + cy.get("tbody").children().eq(1).contains("td", "p. user"); + cy.get("tbody").children().eq(2).contains("td", "p. user"); cy.contains("th", "Created by").click(); - cy.get("tbody").children().eq(0).contains("td", "admin"); - cy.get("tbody").children().eq(1).contains("td", "admin"); - cy.get("tbody").children().eq(2).contains("td", "admin"); + cy.get("tbody").children().eq(0).contains("td", "A. User"); + cy.get("tbody").children().eq(1).contains("td", "A. User"); + cy.get("tbody").children().eq(2).contains("td", "A. User"); // sort by name ascending cy.contains("th", "Original name").click(); @@ -173,6 +175,7 @@ describe("Borehole list tests", () => { it("preserves column sorting and active page when navigating", () => { loginAsEditorInViewerMode(); + cy.visit("/"); // sort by name ascending cy.contains("div", "Original name") diff --git a/src/client/cypress/e2e/codeList.cy.js b/src/client/cypress/e2e/codeList.cy.js index 56969e358..485c80333 100644 --- a/src/client/cypress/e2e/codeList.cy.js +++ b/src/client/cypress/e2e/codeList.cy.js @@ -4,11 +4,8 @@ describe("Codelist translations tests", () => { it("Admin can open codelist translation section", () => { // Login and navigate to editor settings loginAsAdmin(); - cy.get("div[id=map]").should("be.visible"); + cy.visit("/setting/editor"); - cy.get("i[class='th big icon']").click(); - cy.contains("h4", "Settings").click(); - cy.contains("h3", "Editor").click(); cy.get("button").should("not.contain", "Collapse"); cy.contains("div", "Codelist translations") @@ -22,11 +19,7 @@ describe("Codelist translations tests", () => { it("Admin can edit translations", () => { loginAsAdmin(); - cy.get("div[id=map]").should("be.visible"); - - cy.get("i[class='th big icon']").click(); - cy.contains("h4", "Settings").click(); - cy.contains("h3", "Editor").click(); + cy.visit("/setting/editor"); cy.contains("div", "Codelist translations") .parent("div") .children("div") @@ -88,22 +81,14 @@ describe("Codelist translations tests", () => { it("Editor cannot open codelist translation section", () => { loginAsEditorInViewerMode(); - cy.get("div[id=map]").should("be.visible"); - - cy.get("i[class='th big icon']").click(); - cy.contains("h4", "Settings").click(); - cy.contains("h3", "Editor").click(); + cy.visit("/setting/editor"); // Codelist translation section is not available cy.get("div").should("not.contain", "Codelist translations"); }); it("Admin can edit order", () => { loginAsAdmin(); - cy.get("div[id=map]").should("be.visible"); - - cy.get("i[class='th big icon']").click(); - cy.contains("h4", "Settings").click(); - cy.contains("h3", "Editor").click(); + cy.visit("/setting/editor"); cy.contains("div", "Codelist translations") .parent("div") .children("div") diff --git a/src/client/cypress/e2e/editor/boreholeEditorTable.cy.js b/src/client/cypress/e2e/editor/boreholeEditorTable.cy.js index 1f53ae29e..99deb31dd 100644 --- a/src/client/cypress/e2e/editor/boreholeEditorTable.cy.js +++ b/src/client/cypress/e2e/editor/boreholeEditorTable.cy.js @@ -1,8 +1,9 @@ -import { login } from "../../e2e/testHelpers"; +import { loginAsAdmin } from "../../e2e/testHelpers"; describe("Borehole editor table tests", () => { it("preserves column sorting and active page when navigating", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); // sort by name ascending cy.contains("th", "Original name").click(); diff --git a/src/client/cypress/e2e/editor/bulkedit.cy.js b/src/client/cypress/e2e/editor/bulkedit.cy.js index 58db6243e..f910fd036 100644 --- a/src/client/cypress/e2e/editor/bulkedit.cy.js +++ b/src/client/cypress/e2e/editor/bulkedit.cy.js @@ -1,55 +1,18 @@ -import { createBorehole, login, adminUserAuth } from "../testHelpers"; - -export const changeWorkgroupRoleForUser = ( - workgroupId, - userId, - role, - action, -) => { - return cy - .request({ - method: "POST", - url: "/api/v1/user/workgroup/edit", - body: { - action: action, - id: workgroupId, - }, - auth: adminUserAuth, - }) - .then(res => { - expect(res.body).to.have.property("success", true); - cy.request({ - method: "POST", - url: "/api/v1/user/workgroup/edit", - body: { - action: "SET", - user_id: userId, - workgroup_id: workgroupId, - role_name: role, - active: action === "ENABLE" ? true : false, - }, - auth: adminUserAuth, - }).then(res => { - expect(res.body).to.have.property("success", true); - cy.request({ - method: "GET", - url: "/api/v2/user", - auth: adminUserAuth, - }); - }); - }); -}; +import { createBorehole, loginAsAdmin } from "../testHelpers"; +import adminUser from "../../fixtures/adminUser.json"; describe("Test the borehole bulk edit feature.", () => { it("opens the bulk edit dialog with all boreholes selected", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.get('[data-cy="borehole-table"] thead .checkbox').click({ force: true }); cy.contains("button", "Bulk editing").click({ force: true }); cy.wait("@edit_ids"); }); it("checks if all toggle buttons do something", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.get('[data-cy="borehole-table"] thead .checkbox').click({ force: true }); cy.contains("button", "Bulk editing").click({ force: true }); @@ -64,13 +27,26 @@ describe("Test the borehole bulk edit feature.", () => { }); it("displays workgroup toggle only if user has permission for more than one workgroup", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.get('[data-cy="borehole-table"] thead .checkbox').click({ force: true }); cy.contains("button", "Bulk editing").click({ force: true }); - cy.get(".modal .toggle").should("have.length", 30); - changeWorkgroupRoleForUser(6, 1, "EDIT", "ENABLE"); - login("/editor"); + + loginAsAdmin("admin"); + const adminUser2Workgroups = Object.assign({}, adminUser); + adminUser2Workgroups.data.workgroups.push({ + id: 6, + workgroup: "Blue", + roles: ["EDIT"], + disabled: null, + supplier: false, + }); + cy.intercept("/api/v1/user", { + statusCode: 200, + body: JSON.stringify(adminUser2Workgroups), + }).as("adminUser2Workgroups"); + cy.visit("/editor"); cy.get('[data-cy="borehole-table"] thead .checkbox').click({ force: true }); cy.contains("button", "Bulk editing").click({ force: true }); @@ -83,16 +59,15 @@ describe("Test the borehole bulk edit feature.", () => { cy.wrap(el).scrollIntoView().click(); cy.get('.modal [role="option"]').eq(0).click({ force: true }); }); - changeWorkgroupRoleForUser(6, 1, "EDIT", "DISABLE"); }); it("fills all bulkedit fields and saves.", () => { - login("/editor"); - // create boreholes createBorehole({ "extended.original_name": "NINTIC" }).as("borehole_id_1"); createBorehole({ "extended.original_name": "LOMONE" }).as("borehole_id_2"); - cy.contains("a", "Refresh").click(); + + loginAsAdmin(); + cy.visit("/editor"); cy.wait("@borehole"); // select the boreholes for bulk edit diff --git a/src/client/cypress/e2e/editor/chronostratigraphy.cy.js b/src/client/cypress/e2e/editor/chronostratigraphy.cy.js index 67ac9c116..63f838e41 100644 --- a/src/client/cypress/e2e/editor/chronostratigraphy.cy.js +++ b/src/client/cypress/e2e/editor/chronostratigraphy.cy.js @@ -1,8 +1,8 @@ import { createBorehole, createStratigraphy, - adminUserAuth, - login, + loginAsAdmin, + bearerAuth, } from "../testHelpers"; describe("Tests for the chronostratigraphy editor.", () => { @@ -49,31 +49,34 @@ describe("Tests for the chronostratigraphy editor.", () => { ], }; Object.entries(layers).forEach(([key, value]) => { - value.forEach(layer => { - cy.request({ - method: "POST", - url: "/api/v2/" + key, - cache: "no-cache", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - }, - body: { - stratigraphyId: response.body.id, - ...layer, - }, - auth: adminUserAuth, - }).then(response => { - expect(response).to.have.property("status", 200); + cy.get("@id_token").then(token => { + value.forEach(layer => { + cy.request({ + method: "POST", + url: "/api/v2/" + key, + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + body: { + stratigraphyId: response.body.id, + ...layer, + }, + auth: bearerAuth(token), + }).then(response => { + expect(response).to.have.property("status", 200); + }); }); }); }); }); // open chronostratigraphy editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/stratigraphy/chronostratigraphy`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/stratigraphy/chronostratigraphy`); + }); cy.wait("@get-layers-by-profileId"); // start editing session diff --git a/src/client/cypress/e2e/editor/copyBorehole.cy.js b/src/client/cypress/e2e/editor/copyBorehole.cy.js index 9d6c315d2..d4323badb 100644 --- a/src/client/cypress/e2e/editor/copyBorehole.cy.js +++ b/src/client/cypress/e2e/editor/copyBorehole.cy.js @@ -1,10 +1,11 @@ -import { createBorehole, login } from "../testHelpers"; +import { createBorehole, loginAsAdmin } from "../testHelpers"; describe("Test copying of boreholes", () => { it("copies a borehole", () => { - login("/editor"); createBorehole({ "extended.original_name": "NINTIC" }).as("borehole_id_1"); + loginAsAdmin(); + cy.visit("/editor"); cy.get('[data-cy="borehole-table"] tbody') .children() .eq(1) diff --git a/src/client/cypress/e2e/editor/fieldMeasurement.cy.js b/src/client/cypress/e2e/editor/fieldMeasurement.cy.js index 2a298a6d5..d2b01995a 100644 --- a/src/client/cypress/e2e/editor/fieldMeasurement.cy.js +++ b/src/client/cypress/e2e/editor/fieldMeasurement.cy.js @@ -1,4 +1,8 @@ -import { createBorehole, createStratigraphy, login } from "../testHelpers"; +import { + createBorehole, + createStratigraphy, + loginAsAdmin, +} from "../testHelpers"; describe("Tests for the field measurement editor.", () => { beforeEach(function () { @@ -11,9 +15,10 @@ describe("Tests for the field measurement editor.", () => { }); // open field measurement editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/hydrogeology/fieldmeasurement`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/hydrogeology/fieldmeasurement`); + }); // start editing session cy.contains("a", "Start editing").click(); diff --git a/src/client/cypress/e2e/editor/groundwaterLevelMeasurement.cy.js b/src/client/cypress/e2e/editor/groundwaterLevelMeasurement.cy.js index c212ac3f6..d04619812 100644 --- a/src/client/cypress/e2e/editor/groundwaterLevelMeasurement.cy.js +++ b/src/client/cypress/e2e/editor/groundwaterLevelMeasurement.cy.js @@ -1,4 +1,8 @@ -import { createBorehole, createStratigraphy, login } from "../testHelpers"; +import { + createBorehole, + createStratigraphy, + loginAsAdmin, +} from "../testHelpers"; describe("Tests for the groundwater level measurement editor.", () => { beforeEach(function () { @@ -11,9 +15,10 @@ describe("Tests for the groundwater level measurement editor.", () => { }); // open groundwater level measurement editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/hydrogeology/groundwaterlevelmeasurement`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/hydrogeology/groundwaterlevelmeasurement`); + }); // start editing session cy.contains("a", "Start editing").click(); diff --git a/src/client/cypress/e2e/editor/hydrotest.cy.js b/src/client/cypress/e2e/editor/hydrotest.cy.js index c4f1a3f70..53a4c05b5 100644 --- a/src/client/cypress/e2e/editor/hydrotest.cy.js +++ b/src/client/cypress/e2e/editor/hydrotest.cy.js @@ -1,4 +1,8 @@ -import { createBorehole, createStratigraphy, login } from "../testHelpers"; +import { + createBorehole, + createStratigraphy, + loginAsAdmin, +} from "../testHelpers"; const openDropdown = dataCy => { cy.get(`[data-cy="${dataCy}"]`) @@ -34,9 +38,10 @@ describe("Tests for the hydrotest editor.", () => { }); // open hydrotest editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/hydrogeology/hydrotest`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/hydrogeology/hydrotest`); + }); // start editing session cy.contains("a", "Start editing").click(); diff --git a/src/client/cypress/e2e/editor/import.cy.js b/src/client/cypress/e2e/editor/import.cy.js index 60512de58..be0b42e00 100644 --- a/src/client/cypress/e2e/editor/import.cy.js +++ b/src/client/cypress/e2e/editor/import.cy.js @@ -1,8 +1,9 @@ -import { login, getImportFileFromFixtures } from "../../e2e/testHelpers"; +import { loginAsAdmin, getImportFileFromFixtures } from "../../e2e/testHelpers"; describe("Test for importing boreholes.", () => { it("Successfully imports multiple boreholes.", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("a", "Import").click(); // Select borehole csv file @@ -77,31 +78,35 @@ describe("Test for importing boreholes.", () => { }); it("Displays borehole validation errors.", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("a", "Import").click(); // Select borehole csv file - let boreholeFile = new DataTransfer(); getImportFileFromFixtures( "boreholes-missing-fields-and-duplicates.csv", null, - ).then(fileContent => { - const file = new File( - [fileContent], - "boreholes-missing-fields-and-duplicates.csv", - { - type: "text/csv", - }, - ); - boreholeFile.items.add(file); - }); - - cy.get('[data-cy="import-boreholeFile-input"]').within(() => { - cy.get("input[type=file]", { force: true }).then(input => { - input[0].files = boreholeFile.files; - input[0].dispatchEvent(new Event("change", { bubbles: true })); + ) + .then(fileContent => { + const file = new File( + [fileContent], + "boreholes-missing-fields-and-duplicates.csv", + { + type: "text/csv", + }, + ); + let boreholeFile = new DataTransfer(); + boreholeFile.items.add(file); + return boreholeFile; + }) + .then(boreholeFile => { + cy.get('[data-cy="import-boreholeFile-input"]').within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = boreholeFile.files; + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); + }); }); - }); cy.intercept("/api/v2/upload?workgroupId=1").as("borehole-upload"); @@ -123,45 +128,51 @@ describe("Test for importing boreholes.", () => { }); it("Displays lithology validation errors.", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("a", "Import").click(); // Select borehole csv file - let boreholeFile = new DataTransfer(); - getImportFileFromFixtures("boreholes-multiple-valid.csv", null).then( - fileContent => { + getImportFileFromFixtures("boreholes-multiple-valid.csv", null) + .then(fileContent => { const file = new File([fileContent], "boreholes-multiple-valid.csv", { type: "text/csv", }); + let boreholeFile = new DataTransfer(); boreholeFile.items.add(file); - }, - ); - - cy.get('[data-cy="import-boreholeFile-input"]').within(() => { - cy.get("input[type=file]", { force: true }).then(input => { - input[0].files = boreholeFile.files; - input[0].dispatchEvent(new Event("change", { bubbles: true })); + return boreholeFile; + }) + .then(boreholeFile => { + cy.get('[data-cy="import-boreholeFile-input"]').within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = boreholeFile.files; + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); + }); }); - }); // Select lithology csv file - let lithologyFile = new DataTransfer(); getImportFileFromFixtures( "lithology-single-not-valid.csv", null, "invalid-lithology", - ).then(fileContent => { - const file = new File([fileContent], "lithology-single-not-valid.csv", { - type: "text/csv", - }); - lithologyFile.items.add(file); - }); - cy.get('[data-cy="import-lithologyFile-input"]').within(() => { - cy.get("input[type=file]", { force: true }).then(input => { - input[0].files = lithologyFile.files; - input[0].dispatchEvent(new Event("change", { bubbles: true })); + ) + .then(fileContent => { + const file = new File([fileContent], "lithology-single-not-valid.csv", { + type: "text/csv", + }); + let lithologyFile = new DataTransfer(); + lithologyFile.items.add(file); + return lithologyFile; + }) + .then(lithologyFile => { + cy.get('[data-cy="import-lithologyFile-input"]').within(() => { + cy.get("input[type=file]", { force: true }).then(input => { + input[0].files = lithologyFile.files; + input[0].dispatchEvent(new Event("change", { bubbles: true })); + }); + }); }); - }); cy.intercept("/api/v2/upload?workgroupId=1").as("borehole-upload"); diff --git a/src/client/cypress/e2e/editor/instrumentation-v2.cy.js b/src/client/cypress/e2e/editor/instrumentation-v2.cy.js index f11444b9b..a57bac699 100644 --- a/src/client/cypress/e2e/editor/instrumentation-v2.cy.js +++ b/src/client/cypress/e2e/editor/instrumentation-v2.cy.js @@ -1,4 +1,4 @@ -import { loginAsAdmin, adminUserAuth, createBorehole } from "../testHelpers"; +import { loginAsAdmin, bearerAuth, createBorehole } from "../testHelpers"; const openDropdown = dataCy => { cy.get(`[data-cy="${dataCy}"]`) @@ -18,8 +18,8 @@ describe("Instrumentation crud tests", () => { createBorehole({ "extended.original_name": "INTEADAL" }) .as("borehole_id") .then(id => - cy - .request({ + cy.get("@id_token").then(token => { + cy.request({ method: "POST", url: "/api/v2/completion", cache: "no-cache", @@ -32,17 +32,18 @@ describe("Instrumentation crud tests", () => { isPrimary: true, kindId: 16000002, }, - auth: adminUserAuth, - }) - .then(response => { + auth: bearerAuth(token), + }).then(response => { expect(response).to.have.property("status", 200); - }), + }); + }), ); // open completion editor - cy.get("@borehole_id").then(id => - loginAsAdmin(`/editor/${id}/completion/v2`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`/editor/${id}/completion/v2`); + }); cy.contains("Tabs"); diff --git a/src/client/cypress/e2e/editor/instrumentation.cy.js b/src/client/cypress/e2e/editor/instrumentation.cy.js index 6fbc2c040..a9e768bdc 100644 --- a/src/client/cypress/e2e/editor/instrumentation.cy.js +++ b/src/client/cypress/e2e/editor/instrumentation.cy.js @@ -1,5 +1,5 @@ import { - loginAsViewer, + loginAsEditorInViewerMode, createBorehole, createAndEditBoreholeAsAdmin, } from "../testHelpers"; @@ -7,7 +7,8 @@ import { describe("Instrumentation tests", () => { it("Displays correct 'No Instumentation' message when logged in as viewer and no stratigraphies are defined", () => { createBorehole({ "extended.original_name": "A1_Borehole" }); - loginAsViewer("/editor"); + loginAsEditorInViewerMode(); + cy.visit("/editor"); // Select borehole A1_Borehole cy.get("tbody").children().first().click(); diff --git a/src/client/cypress/e2e/editor/lithostratigraphy.cy.js b/src/client/cypress/e2e/editor/lithostratigraphy.cy.js index f06acca71..5cd38ddf7 100644 --- a/src/client/cypress/e2e/editor/lithostratigraphy.cy.js +++ b/src/client/cypress/e2e/editor/lithostratigraphy.cy.js @@ -1,8 +1,8 @@ import { createBorehole, createStratigraphy, - adminUserAuth, - login, + bearerAuth, + loginAsAdmin, } from "../testHelpers"; describe("Tests for the lithostratigraphy editor.", () => { @@ -34,29 +34,34 @@ describe("Tests for the lithostratigraphy editor.", () => { toDepth: 43, }, ].forEach(layer => { - cy.request({ - method: "POST", - url: "/api/v2/layer", - cache: "no-cache", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - }, - body: { - stratigraphyId: response.body.id, - ...layer, - }, - auth: adminUserAuth, - }).then(response => { - expect(response).to.have.property("status", 200); - }); + cy.get("@id_token").then(token => + cy + .request({ + method: "POST", + url: "/api/v2/layer", + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + body: { + stratigraphyId: response.body.id, + ...layer, + }, + auth: bearerAuth(token), + }) + .then(response => { + expect(response).to.have.property("status", 200); + }), + ); }); }); // open lithostratigraphy editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/stratigraphy/lithostratigraphy`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/stratigraphy/lithostratigraphy`); + }); cy.wait("@get-layers-by-profileId"); // start editing session diff --git a/src/client/cypress/e2e/editor/waterIngress.cy.js b/src/client/cypress/e2e/editor/waterIngress.cy.js index 455f1be61..c2ee54dfa 100644 --- a/src/client/cypress/e2e/editor/waterIngress.cy.js +++ b/src/client/cypress/e2e/editor/waterIngress.cy.js @@ -1,4 +1,8 @@ -import { createBorehole, createStratigraphy, login } from "../testHelpers"; +import { + createBorehole, + createStratigraphy, + loginAsAdmin, +} from "../testHelpers"; describe("Tests for the wateringress editor.", () => { beforeEach(function () { @@ -11,9 +15,10 @@ describe("Tests for the wateringress editor.", () => { }); // open wateringress editor - cy.get("@borehole_id").then(id => - login(`editor/${id}/hydrogeology/wateringress`), - ); + cy.get("@borehole_id").then(id => { + loginAsAdmin(); + cy.visit(`editor/${id}/hydrogeology/wateringress`); + }); // start editing session cy.contains("a", "Start editing").click(); diff --git a/src/client/cypress/e2e/filter.cy.js b/src/client/cypress/e2e/filter.cy.js index 2e787dbdf..9514f5e33 100644 --- a/src/client/cypress/e2e/filter.cy.js +++ b/src/client/cypress/e2e/filter.cy.js @@ -1,13 +1,15 @@ -import { login, loginAsViewer } from "../e2e/testHelpers"; +import { loginAsAdmin, loginAsEditorInViewerMode } from "../e2e/testHelpers"; describe("Search filter tests", () => { it("has search filters", () => { - login(); + loginAsAdmin(); + cy.visit("/"); cy.contains("Search filters:"); }); it("shows the correct dropdowns", () => { - login(); + loginAsAdmin(); + cy.visit("/"); cy.contains("span", "Location").click(); cy.contains("Show all fields").children().eq(0).click(); let indentifierDropdown = cy.contains("label", "ID type").next(); @@ -40,7 +42,8 @@ describe("Search filter tests", () => { }); it("shows 'fiter by map' in editor on 'Large Map' appearance", () => { - loginAsViewer("/setting/editor"); + loginAsEditorInViewerMode(); + cy.visit("/setting/editor"); // Check if Editor mode settings are apparant cy.contains("Location filters"); @@ -67,7 +70,8 @@ describe("Search filter tests", () => { it("checks that the registration filter settings control the filter visibility.", () => { // precondition filters not visible - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("Registration").click(); cy.contains("Show all fields") .next() @@ -101,12 +105,13 @@ describe("Search filter tests", () => { }); it("filters boreholes by creator name", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("Registration").click(); cy.contains("Show all fields").children(".checkbox").click(); // input value - cy.contains("Created by").next().find("input").type("val_da%r"); + cy.contains("Created by").next().find("input").type("v_ U%r"); cy.wait("@edit_list"); // check content of table @@ -114,12 +119,13 @@ describe("Search filter tests", () => { .children() .should("have.length", 100) .each((el, index, list) => { - cy.wrap(el).contains("validator"); + cy.wrap(el).contains("v. user"); }); }); it("filters boreholes by original lithology", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("Lithology").click(); cy.contains("Show all fields").children(".checkbox").click(); @@ -134,7 +140,8 @@ describe("Search filter tests", () => { }); it("filters boreholes by creation date", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("Registration").click(); cy.contains("Show all fields").children(".checkbox").click(); @@ -172,7 +179,8 @@ describe("Search filter tests", () => { }); it("filters boreholes by workgroup in viewer", () => { - login(); + loginAsAdmin(); + cy.visit("/"); cy.contains("Workgroup").click(); cy.contains("Default").click(); cy.wait("@borehole"); diff --git a/src/client/cypress/e2e/hierarchicalDataFilter.cy.js b/src/client/cypress/e2e/hierarchicalDataFilter.cy.js index 8e979cc3d..4c40d3a1e 100644 --- a/src/client/cypress/e2e/hierarchicalDataFilter.cy.js +++ b/src/client/cypress/e2e/hierarchicalDataFilter.cy.js @@ -1,8 +1,9 @@ -import { login } from "./testHelpers"; +import { loginAsAdmin } from "./testHelpers"; describe("Hierachical data filter tests", () => { it("check visible filters", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("span", "Chronostratigraphy").click(); cy.get("Show all fields").should("not.exist"); cy.get('[data-cy="hierarchical-data-search"]').should("have.length", 7); @@ -12,7 +13,8 @@ describe("Hierachical data filter tests", () => { }); it("check sorting of filter values", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("span", "Chronostratigraphy").click(); let periodsDropdown = cy.contains("label", "Period").next(); periodsDropdown.click(); @@ -46,7 +48,8 @@ describe("Hierachical data filter tests", () => { "Burdigalian", "late Burdigalian", ]; - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("span", "Chronostratigraphy").click(); cy.get('[data-cy="hierarchical-data-search"]') .eq(filterValues.length - 1) diff --git a/src/client/cypress/e2e/srsFilter.cy.js b/src/client/cypress/e2e/srsFilter.cy.js index 2a577b50c..b446fca59 100644 --- a/src/client/cypress/e2e/srsFilter.cy.js +++ b/src/client/cypress/e2e/srsFilter.cy.js @@ -1,4 +1,4 @@ -import { newEditableBorehole, login } from "./testHelpers"; +import { newEditableBorehole, loginAsAdmin } from "./testHelpers"; describe("Tests for filtering data by reference system.", () => { function goToEditorLocationFilter() { @@ -16,7 +16,8 @@ describe("Tests for filtering data by reference system.", () => { } it("can set filters as viewer", () => { - login(); + loginAsAdmin(); + cy.visit("/"); goToViewerLocationFilter(); cy.contains("div", "Spatial reference system") @@ -44,7 +45,8 @@ describe("Tests for filtering data by reference system.", () => { }); it("can set filters as editor", () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); goToEditorLocationFilter(); cy.contains("div", "Spatial reference system") diff --git a/src/client/cypress/e2e/testHelpers.js b/src/client/cypress/e2e/testHelpers.js index 284b194f5..7f359d71e 100644 --- a/src/client/cypress/e2e/testHelpers.js +++ b/src/client/cypress/e2e/testHelpers.js @@ -2,10 +2,7 @@ import adminUser from "../fixtures/adminUser.json"; import editorUser from "../fixtures/editorUser.json"; import viewerUser from "../fixtures/viewerUser.json"; -export const adminUserAuth = { - user: "admin", - password: "swissforages", -}; +export const bearerAuth = token => ({ bearer: token }); export const interceptApiCalls = () => { // Api V1 @@ -73,43 +70,76 @@ export const interceptApiCalls = () => { }; /** - * Login into the application with the pre-filled user for the development environment. - * @param {string} visitUrl The url to visit after logging in. Default is the root path. + * Login into the application with the user for the development environment. */ -export const login = (visitUrl = "/") => { - cy.intercept("api/v1/content").as("content"); - cy.intercept("/api/v1/borehole/codes").as("code"); - cy.visit(visitUrl); - cy.wait("@content"); - cy.contains("button", "Login").click({ force: true }); - cy.wait("@code"); +export const login = user => { + cy.session( + ["login", user], + () => { + cy.intercept("http://localhost:4011/connect/token").as("token"); + cy.visit("/"); + cy.get('[data-cy="login-button"]').click({ force: true }); + cy.origin("http://localhost:4011", { args: { user } }, ({ user }) => { + cy.get("#Username").type(user); + cy.get("#Password").type("swissforages"); + cy.contains("button", "Login").click({ force: true }); + }); + cy.wait("@token") + .then(interception => interception.response.body.id_token) + .then(token => window.localStorage.setItem("id_token", token)); + }, + { + validate() { + cy.window() + .then(win => win.localStorage.getItem("id_token")) + .as("id_token"); + cy.get("@id_token").then(token => + cy.request({ + method: "POST", + url: "/api/v1/user", + body: { + action: "GET", + }, + auth: bearerAuth(token), + }), + ); + }, + cacheAcrossSpecs: true, + }, + ); }; /** * Login into the application as admin. - * @param {string} visitUrl The url to visit after logging in. Default is the root path. */ -export const loginAsAdmin = (visitUrl = "/") => { - cy.intercept("/api/v1/user", adminUser); - login(visitUrl); +export const loginAsAdmin = () => { + login("admin"); + cy.intercept("/api/v1/user", { + statusCode: 200, + body: JSON.stringify(adminUser), + }).as("stubAdminUser"); }; /** * Login into the application as editor. - * @param {string} visitUrl The url to visit after logging in. Default is the root path. */ -export const loginAsEditorInViewerMode = (visitUrl = "/") => { - cy.intercept("/api/v1/user", editorUser); - login(visitUrl); +export const loginAsEditor = () => { + login("editor"); + cy.intercept("/api/v1/user", { + statusCode: 200, + body: JSON.stringify(editorUser), + }).as("stubEditorUser"); }; /** * Login into the application as viewer. - * @param {string} visitUrl The url to visit after logging in. Default is the root path. */ -export const loginAsViewer = (visitUrl = "/") => { - cy.intercept("/api/v1/user", viewerUser); - login(visitUrl); +export const loginAsEditorInViewerMode = () => { + login("editor"); + cy.intercept("/api/v1/user", { + statusCode: 200, + body: JSON.stringify(viewerUser), + }).as("stubViewerUser"); }; export const newEditableBorehole = () => { @@ -120,7 +150,8 @@ export const newEditableBorehole = () => { }; export const newUneditableBorehole = () => { - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.contains("a", "New").click(); cy.contains("button", "Create").click(); const id = waitForCreation(); @@ -139,83 +170,93 @@ const waitForCreation = () => { }; export const createBorehole = values => { - return cy - .request({ - method: "POST", - url: "/api/v1/borehole/edit", - body: { - action: "CREATE", - id: 1, - }, - auth: adminUserAuth, - }) - .then(res => { - expect(res.body).to.have.property("success", true); - let boreholeId = res.body.id; - let fields = Object.entries(values).map(([key, value]) => [key, value]); - if (fields.length > 0) { - cy.request({ - method: "POST", - url: "/api/v1/borehole/edit", - body: { - action: "MULTIPATCH", - fields: fields, - ids: [boreholeId], - }, - auth: adminUserAuth, - }).then(res => expect(res.body).to.have.property("success", true)); - } - return cy.wrap(boreholeId); - }); + loginAsAdmin(); + return cy.get("@id_token").then(token => + cy + .request({ + method: "POST", + url: "/api/v1/borehole/edit", + body: { + action: "CREATE", + id: 1, + }, + auth: bearerAuth(token), + }) + .then(res => { + expect(res.body).to.have.property("success", true); + let boreholeId = res.body.id; + let fields = Object.entries(values).map(([key, value]) => [key, value]); + if (fields.length > 0) { + cy.request({ + method: "POST", + url: "/api/v1/borehole/edit", + body: { + action: "MULTIPATCH", + fields: fields, + ids: [boreholeId], + }, + auth: bearerAuth(token), + }).then(res => expect(res.body).to.have.property("success", true)); + } + return cy.wrap(boreholeId); + }), + ); }; export const createAndEditBoreholeAsAdmin = values => { - return createBorehole(values).then(value => loginAsAdmin(`/editor/${value}`)); + return createBorehole(values).then(value => { + loginAsAdmin(); + cy.visit(`/editor/${value}`); + }); }; export const deleteBorehole = id => { - cy.request({ - method: "POST", - url: "/api/v1/borehole/edit", - body: { - action: "DELETE", - id: id, - }, - auth: adminUserAuth, - }) - .its("body.success") - .should("eq", true); + loginAsAdmin(); + cy.get("@id_token").then(token => { + cy.request({ + method: "POST", + url: "/api/v1/borehole/edit", + body: { + action: "DELETE", + id: id, + }, + auth: bearerAuth(token), + }) + .its("body.success") + .should("eq", true); + }); }; export const loginAndResetState = () => { - // Reset boreholes - cy.request({ - method: "POST", - url: "/api/v1/borehole/edit", - body: { - action: "IDS", - }, - auth: adminUserAuth, - }).then(response => { - response.body.data - .filter(id => id > 1009999) // max id in seed data. - .forEach(id => { - deleteBorehole(id); - }); - }); + loginAsAdmin(); + cy.get("@id_token").then(token => { + // Reset boreholes + cy.request({ + method: "POST", + url: "/api/v1/borehole/edit", + body: { + action: "IDS", + }, + auth: bearerAuth(token), + }).then(response => { + response.body.data + .filter(id => id > 1009999) // max id in seed data. + .forEach(id => { + deleteBorehole(id); + }); + }); - // Reset user settings (i.e. table ordering) - cy.request({ - method: "POST", - url: "/api/v2/user/resetAllSettings", - cache: "no-cache", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${btoa( - `${adminUserAuth.user}:${adminUserAuth.password}`, - )}`, - }, + // Reset user settings (i.e. table ordering) + cy.request({ + method: "POST", + url: "/api/v2/user/resetAllSettings", + cache: "no-cache", + credentials: "same-origin", + auth: bearerAuth(token), + headers: { + "Content-Type": "application/json", + }, + }); }); }; @@ -294,15 +335,17 @@ export const getImportFileFromFixtures = (fileName, encoding, dataSet) => { }; export const createStratigraphy = (boreholeId, kindId) => { - return cy.request({ - method: "POST", - url: "/api/v2/stratigraphy", - body: { - boreholeId: boreholeId, - kindId: kindId, - }, - cache: "no-cache", - credentials: "same-origin", - auth: adminUserAuth, + cy.get("@id_token").then(token => { + return cy.request({ + method: "POST", + url: "/api/v2/stratigraphy", + body: { + boreholeId: boreholeId, + kindId: kindId, + }, + cache: "no-cache", + credentials: "same-origin", + auth: bearerAuth(token), + }); }); }; diff --git a/src/client/cypress/e2e/viewer/displayLayer.cy.js b/src/client/cypress/e2e/viewer/displayLayer.cy.js index 8216870b2..7ac161d75 100644 --- a/src/client/cypress/e2e/viewer/displayLayer.cy.js +++ b/src/client/cypress/e2e/viewer/displayLayer.cy.js @@ -1,6 +1,6 @@ import { newEditableBorehole, - login, + loginAsAdmin, createBorehole, setValueOfInputElement, } from "../testHelpers"; @@ -8,7 +8,8 @@ import { describe("Test for the borehole form.", () => { it("Adds complete layer and displays it in viewer mode, checks if fields can be optionally hidden.", () => { // Assert map number of boreholes - login("/editor"); + loginAsAdmin(); + cy.visit("/editor"); cy.get("div[id=map]").should("be.visible"); cy.get("tbody").children().should("have.length", 100); @@ -155,7 +156,8 @@ describe("Test for the borehole form.", () => { it("Checks if null values are displayed as dash.", () => { createBorehole({ "extended.original_name": "A1_Borehole" }); - login(); + loginAsAdmin(); + cy.visit("/"); // Select borehole A1_Borehole cy.get("tbody").children().contains("td", "A1_Borehole").click(); diff --git a/src/client/cypress/fixtures/adminUser.json b/src/client/cypress/fixtures/adminUser.json index c11b12d72..1115df269 100644 --- a/src/client/cypress/fixtures/adminUser.json +++ b/src/client/cypress/fixtures/adminUser.json @@ -2,10 +2,11 @@ "success": true, "data": { "admin": true, - "name": "admin user", + "name": "Admin User", "roles": ["VIEW", "EDIT", "CONTROL", "VALID", "PUBLIC"], "terms": true, - "username": "admin", + "id": 1, + "username": "A. User", "viewer": true, "workgroups": [ { diff --git a/src/client/cypress/fixtures/editorUser.json b/src/client/cypress/fixtures/editorUser.json index ec5ea2c6c..1558c49bd 100644 --- a/src/client/cypress/fixtures/editorUser.json +++ b/src/client/cypress/fixtures/editorUser.json @@ -2,10 +2,11 @@ "success": true, "data": { "admin": false, - "name": "editor user", + "name": "Editor User", "roles": ["EDIT"], "terms": true, - "username": "editor", + "id": 2, + "username": "E. User", "viewer": true, "workgroups": [ { diff --git a/src/client/cypress/fixtures/viewerUser.json b/src/client/cypress/fixtures/viewerUser.json index c6c4b18df..0c13efce3 100644 --- a/src/client/cypress/fixtures/viewerUser.json +++ b/src/client/cypress/fixtures/viewerUser.json @@ -2,10 +2,10 @@ "success": true, "data": { "admin": false, - "name": "viewer user", + "name": "Editor User", "roles": ["VIEW"], "terms": true, - "username": "viewer", + "username": "E. User", "viewer": true, "workgroups": [ { diff --git a/src/client/package-lock.json b/src/client/package-lock.json index 183513fb6..5449e283b 100644 --- a/src/client/package-lock.json +++ b/src/client/package-lock.json @@ -15,6 +15,7 @@ "@react-hook/resize-observer": "^1.2.6", "axios": "^1.6.2", "date-fns": "^2.30.0", + "dotenv": "^16.3.1", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "http-proxy-middleware": "^2.0.6", @@ -25,6 +26,7 @@ "lodash": "^4.17.21", "markdown-to-jsx": "^6.11.4", "moment": "^2.29.4", + "oidc-client-ts": "2.4.0", "ol": "^8.2.0", "proj4": "^2.9.2", "prop-types": "^15.8.1", @@ -37,6 +39,7 @@ "react-hook-form": "^7.48.2", "react-i18next": "^13.5.0", "react-number-format": "^5.3.1", + "react-oidc-context": "2.3.1", "react-query": "^3.39.3", "react-redux": "^7.2.9", "react-router-dom": "^5.3.4", @@ -6649,6 +6652,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -7737,11 +7745,14 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dotenv-expand": { @@ -11990,6 +12001,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/keyboard-key": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", @@ -13051,6 +13067,18 @@ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "node_modules/oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "dependencies": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/ol": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ol/-/ol-8.2.0.tgz", @@ -15245,6 +15273,18 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-oidc-context": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.3.1.tgz", + "integrity": "sha512-WdhmEU6odNzMk9pvOScxUkf6/1aduiI/nQryr7+iCl2VDnYLASDTIV/zy58KuK4VXG3fBaRKukc/mRpMjF9a3Q==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "oidc-client-ts": "^2.2.1", + "react": ">=16.8.0" + } + }, "node_modules/react-onclickoutside": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", @@ -15460,6 +15500,14 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/react-scripts/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", diff --git a/src/client/package.json b/src/client/package.json index 780f468b1..be44601b0 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -29,6 +29,7 @@ "@react-hook/resize-observer": "^1.2.6", "axios": "^1.6.2", "date-fns": "^2.30.0", + "dotenv": "^16.3.1", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "http-proxy-middleware": "^2.0.6", @@ -39,6 +40,7 @@ "lodash": "^4.17.21", "markdown-to-jsx": "^6.11.4", "moment": "^2.29.4", + "oidc-client-ts": "2.4.0", "ol": "^8.2.0", "proj4": "^2.9.2", "prop-types": "^15.8.1", @@ -51,6 +53,7 @@ "react-hook-form": "^7.48.2", "react-i18next": "^13.5.0", "react-number-format": "^5.3.1", + "react-oidc-context": "2.3.1", "react-query": "^3.39.3", "react-redux": "^7.2.9", "react-router-dom": "^5.3.4", @@ -73,6 +76,7 @@ "prettier": "^3.1.0" }, "scripts": { + "prestart": "node -e \"require('\\.\\/react-environment.js').writeEnvJs()\"", "start": "react-scripts start", "build": "react-scripts build", "test": "cypress run", diff --git a/src/client/public/index.html b/src/client/public/index.html index d9c4cdc5e..225d61bef 100644 --- a/src/client/public/index.html +++ b/src/client/public/index.html @@ -9,6 +9,7 @@ +