aboutsummaryrefslogtreecommitdiffstats
path: root/code
diff options
context:
space:
mode:
Diffstat (limited to 'code')
-rw-r--r--code/api/.build.yaml23
-rw-r--r--code/api/.dockerignore10
-rw-r--r--code/api/.version1
-rw-r--r--code/api/.version-dev1
-rw-r--r--code/api/CHANGELOG.md123
-rw-r--r--code/api/Dockerfile16
-rwxr-xr-xcode/api/build_and_push.sh89
-rw-r--r--code/api/cliff.toml62
-rw-r--r--code/api/sql/quartz-create.sql156
-rw-r--r--code/api/sql/quartz-drop.sql23
-rw-r--r--code/api/src/Endpoints/EndpointBase.cs54
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs64
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs32
-rw-r--r--code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs17
-rw-r--r--code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs26
-rw-r--r--code/api/src/Endpoints/Internal/Account/LoginRoute.cs37
-rw-r--r--code/api/src/Endpoints/Internal/Account/LogoutRoute.cs22
-rw-r--r--code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs59
-rw-r--r--code/api/src/Endpoints/Internal/Account/_calls.http9
-rw-r--r--code/api/src/Endpoints/Internal/INT_EndpointBase.cs9
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs40
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs36
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs26
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http22
-rw-r--r--code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs64
-rw-r--r--code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs10
-rw-r--r--code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs17
-rw-r--r--code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs15
-rw-r--r--code/api/src/Endpoints/Internal/Root/ValidateRoute.cs39
-rw-r--r--code/api/src/Endpoints/Internal/RouteBaseAsync.cs73
-rw-r--r--code/api/src/Endpoints/Internal/RouteBaseSync.cs53
-rw-r--r--code/api/src/Endpoints/V1/ApiSpecV1.cs18
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs58
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs31
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs36
-rw-r--r--code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs66
-rw-r--r--code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs83
-rw-r--r--code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs40
-rw-r--r--code/api/src/Endpoints/V1/Projects/_calls.http12
-rw-r--r--code/api/src/Endpoints/V1/RouteBaseAsync.cs73
-rw-r--r--code/api/src/Endpoints/V1/RouteBaseSync.cs53
-rw-r--r--code/api/src/Endpoints/V1/V1_EndpointBase.cs29
-rw-r--r--code/api/src/IOL.GreatOffice.Api.csproj71
-rw-r--r--code/api/src/Jobs/AccessTokenCleanupJob.cs20
-rw-r--r--code/api/src/Jobs/JobRegister.cs23
-rw-r--r--code/api/src/Jobs/VaultTokenRenewalJob.cs24
-rw-r--r--code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs238
-rw-r--r--code/api/src/Migrations/20210517202115_InitialMigration.cs162
-rw-r--r--code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs229
-rw-r--r--code/api/src/Migrations/20210522165932_RenameNoteToDescription.cs34
-rw-r--r--code/api/src/Migrations/20211002113037_V6Migration.Designer.cs233
-rw-r--r--code/api/src/Migrations/20211002113037_V6Migration.cs130
-rw-r--r--code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs270
-rw-r--r--code/api/src/Migrations/20220225143559_GithubUserMappings.cs43
-rw-r--r--code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs270
-rw-r--r--code/api/src/Migrations/20220319135910_RenameCreated.cs65
-rw-r--r--code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs290
-rw-r--r--code/api/src/Migrations/20220319144958_ModifiedAt.cs66
-rw-r--r--code/api/src/Migrations/20220319203018_UserBase.Designer.cs322
-rw-r--r--code/api/src/Migrations/20220319203018_UserBase.cs140
-rw-r--r--code/api/src/Migrations/20220320115601_Update1.Designer.cs342
-rw-r--r--code/api/src/Migrations/20220320115601_Update1.cs139
-rw-r--r--code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs344
-rw-r--r--code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs57
-rw-r--r--code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs401
-rw-r--r--code/api/src/Migrations/20220529190359_ApiAccessTokens.cs48
-rw-r--r--code/api/src/Migrations/20220530174741_Tenants.Designer.cs710
-rw-r--r--code/api/src/Migrations/20220530174741_Tenants.cs481
-rw-r--r--code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs686
-rw-r--r--code/api/src/Migrations/20220530175322_RemoveUnusedNavs.cs78
-rw-r--r--code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs656
-rw-r--r--code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.cs649
-rw-r--r--code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs510
-rw-r--r--code/api/src/Migrations/20220606232346_FleshOutNewModules.cs630
-rw-r--r--code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs533
-rw-r--r--code/api/src/Migrations/20220616170311_DataProtectionKeys.cs33
-rw-r--r--code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs496
-rw-r--r--code/api/src/Migrations/20220819203816_RemoveGithubUsers.cs43
-rw-r--r--code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs1074
-rw-r--r--code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs304
-rw-r--r--code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs1126
-rw-r--r--code/api/src/Migrations/20221030081459_DeletedAt.cs146
-rw-r--r--code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs1126
-rw-r--r--code/api/src/Migrations/20221030084716_MinorChanges.cs105
-rw-r--r--code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs1148
-rw-r--r--code/api/src/Migrations/20221030090557_MoreMinorChanges.cs78
-rw-r--r--code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.Designer.cs1863
-rw-r--r--code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.cs1092
-rw-r--r--code/api/src/Migrations/20221114034213_RemoveTimeTracker.Designer.cs1589
-rw-r--r--code/api/src/Migrations/20221114034213_RemoveTimeTracker.cs177
-rw-r--r--code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.Designer.cs1589
-rw-r--r--code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.cs78
-rw-r--r--code/api/src/Migrations/20221209041908_TenantSlug.Designer.cs1593
-rw-r--r--code/api/src/Migrations/20221209041908_TenantSlug.cs28
-rw-r--r--code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs1618
-rw-r--r--code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs46
-rw-r--r--code/api/src/Migrations/20221214143556_AddUserDeletedBy.Designer.cs1622
-rw-r--r--code/api/src/Migrations/20221214143556_AddUserDeletedBy.cs29
-rw-r--r--code/api/src/Migrations/AppDbContextModelSnapshot.cs1619
-rw-r--r--code/api/src/Models/Database/Api/ApiAccessToken.cs12
-rw-r--r--code/api/src/Models/Database/Base.cs22
-rw-r--r--code/api/src/Models/Database/BaseWithOwner.cs40
-rw-r--r--code/api/src/Models/Database/Customer/Customer.cs29
-rw-r--r--code/api/src/Models/Database/Customer/CustomerContact.cs12
-rw-r--r--code/api/src/Models/Database/Customer/CustomerEvent.cs8
-rw-r--r--code/api/src/Models/Database/Customer/CustomerGroup.cs7
-rw-r--r--code/api/src/Models/Database/Customer/CustomerGroupMembership.cs7
-rw-r--r--code/api/src/Models/Database/Internal/PasswordResetRequest.cs23
-rw-r--r--code/api/src/Models/Database/Internal/Tenant.cs12
-rw-r--r--code/api/src/Models/Database/Internal/User.cs44
-rw-r--r--code/api/src/Models/Database/MainAppDatabase.cs107
-rw-r--r--code/api/src/Models/Database/Project/Project.cs15
-rw-r--r--code/api/src/Models/Database/Project/ProjectLabel.cs8
-rw-r--r--code/api/src/Models/Database/Project/ProjectMember.cs8
-rw-r--r--code/api/src/Models/Database/Queues/ValidationEmail.cs8
-rw-r--r--code/api/src/Models/Database/Todo/Todo.cs17
-rw-r--r--code/api/src/Models/Database/Todo/TodoCollection.cs11
-rw-r--r--code/api/src/Models/Database/Todo/TodoCollectionAccessControl.cs12
-rw-r--r--code/api/src/Models/Database/Todo/TodoComment.cs8
-rw-r--r--code/api/src/Models/Database/Todo/TodoLabel.cs8
-rw-r--r--code/api/src/Models/Enums/FulfillPasswordResetRequestResult.cs8
-rw-r--r--code/api/src/Models/Enums/PasswordResetRequestStatus.cs6
-rw-r--r--code/api/src/Models/Enums/ProjectRole.cs9
-rw-r--r--code/api/src/Models/Enums/TodoClosingStatement.cs13
-rw-r--r--code/api/src/Models/Enums/TodoVisibility.cs10
-rw-r--r--code/api/src/Models/Misc/ApiSpecDocument.cs9
-rw-r--r--code/api/src/Models/Misc/AppConfiguration.cs118
-rw-r--r--code/api/src/Models/Misc/AppPath.cs23
-rw-r--r--code/api/src/Models/Misc/KnownProblemModel.cs26
-rw-r--r--code/api/src/Models/Misc/LoggedInUserModel.cs8
-rw-r--r--code/api/src/Models/Misc/RequestTimeZoneInfo.cs8
-rw-r--r--code/api/src/Models/Static/AppClaims.cs7
-rw-r--r--code/api/src/Models/Static/AppConstants.cs12
-rw-r--r--code/api/src/Models/Static/AppCookies.cs7
-rw-r--r--code/api/src/Models/Static/AppDateTime.cs16
-rw-r--r--code/api/src/Models/Static/AppEnvironmentVariables.cs21
-rw-r--r--code/api/src/Models/Static/AppHeaders.cs7
-rw-r--r--code/api/src/Models/Static/AppPaths.cs17
-rw-r--r--code/api/src/Models/Static/JsonSettings.cs11
-rw-r--r--code/api/src/Program.cs249
-rw-r--r--code/api/src/Properties/launchSettings.json14
-rw-r--r--code/api/src/Resources/SharedResources.cs4
-rw-r--r--code/api/src/Resources/SharedResources.en.Designer.cs84
-rw-r--r--code/api/src/Resources/SharedResources.en.resx45
-rw-r--r--code/api/src/Resources/SharedResources.nb.Designer.cs120
-rw-r--r--code/api/src/Resources/SharedResources.nb.resx101
-rw-r--r--code/api/src/Resources/SharedResources.resx33
-rw-r--r--code/api/src/Services/EmailValidationService.cs67
-rw-r--r--code/api/src/Services/MailService.cs132
-rw-r--r--code/api/src/Services/PasswordResetService.cs101
-rw-r--r--code/api/src/Services/TenantService.cs61
-rw-r--r--code/api/src/Services/UserService.cs54
-rw-r--r--code/api/src/Services/VaultService.cs237
-rw-r--r--code/api/src/Utilities/BasicAuthenticationAttribute.cs39
-rw-r--r--code/api/src/Utilities/BasicAuthenticationHandler.cs79
-rw-r--r--code/api/src/Utilities/ConfigurationExtensions.cs85
-rw-r--r--code/api/src/Utilities/DateTimeExtensions.cs8
-rw-r--r--code/api/src/Utilities/PaginationOperationFilter.cs39
-rw-r--r--code/api/src/Utilities/QuartzJsonSerializer.cs16
-rw-r--r--code/api/src/Utilities/QueryableExtensions.cs12
-rw-r--r--code/api/src/Utilities/SwaggerDefaultValues.cs58
-rw-r--r--code/api/src/Utilities/SwaggerGenOptionsExtensions.cs43
-rw-r--r--code/api/src/wwwroot/version.txt1
-rw-r--r--code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs23
-rw-r--r--code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs6
-rw-r--r--code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs48
-rw-r--r--code/api/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj21
-rw-r--r--code/app/.dockerignore7
-rw-r--r--code/app/.env-example4
-rw-r--r--code/app/.gitignore8
-rw-r--r--code/app/.npmrc1
-rw-r--r--code/app/.typesafe-i18n.json5
-rw-r--r--code/app/.version1
-rw-r--r--code/app/.version-dev1
-rw-r--r--code/app/Dockerfile13
-rwxr-xr-xcode/app/build_and_push.sh89
-rw-r--r--code/app/package.json50
-rw-r--r--code/app/playwright.config.ts10
-rw-r--r--code/app/pnpm-lock.yaml4389
-rw-r--r--code/app/postcss.config.cjs13
-rw-r--r--code/app/src/actions/pwKey.ts7
-rw-r--r--code/app/src/app.d.ts9
-rw-r--r--code/app/src/app.html14
-rw-r--r--code/app/src/app.pcss34
-rw-r--r--code/app/src/components/alert.svelte268
-rw-r--r--code/app/src/components/badge.svelte76
-rw-r--r--code/app/src/components/button.svelte116
-rw-r--r--code/app/src/components/checkbox.svelte29
-rw-r--r--code/app/src/components/combobox.svelte451
-rw-r--r--code/app/src/components/icons/adjustments.svelte14
-rw-r--r--code/app/src/components/icons/bars-3-center-left.svelte15
-rw-r--r--code/app/src/components/icons/calendar.svelte14
-rw-r--r--code/app/src/components/icons/check-circle.svelte13
-rw-r--r--code/app/src/components/icons/chevron-down.svelte7
-rw-r--r--code/app/src/components/icons/chevron-up-down.svelte13
-rw-r--r--code/app/src/components/icons/chevron-up.svelte7
-rw-r--r--code/app/src/components/icons/database.svelte14
-rw-r--r--code/app/src/components/icons/exclamation-circle.svelte13
-rw-r--r--code/app/src/components/icons/exclamation-triangle.svelte13
-rw-r--r--code/app/src/components/icons/folder-open.svelte14
-rw-r--r--code/app/src/components/icons/funnel.svelte7
-rw-r--r--code/app/src/components/icons/home.svelte14
-rw-r--r--code/app/src/components/icons/index.ts47
-rw-r--r--code/app/src/components/icons/information-circle.svelte13
-rw-r--r--code/app/src/components/icons/magnifying-glass.svelte13
-rw-r--r--code/app/src/components/icons/megaphone.svelte14
-rw-r--r--code/app/src/components/icons/menu.svelte14
-rw-r--r--code/app/src/components/icons/queue-list.svelte14
-rw-r--r--code/app/src/components/icons/spinner.svelte20
-rw-r--r--code/app/src/components/icons/x-circle.svelte13
-rw-r--r--code/app/src/components/icons/x-mark.svelte11
-rw-r--r--code/app/src/components/icons/x.svelte14
-rw-r--r--code/app/src/components/index.ts25
-rw-r--r--code/app/src/components/input.svelte112
-rw-r--r--code/app/src/components/locale-switcher.svelte56
-rw-r--r--code/app/src/components/notification.svelte119
-rw-r--r--code/app/src/components/project-status-badge.svelte25
-rw-r--r--code/app/src/components/switch.svelte125
-rw-r--r--code/app/src/components/textarea.svelte81
-rw-r--r--code/app/src/configuration/index.ts64
-rw-r--r--code/app/src/global.d.ts11
-rw-r--r--code/app/src/hooks.server.ts49
-rw-r--r--code/app/src/i18n/en/app/index.ts7
-rw-r--r--code/app/src/i18n/en/index.ts63
-rw-r--r--code/app/src/i18n/formatters.ts13
-rw-r--r--code/app/src/i18n/i18n-svelte.ts12
-rw-r--r--code/app/src/i18n/i18n-types.ts461
-rw-r--r--code/app/src/i18n/i18n-util.async.ts42
-rw-r--r--code/app/src/i18n/i18n-util.sync.ts35
-rw-r--r--code/app/src/i18n/i18n-util.ts45
-rw-r--r--code/app/src/i18n/nb/app/index.ts7
-rw-r--r--code/app/src/i18n/nb/index.ts51
-rw-r--r--code/app/src/models/base/Customer.ts21
-rw-r--r--code/app/src/models/base/CustomerContact.ts8
-rw-r--r--code/app/src/models/base/CustomerEvent.ts6
-rw-r--r--code/app/src/models/base/SessionData.ts5
-rw-r--r--code/app/src/models/base/Tenant.ts8
-rw-r--r--code/app/src/models/base/User.ts13
-rw-r--r--code/app/src/models/base/UserRole.ts5
-rw-r--r--code/app/src/models/internal/FormError.ts24
-rw-r--r--code/app/src/models/internal/IForm.ts15
-rw-r--r--code/app/src/models/internal/KnownProblem.ts10
-rw-r--r--code/app/src/models/projects/Project.ts13
-rw-r--r--code/app/src/models/projects/ProjectLabel.ts5
-rw-r--r--code/app/src/models/projects/ProjectMember.ts10
-rw-r--r--code/app/src/models/projects/ProjectMeta.ts7
-rw-r--r--code/app/src/models/projects/ProjectRole.ts7
-rw-r--r--code/app/src/models/projects/ProjectStatus.ts5
-rw-r--r--code/app/src/models/work/WorkCategory.ts5
-rw-r--r--code/app/src/models/work/WorkEntry.ts13
-rw-r--r--code/app/src/models/work/WorkEntryQueryResponse.ts27
-rw-r--r--code/app/src/models/work/WorkLabel.ts5
-rw-r--r--code/app/src/models/work/WorkQuery.ts17
-rw-r--r--code/app/src/routes/(api)/delete-cookie/+server.ts8
-rw-r--r--code/app/src/routes/(main)/(app)/+layout.svelte379
-rw-r--r--code/app/src/routes/(main)/(app)/home/+page.svelte1
-rw-r--r--code/app/src/routes/(main)/(app)/org/+page.svelte4
-rw-r--r--code/app/src/routes/(main)/(app)/profile/+page.svelte4
-rw-r--r--code/app/src/routes/(main)/(app)/projects/+page.svelte118
-rw-r--r--code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte5
-rw-r--r--code/app/src/routes/(main)/(app)/projects/create/+page.svelte59
-rw-r--r--code/app/src/routes/(main)/(app)/settings/+page.svelte205
-rw-r--r--code/app/src/routes/(main)/(app)/tickets/+page.svelte4
-rw-r--r--code/app/src/routes/(main)/(app)/todo/+page.svelte4
-rw-r--r--code/app/src/routes/(main)/(app)/wiki/+page.svelte4
-rw-r--r--code/app/src/routes/(main)/(public)/+layout.svelte18
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.svelte26
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.ts9
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.svelte81
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte82
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.svelte155
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/index.spec.js12
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/index.ts20
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.svelte106
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.ts11
-rw-r--r--code/app/src/routes/(main)/+layout.server.ts45
-rw-r--r--code/app/src/routes/(main)/+layout.svelte31
-rw-r--r--code/app/src/routes/(main)/+layout.ts10
-rw-r--r--code/app/src/routes/(main)/+page.svelte1
-rw-r--r--code/app/src/routes/book/+layout.svelte46
-rw-r--r--code/app/src/routes/book/+layout.ts3
-rw-r--r--code/app/src/routes/book/+page.svelte1
-rw-r--r--code/app/src/routes/book/alerts/+page.svelte70
-rw-r--r--code/app/src/routes/book/badges/+page.svelte19
-rw-r--r--code/app/src/routes/book/buttons/+page.svelte23
-rw-r--r--code/app/src/routes/book/inputs/+page.svelte75
-rw-r--r--code/app/src/routes/book/notifications/+page.svelte50
-rw-r--r--code/app/src/routes/book/toggles/+page.svelte27
-rw-r--r--code/app/src/services/abstractions/IAccountService.ts54
-rw-r--r--code/app/src/services/abstractions/IApiTokenService.ts34
-rw-r--r--code/app/src/services/abstractions/IPasswordResetService.ts21
-rw-r--r--code/app/src/services/abstractions/ISettingsService.ts3
-rw-r--r--code/app/src/services/account-service.ts124
-rw-r--r--code/app/src/services/api-tokens-service.ts22
-rw-r--r--code/app/src/services/password-reset-service.ts48
-rw-r--r--code/app/src/services/settings-service.ts10
-rw-r--r--code/app/src/utilities/_fetch.ts94
-rw-r--r--code/app/src/utilities/cache.ts38
-rw-r--r--code/app/src/utilities/colors.ts47
-rw-r--r--code/app/src/utilities/crypto-helpers.ts48
-rw-r--r--code/app/src/utilities/dom-helpers.ts105
-rw-r--r--code/app/src/utilities/global-state.ts22
-rw-r--r--code/app/src/utilities/logger.ts118
-rw-r--r--code/app/src/utilities/misc-helpers.ts77
-rw-r--r--code/app/src/utilities/persistent-store.ts111
-rw-r--r--code/app/src/utilities/storage-helpers.ts26
-rw-r--r--code/app/src/utilities/testing-helpers.ts7
-rw-r--r--code/app/src/utilities/validators.ts34
-rw-r--r--code/app/static/favicon.icobin0 -> 1406 bytes
-rw-r--r--code/app/static/version.txt1
-rw-r--r--code/app/svelte.config.js27
-rw-r--r--code/app/tailwind.config.cjs141
-rw-r--r--code/app/tsconfig.json10
-rw-r--r--code/app/vite.config.js16
-rw-r--r--code/frontpage/.gitignore5
-rw-r--r--code/frontpage/LICENSE22
-rw-r--r--code/frontpage/archetypes/blog.md15
-rw-r--r--code/frontpage/archetypes/default.md8
-rw-r--r--code/frontpage/archetypes/docs.md15
-rw-r--r--code/frontpage/archetypes/docs/_index.md10
-rw-r--r--code/frontpage/archetypes/docs/lorem/_index.md10
-rw-r--r--code/frontpage/archetypes/docs/lorem/ipsum/index.md16
-rw-r--r--code/frontpage/assets/fonts/.gitkeep0
-rw-r--r--code/frontpage/assets/images/default-image.pngbin0 -> 4694 bytes
-rw-r--r--code/frontpage/assets/js/alert-init.js5
-rw-r--r--code/frontpage/assets/js/alert.js20
-rw-r--r--code/frontpage/assets/js/app.js0
-rw-r--r--code/frontpage/assets/js/bootstrap.js2
-rw-r--r--code/frontpage/assets/js/clipboard.js37
-rw-r--r--code/frontpage/assets/js/darkmode-init.js21
-rw-r--r--code/frontpage/assets/js/darkmode.js38
-rw-r--r--code/frontpage/assets/js/highlight.js26
-rw-r--r--code/frontpage/assets/js/index.js179
-rw-r--r--code/frontpage/assets/js/instant.page.js1
-rw-r--r--code/frontpage/assets/js/katex.js10
-rw-r--r--code/frontpage/assets/js/lazysizes.js1
-rw-r--r--code/frontpage/assets/js/mermaid.js11
-rw-r--r--code/frontpage/assets/js/scroll-lock.js14
-rw-r--r--code/frontpage/assets/js/to-top.js20
-rw-r--r--code/frontpage/assets/js/vendor/.gitkeep0
-rw-r--r--code/frontpage/assets/scss/app.scss31
-rw-r--r--code/frontpage/assets/scss/common/_dark.scss582
-rw-r--r--code/frontpage/assets/scss/common/_global.scss288
-rw-r--r--code/frontpage/assets/scss/common/_variables.scss190
-rw-r--r--code/frontpage/assets/scss/components/_alerts.scss164
-rw-r--r--code/frontpage/assets/scss/components/_buttons.scss255
-rw-r--r--code/frontpage/assets/scss/components/_code.scss66
-rw-r--r--code/frontpage/assets/scss/components/_comments.scss30
-rw-r--r--code/frontpage/assets/scss/components/_details.scss77
-rw-r--r--code/frontpage/assets/scss/components/_forms.scss19
-rw-r--r--code/frontpage/assets/scss/components/_images.scss62
-rw-r--r--code/frontpage/assets/scss/components/_mermaid.scss8
-rw-r--r--code/frontpage/assets/scss/components/_search.scss91
-rw-r--r--code/frontpage/assets/scss/components/_syntax.scss62
-rw-r--r--code/frontpage/assets/scss/components/_tables.scss5
-rw-r--r--code/frontpage/assets/scss/layouts/_footer.scss20
-rw-r--r--code/frontpage/assets/scss/layouts/_header.scss493
-rw-r--r--code/frontpage/assets/scss/layouts/_pages.scss64
-rw-r--r--code/frontpage/assets/scss/layouts/_posts.scss57
-rw-r--r--code/frontpage/assets/scss/layouts/_sidebar.scss116
-rw-r--r--code/frontpage/assets/scss/vendor/.gitkeep0
-rw-r--r--code/frontpage/babel.config.js17
-rw-r--r--code/frontpage/config/_default/config.toml91
-rw-r--r--code/frontpage/config/_default/languages.toml7
-rw-r--r--code/frontpage/config/_default/markup.toml29
-rw-r--r--code/frontpage/config/_default/menus/menus.en.toml24
-rw-r--r--code/frontpage/config/_default/params.toml98
-rw-r--r--code/frontpage/config/next/config.toml1
-rw-r--r--code/frontpage/config/postcss.config.js44
-rw-r--r--code/frontpage/config/production/config.toml2
-rw-r--r--code/frontpage/content/en/_index.md9
-rw-r--r--code/frontpage/content/en/blog/_index.md8
-rw-r--r--code/frontpage/content/en/blog/hi-greatoffice/index.md17
-rw-r--r--code/frontpage/content/en/contact/index.md11
-rw-r--r--code/frontpage/content/en/docs/index.md11
-rw-r--r--code/frontpage/content/en/privacy/index.md33
-rw-r--r--code/frontpage/content/en/terms/index.md18
-rw-r--r--code/frontpage/data/docs-versions.yml5
-rw-r--r--code/frontpage/i18n/de.yaml5
-rw-r--r--code/frontpage/i18n/en.yaml17
-rw-r--r--code/frontpage/i18n/nl.yaml17
-rw-r--r--code/frontpage/layouts/404.html10
-rw-r--r--code/frontpage/layouts/_default/_markup/render-heading.html1
-rw-r--r--code/frontpage/layouts/_default/baseof.html32
-rw-r--r--code/frontpage/layouts/_default/index.js10
-rw-r--r--code/frontpage/layouts/_default/index.json5
-rw-r--r--code/frontpage/layouts/_default/list.html31
-rw-r--r--code/frontpage/layouts/_default/section.sitemap.xml46
-rw-r--r--code/frontpage/layouts/_default/single.html10
-rw-r--r--code/frontpage/layouts/_default/terms.html20
-rw-r--r--code/frontpage/layouts/_default/versions.html27
-rw-r--r--code/frontpage/layouts/blog/single.html53
-rw-r--r--code/frontpage/layouts/docs/list.html22
-rw-r--r--code/frontpage/layouts/docs/single.html53
-rw-r--r--code/frontpage/layouts/index.headers10
-rw-r--r--code/frontpage/layouts/index.html68
-rw-r--r--code/frontpage/layouts/index.redirects13
-rw-r--r--code/frontpage/layouts/partials/content/card-image.html17
-rw-r--r--code/frontpage/layouts/partials/content/figure.html37
-rw-r--r--code/frontpage/layouts/partials/content/image.html32
-rw-r--r--code/frontpage/layouts/partials/footer/footer.html18
-rw-r--r--code/frontpage/layouts/partials/footer/script-footer.html119
-rw-r--r--code/frontpage/layouts/partials/head/custom-head.html1
-rw-r--r--code/frontpage/layouts/partials/head/favicons.html9
-rw-r--r--code/frontpage/layouts/partials/head/head.html11
-rw-r--r--code/frontpage/layouts/partials/head/opengraph.html69
-rw-r--r--code/frontpage/layouts/partials/head/resource-hints.html4
-rw-r--r--code/frontpage/layouts/partials/head/script-header.html8
-rw-r--r--code/frontpage/layouts/partials/head/seo.html48
-rw-r--r--code/frontpage/layouts/partials/head/structured-data.html210
-rw-r--r--code/frontpage/layouts/partials/head/stylesheet.html11
-rw-r--r--code/frontpage/layouts/partials/head/twitter_cards.html24
-rw-r--r--code/frontpage/layouts/partials/header/alert.html10
-rw-r--r--code/frontpage/layouts/partials/header/header.html254
-rw-r--r--code/frontpage/layouts/partials/main/blog-meta.html2
-rw-r--r--code/frontpage/layouts/partials/main/breadcrumb.html4
-rw-r--r--code/frontpage/layouts/partials/main/date.html6
-rw-r--r--code/frontpage/layouts/partials/main/docs-navigation.html24
-rw-r--r--code/frontpage/layouts/partials/main/edit-page.html34
-rw-r--r--code/frontpage/layouts/partials/main/last-modified.html10
-rw-r--r--code/frontpage/layouts/partials/sidebar/auto-collapsible-menu.html57
-rw-r--r--code/frontpage/layouts/partials/sidebar/auto-default-menu.html37
-rw-r--r--code/frontpage/layouts/partials/sidebar/docs-menu.html9
-rw-r--r--code/frontpage/layouts/partials/sidebar/docs-toc.html26
-rw-r--r--code/frontpage/layouts/partials/sidebar/manual-collapsible-menu.html63
-rw-r--r--code/frontpage/layouts/partials/sidebar/manual-default-menu.html41
-rw-r--r--code/frontpage/layouts/robots.txt7
-rw-r--r--code/frontpage/layouts/rss.xml27
-rw-r--r--code/frontpage/layouts/shortcodes/alert.html12
-rw-r--r--code/frontpage/layouts/shortcodes/details.html4
-rw-r--r--code/frontpage/layouts/shortcodes/email.html1
-rw-r--r--code/frontpage/layouts/shortcodes/mermaid.html8
-rw-r--r--code/frontpage/layouts/shortcodes/video.html11
-rw-r--r--code/frontpage/layouts/sitemap.xml22
-rw-r--r--code/frontpage/package.json74
-rw-r--r--code/frontpage/pnpm-lock.yaml4292
-rw-r--r--code/frontpage/static/android-chrome-192x192.pngbin0 -> 5765 bytes
-rw-r--r--code/frontpage/static/android-chrome-512x512.pngbin0 -> 20786 bytes
-rw-r--r--code/frontpage/static/apple-touch-icon.pngbin0 -> 5105 bytes
-rw-r--r--code/frontpage/static/css/vendor/.gitkeep0
-rw-r--r--code/frontpage/static/doks.pngbin0 -> 8211 bytes
-rw-r--r--code/frontpage/static/doks.svg1
-rw-r--r--code/frontpage/static/favicon-16x16.pngbin0 -> 416 bytes
-rw-r--r--code/frontpage/static/favicon-32x32.pngbin0 -> 773 bytes
-rw-r--r--code/frontpage/static/favicon.icobin0 -> 15406 bytes
-rw-r--r--code/frontpage/static/favicon.svg1
-rw-r--r--code/frontpage/static/images/vendor/.gitkeep0
-rw-r--r--code/frontpage/static/js/vendor/.gitkeep0
-rw-r--r--code/frontpage/static/logo-doks.pngbin0 -> 20786 bytes
-rw-r--r--code/frontpage/static/logo-doks.svg1
-rw-r--r--code/frontpage/static/site.webmanifest1
-rw-r--r--code/frontpage/theme.toml14
456 files changed, 52045 insertions, 0 deletions
diff --git a/code/api/.build.yaml b/code/api/.build.yaml
new file mode 100644
index 0000000..412cd17
--- /dev/null
+++ b/code/api/.build.yaml
@@ -0,0 +1,23 @@
+image: ubuntu/lts
+packages:
+ - docker.io
+secrets:
+ - ea28f7fe-b300-4b79-addf-d487ed6eb1ef
+ - b6c0403d-10a9-4238-89cc-5402dc0c9fe5
+sources:
+ - git@git.ivar.systems:greatoffice
+tasks:
+ - setup: |
+ echo "export IMAGE_NAME=greatoffice/server
+ export HUB_NAME=dr.ivar.systems/greatoffice/server
+ export CURRENT_VERSION=$(cat ~/greatoffice/server/.version)
+ export CURRENT_VERSION_INT=${CURRENT_VERSION//[!0-9]/}
+ export NEW_VERSION=v$(CURRENT_VERSION_INT+1)-server" >> .buildenv
+ - build: |
+ sudo docker build -t $IMAGE_NAME:$NEW_VERSION ~/greatoffice/server
+ - publish: |
+ cat ~/.dockerpassword | sudo docker login dr.ivar.systems -u builder --password-stdin
+ sudo docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:$NEW_VERSION
+ sudo docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest
+ sudo docker push -a
+ complete-build() \ No newline at end of file
diff --git a/code/api/.dockerignore b/code/api/.dockerignore
new file mode 100644
index 0000000..2b24da3
--- /dev/null
+++ b/code/api/.dockerignore
@@ -0,0 +1,10 @@
+*/**/bin*
+*/**/obj*
+server-secrets.*
+.git
+*/**/node_modules/*
+src/web-app
+src/web-shared
+src/tests
+build_and_push.sh
+cloc.sh
diff --git a/code/api/.version b/code/api/.version
new file mode 100644
index 0000000..f20f9eb
--- /dev/null
+++ b/code/api/.version
@@ -0,0 +1 @@
+v22-server
diff --git a/code/api/.version-dev b/code/api/.version-dev
new file mode 100644
index 0000000..9cf68da
--- /dev/null
+++ b/code/api/.version-dev
@@ -0,0 +1 @@
+v47-server-dev
diff --git a/code/api/CHANGELOG.md b/code/api/CHANGELOG.md
new file mode 100644
index 0000000..88710c3
--- /dev/null
+++ b/code/api/CHANGELOG.md
@@ -0,0 +1,123 @@
+# Changelog
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v41-server-dev
+
+### Refactor
+
+- Implement caching in VaultService and use VaultService instead of IOptions
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v40-server-dev
+
+### Refactor
+
+- Use Vault to get configuration
+
+## [unreleased]
+
+### Features
+
+- !WIP start implementation of svelte-query
+
+### Miscellaneous Tasks
+
+- Bump version
+- Remove logging of quartz db host
+- Update CHANGELOG.md for v6-projects-dev
+- Bump version
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v39-server-dev
+
+### Refactor
+
+- Use Vault to get configuration
+- Small changes on button style
+- Add a small box-shadow
+
+## [unreleased]
+
+### Bug Fixes
+
+- !WIP flickering dropdown on multi dropdowns with new focus strategy
+- Fix route matching when deciding which tab is open
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v22-server
+- Bump version
+- Update CHANGELOG.md for v15-portal-dev
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v14-portal-dev
+- Bump version
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v38-server-dev
+
+### Refactor
+
+- Dont loose focus on search input when navigating results
+- Make optional base fields nullable
+- Remove text
+- Rename accounts to portal
+- Rename accounts to portal
+- Rename accounts to portal
+- Rename noResultsText
+
+## [unreleased]
+
+### Bug Fixes
+
+- !WIP flickering dropdown on multi dropdowns with new focus strategy
+- Fix route matching when deciding which tab is open
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v15-portal-dev
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v14-portal-dev
+- Bump version
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v37-server-dev
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v22-server
+
+### Refactor
+
+- Dont loose focus on search input when navigating results
+- Make optional base fields nullable
+- Remove text
+- Rename accounts to portal
+- Rename accounts to portal
+- Rename accounts to portal
+- Rename noResultsText
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v37-server-dev
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v21-server
+
diff --git a/code/api/Dockerfile b/code/api/Dockerfile
new file mode 100644
index 0000000..5a5545f
--- /dev/null
+++ b/code/api/Dockerfile
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
+WORKDIR /source
+
+# Copy csproj and restore as distinct layers
+COPY src/*.csproj ./
+RUN dotnet restore
+
+# Copy everything else and build
+COPY src/ ./
+RUN dotnet publish -c Release -o out
+
+# Build runtime image
+FROM mcr.microsoft.com/dotnet/aspnet:7.0
+WORKDIR /app
+COPY --from=build-env /source/out .
+ENTRYPOINT ["dotnet", "IOL.GreatOffice.Api.dll"]
diff --git a/code/api/build_and_push.sh b/code/api/build_and_push.sh
new file mode 100755
index 0000000..163d7f3
--- /dev/null
+++ b/code/api/build_and_push.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+
+set -Eueo pipefail
+
+CURRENT_DEV_VERSION=$(cat .version-dev)
+CURRENT_DEV_VERSION_INT=${CURRENT_DEV_VERSION//[!0-9]/}
+CURRENT_VERSION=$(cat .version)
+CURRENT_VERSION_INT=${CURRENT_VERSION//[!0-9]/}
+if [ ${1-prod} == "dev" ]; then
+ NEW_VERSION="v$((CURRENT_DEV_VERSION_INT + 1))-dev"
+ OLD_VERSION=$CURRENT_DEV_VERSION
+else
+ NEW_VERSION="v$((CURRENT_VERSION_INT + 1))"
+ OLD_VERSION=$CURRENT_VERSION
+fi
+IMAGE_NAME="greatoffice/server"
+HUB_NAME="dr.ivar.systems/greatoffice/server"
+
+# Check for uncommited changes and optionally commit them
+if [ "$(git status --untracked-files=no --porcelain)" ]; then
+ echo "Unclean git tree! press CTRL+C to exit or press ENTER to commit and push to the default branch"
+ read -n 1
+
+ read -p "Enter commit message: " COMMIT_MESSAGE
+ git add ..
+ git commit --quiet -m "$COMMIT_MESSAGE"
+fi
+
+if [ ${1-prod} == "dev" ]; then
+ echo $NEW_VERSION >|.version-dev
+ git add .version-dev
+else
+ echo $NEW_VERSION >|.version
+ git add .version
+fi
+
+echo "Starting build of $HUB_NAME:$NEW_VERSION at $(date -u)..."
+echo
+
+# Put version.txt inside of server
+pushd src/wwwroot
+echo "$NEW_VERSION" >version.txt
+git add version.txt
+popd
+
+git commit --quiet -m "chore(release): Bump version"
+
+read -p "Do you want to tag this build? (y/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ read -p "Enter tag message (can be empty): " TAG_MESSAGE
+ git tag -am "$TAG_MESSAGE" $NEW_VERSION
+fi
+
+read -p "Do you want to push the latest commits and tags to origin? (y/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Pushing latest changes to remotes..."
+ echo
+ git push --quiet --follow-tags
+fi
+
+# Build docker image
+echo "Building docker image"
+echo
+
+docker build -t $IMAGE_NAME:$NEW_VERSION .
+
+docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:$NEW_VERSION
+
+if [ ${1-prod} == "dev" ]; then
+ docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest-dev
+fi
+if [ ${1-prod} == "prod" ]; then
+ docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest
+fi
+
+# Optionally push images to docker registry
+echo "Press CTRL+C to exit or press ENTER to push docker image to registry"
+read -n 1
+docker push $HUB_NAME:$NEW_VERSION
+
+if [ ${1-prod} == "dev" ]; then
+ docker push $HUB_NAME:latest-dev
+fi
+
+if [ ${1-prod} == "prod" ]; then
+ docker push $HUB_NAME:latest
+fi
diff --git a/code/api/cliff.toml b/code/api/cliff.toml
new file mode 100644
index 0000000..7299951
--- /dev/null
+++ b/code/api/cliff.toml
@@ -0,0 +1,62 @@
+# configuration file for git-cliff (0.1.0)
+
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+"""
+# template for the changelog body
+# https://tera.netlify.app/docs/#introduction
+body = """
+{% if version %}\
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+ ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
+ {% endfor %}
+{% endfor %}\n
+"""
+# remove the leading and trailing whitespace from the template
+trim = true
+# changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# filter out the commits that are not conventional
+filter_unconventional = true
+# regex for preprocessing the commit messages
+commit_preprocessors = [
+ { pattern = "([ \\n])(([a-f0-9]{7})[a-f0-9]*)", replace = "${1}commit # [${3}](https://git.ivar.systems/greatoffice/commit/${2})" },
+ { pattern = "https://git.ivar.systems/greatoffice/commit/([a-f0-9]{7})[a-f0-9]*", replace = "commit # [${1}](${0})" },
+]
+# regex for parsing and grouping commits
+commit_parsers = [
+ { message = "^feat", group = "Features" },
+ { message = "^fix", group = "Bug Fixes" },
+ { message = "^doc", group = "Documentation" },
+ { message = "^perf", group = "Performance" },
+ { message = "^refactor", group = "Refactor" },
+ { message = "^style", group = "Styling" },
+ { message = "^test", group = "Testing" },
+ { message = "^chore", group = "Miscellaneous Tasks" },
+]
+# filter out the commits that are not matched by commit parsers
+filter_commits = true
+# glob pattern for matching git tags
+tag_pattern = "v.*"
+# regex for skipping tags
+skip_tags = "v0.1.0-beta.1"
+# regex for ignoring tags
+ignore_tags = ""
+# sort the tags chronologically
+date_order = true
+# sort the commits inside sections by oldest/newest order
+sort_commits = "newest"
diff --git a/code/api/sql/quartz-create.sql b/code/api/sql/quartz-create.sql
new file mode 100644
index 0000000..d0dc298
--- /dev/null
+++ b/code/api/sql/quartz-create.sql
@@ -0,0 +1,156 @@
+CREATE TABLE IF NOT EXISTS qrtz_job_details
+(
+ sched_name TEXT NOT NULL,
+ job_name TEXT NOT NULL,
+ job_group TEXT NOT NULL,
+ description TEXT NULL,
+ job_class_name TEXT NOT NULL,
+ is_durable BOOL NOT NULL,
+ is_nonconcurrent BOOL NOT NULL,
+ is_update_data BOOL NOT NULL,
+ requests_recovery BOOL NOT NULL,
+ job_data BYTEA NULL,
+ PRIMARY KEY (sched_name, job_name, job_group)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_triggers
+(
+ sched_name TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ job_name TEXT NOT NULL,
+ job_group TEXT NOT NULL,
+ description TEXT NULL,
+ next_fire_time BIGINT NULL,
+ prev_fire_time BIGINT NULL,
+ priority INTEGER NULL,
+ trigger_state TEXT NOT NULL,
+ trigger_type TEXT NOT NULL,
+ start_time BIGINT NOT NULL,
+ end_time BIGINT NULL,
+ calendar_name TEXT NULL,
+ misfire_instr SMALLINT NULL,
+ job_data BYTEA NULL,
+ PRIMARY KEY (sched_name, trigger_name, trigger_group),
+ FOREIGN KEY (sched_name, job_name, job_group)
+ REFERENCES qrtz_job_details (sched_name, job_name, job_group)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_simple_triggers
+(
+ sched_name TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ repeat_count BIGINT NOT NULL,
+ repeat_interval BIGINT NOT NULL,
+ times_triggered BIGINT NOT NULL,
+ PRIMARY KEY (sched_name, trigger_name, trigger_group),
+ FOREIGN KEY (sched_name, trigger_name, trigger_group)
+ REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS QRTZ_SIMPROP_TRIGGERS
+(
+ sched_name TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ str_prop_1 TEXT NULL,
+ str_prop_2 TEXT NULL,
+ str_prop_3 TEXT NULL,
+ int_prop_1 INTEGER NULL,
+ int_prop_2 INTEGER NULL,
+ long_prop_1 BIGINT NULL,
+ long_prop_2 BIGINT NULL,
+ dec_prop_1 NUMERIC NULL,
+ dec_prop_2 NUMERIC NULL,
+ bool_prop_1 BOOL NULL,
+ bool_prop_2 BOOL NULL,
+ time_zone_id TEXT NULL,
+ PRIMARY KEY (sched_name, trigger_name, trigger_group),
+ FOREIGN KEY (sched_name, trigger_name, trigger_group)
+ REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_cron_triggers
+(
+ sched_name TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ cron_expression TEXT NOT NULL,
+ time_zone_id TEXT,
+ PRIMARY KEY (sched_name, trigger_name, trigger_group),
+ FOREIGN KEY (sched_name, trigger_name, trigger_group)
+ REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_blob_triggers
+(
+ sched_name TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ blob_data BYTEA NULL,
+ PRIMARY KEY (sched_name, trigger_name, trigger_group),
+ FOREIGN KEY (sched_name, trigger_name, trigger_group)
+ REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_calendars
+(
+ sched_name TEXT NOT NULL,
+ calendar_name TEXT NOT NULL,
+ calendar BYTEA NOT NULL,
+ PRIMARY KEY (sched_name, calendar_name)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_paused_trigger_grps
+(
+ sched_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ PRIMARY KEY (sched_name, trigger_group)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_fired_triggers
+(
+ sched_name TEXT NOT NULL,
+ entry_id TEXT NOT NULL,
+ trigger_name TEXT NOT NULL,
+ trigger_group TEXT NOT NULL,
+ instance_name TEXT NOT NULL,
+ fired_time BIGINT NOT NULL,
+ sched_time BIGINT NOT NULL,
+ priority INTEGER NOT NULL,
+ state TEXT NOT NULL,
+ job_name TEXT NULL,
+ job_group TEXT NULL,
+ is_nonconcurrent BOOL NOT NULL,
+ requests_recovery BOOL NULL,
+ PRIMARY KEY (sched_name, entry_id)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_scheduler_state
+(
+ sched_name TEXT NOT NULL,
+ instance_name TEXT NOT NULL,
+ last_checkin_time BIGINT NOT NULL,
+ checkin_interval BIGINT NOT NULL,
+ PRIMARY KEY (sched_name, instance_name)
+);
+
+CREATE TABLE IF NOT EXISTS qrtz_locks
+(
+ sched_name TEXT NOT NULL,
+ lock_name TEXT NOT NULL,
+ PRIMARY KEY (sched_name, lock_name)
+);
+
+CREATE INDEX IF NOT EXISTS idx_qrtz_j_req_recovery on qrtz_job_details (requests_recovery);
+CREATE INDEX IF NOT EXISTS idx_qrtz_t_next_fire_time on qrtz_triggers (next_fire_time);
+CREATE INDEX IF NOT EXISTS idx_qrtz_t_state on qrtz_triggers (trigger_state);
+CREATE INDEX IF NOT EXISTS idx_qrtz_t_nft_st on qrtz_triggers (next_fire_time, trigger_state);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_trig_name on qrtz_fired_triggers (trigger_name);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_trig_group on qrtz_fired_triggers (trigger_group);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_trig_nm_gp on qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers (instance_name);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_job_name on qrtz_fired_triggers (job_name);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_job_group on qrtz_fired_triggers (job_group);
+CREATE INDEX IF NOT EXISTS idx_qrtz_ft_job_req_recovery on qrtz_fired_triggers (requests_recovery);
diff --git a/code/api/sql/quartz-drop.sql b/code/api/sql/quartz-drop.sql
new file mode 100644
index 0000000..87b0797
--- /dev/null
+++ b/code/api/sql/quartz-drop.sql
@@ -0,0 +1,23 @@
+DROP TABLE IF EXISTS qrtz_fired_triggers;
+DROP TABLE IF EXISTS qrtz_paused_trigger_grps;
+DROP TABLE IF EXISTS qrtz_scheduler_state;
+DROP TABLE IF EXISTS qrtz_locks;
+DROP TABLE IF EXISTS qrtz_simprop_triggers;
+DROP TABLE IF EXISTS qrtz_simple_triggers;
+DROP TABLE IF EXISTS qrtz_cron_triggers;
+DROP TABLE IF EXISTS qrtz_blob_triggers;
+DROP TABLE IF EXISTS qrtz_triggers;
+DROP TABLE IF EXISTS qrtz_job_details;
+DROP TABLE IF EXISTS qrtz_calendars;
+
+DROP INDEX IF EXISTS idx_qrtz_j_req_recovery;
+DROP INDEX IF EXISTS idx_qrtz_t_next_fire_time;
+DROP INDEX IF EXISTS idx_qrtz_t_state;
+DROP INDEX IF EXISTS idx_qrtz_t_nft_st;
+DROP INDEX IF EXISTS idx_qrtz_ft_trig_name;
+DROP INDEX IF EXISTS idx_qrtz_ft_trig_group;
+DROP INDEX IF EXISTS idx_qrtz_ft_trig_nm_gp;
+DROP INDEX IF EXISTS idx_qrtz_ft_trig_inst_name;
+DROP INDEX IF EXISTS idx_qrtz_ft_job_name;
+DROP INDEX IF EXISTS idx_qrtz_ft_job_group;
+DROP INDEX IF EXISTS idx_qrtz_ft_job_req_recovery;
diff --git a/code/api/src/Endpoints/EndpointBase.cs b/code/api/src/Endpoints/EndpointBase.cs
new file mode 100644
index 0000000..105fbdf
--- /dev/null
+++ b/code/api/src/Endpoints/EndpointBase.cs
@@ -0,0 +1,54 @@
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IOL.GreatOffice.Api.Endpoints;
+
+[ApiController]
+public class EndpointBase : ControllerBase
+{
+ /// <summary>
+ /// User data for the currently logged on user.
+ /// </summary>
+ protected LoggedInUserModel LoggedInUser => new() {
+ Username = User.FindFirstValue(AppClaims.NAME),
+ Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(),
+ };
+
+ [NonAction]
+ protected ActionResult KnownProblem(string title = default, string subtitle = default, Dictionary<string, string[]> errors = default) {
+ HttpContext.Response.Headers.Add(AppHeaders.IS_KNOWN_PROBLEM, "1");
+ return BadRequest(new KnownProblemModel {
+ Title = title,
+ Subtitle = subtitle,
+ Errors = errors,
+ TraceId = HttpContext.TraceIdentifier
+ });
+ }
+
+ [NonAction]
+ protected ActionResult KnownProblem(KnownProblemModel problem) {
+ HttpContext.Response.Headers.Add(AppHeaders.IS_KNOWN_PROBLEM, "1");
+ problem.TraceId = HttpContext.TraceIdentifier;
+ return BadRequest(problem);
+ }
+
+ [NonAction]
+ protected RequestTimeZoneInfo GetRequestTimeZone(ILogger logger = default) {
+ Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader);
+ var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC");
+ var offset = tz.BaseUtcOffset.Hours;
+
+ // This is fine as long as the client is not connecting from Australia: Lord Howe Island,
+ // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country
+ if (tz.IsDaylightSavingTime(AppDateTime.UtcNow)) {
+ offset++;
+ }
+
+ logger?.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours");
+
+ return new RequestTimeZoneInfo() {
+ TimeZoneInfo = tz,
+ Offset = offset,
+ LocalDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz)
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs
new file mode 100644
index 0000000..ee136a9
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs
@@ -0,0 +1,64 @@
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountRoute.Payload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+ private readonly EmailValidationService _emailValidation;
+ private readonly ILogger<CreateAccountRoute> _logger;
+ private readonly TenantService _tenantService;
+
+ public CreateAccountRoute(UserService userService, MainAppDatabase database, IStringLocalizer<SharedResources> localizer, EmailValidationService emailValidation, TenantService tenantService, ILogger<CreateAccountRoute> logger) {
+ _userService = userService;
+ _database = database;
+ _localizer = localizer;
+ _emailValidation = emailValidation;
+ _tenantService = tenantService;
+ _logger = logger;
+ }
+
+ public class Payload
+ {
+ public string Username { get; set; }
+ public string Password { get; set; }
+ }
+
+ [AllowAnonymous]
+ [HttpPost("~/_/account/create")]
+ public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) {
+ var problem = new KnownProblemModel();
+ var username = request.Username.Trim();
+ if (username.IsValidEmailAddress() == false) {
+ problem.AddError("username", _localizer["{username} does not look like a valid email", username]);
+ } else if (_database.Users.FirstOrDefault(c => c.Username == username) != default) {
+ problem.AddError("username", _localizer["There is already a user registered with username: {username}", username]);
+ }
+
+ if (request.Password.Length < 6) {
+ problem.AddError("password", _localizer["The password requires 6 or more characters."]);
+ }
+
+ if (problem.Errors.Any()) {
+ problem.Title = _localizer["Invalid form"];
+ problem.Subtitle = _localizer["One or more fields is invalid"];
+ return KnownProblem(problem);
+ }
+
+ var user = new User(username);
+ var tenant = _tenantService.CreateTenant(user.DisplayName() + "'s tenant", user.Id, user.Username);
+ if (tenant == default) {
+ _logger.LogError("Not creating new user because the tenant could not be created");
+ return KnownProblem(_localizer["Could not create your account, try again soon."]);
+ }
+
+ user.HashAndSetPassword(request.Password);
+ _database.Users.Add(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ await _userService.LogInUserAsync(HttpContext, user, false, cancellationToken);
+ await _emailValidation.SendValidationEmailAsync(user);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs
new file mode 100644
index 0000000..e1d13dd
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs
@@ -0,0 +1,32 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
+
+ public CreateInitialAccountRoute(MainAppDatabase database, UserService userService) {
+ _database = database;
+ _userService = userService;
+ }
+
+ /// <summary>
+ /// Create an initial user account.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpGet("~/_/account/create-initial")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ if (_database.Users.Any()) {
+ return NotFound();
+ }
+
+ var user = new User("admin@ivarlovlie.no");
+ user.HashAndSetPassword("ivar123");
+ _database.Users.Add(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ await _userService.LogInUserAsync(HttpContext, user, cancellationToken: cancellationToken);
+ return Redirect("/");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs
new file mode 100644
index 0000000..f487f74
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs
@@ -0,0 +1,17 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult
+{
+ private readonly UserService _userService;
+
+ public DeleteAccountRoute(UserService userService) {
+ _userService = userService;
+ }
+
+ [HttpDelete("~/_/account/delete")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ await _userService.LogOutUser(HttpContext, cancellationToken);
+ await _userService.MarkUserAsDeleted(LoggedInUser.Id, LoggedInUser.Id);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs
new file mode 100644
index 0000000..121b40f
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs
@@ -0,0 +1,26 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<LoggedInUserModel>
+{
+ private readonly MainAppDatabase _database;
+
+ public GetAccountRoute(MainAppDatabase database) {
+ _database = database;
+ }
+
+ [HttpGet("~/_/account")]
+ public override async Task<ActionResult<LoggedInUserModel>> HandleAsync(CancellationToken cancellationToken = default) {
+ var user = _database.Users
+ .Select(x => new {x.Username, x.Id})
+ .SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user != default) {
+ return Ok(new LoggedInUserModel {
+ Id = LoggedInUser.Id,
+ Username = LoggedInUser.Username
+ });
+ }
+
+ await HttpContext.SignOutAsync();
+ return Unauthorized();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs
new file mode 100644
index 0000000..703f324
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs
@@ -0,0 +1,37 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class LoginRoute : RouteBaseAsync.WithRequest<LoginRoute.Payload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public LoginRoute(MainAppDatabase database, UserService userService, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _userService = userService;
+ _localizer = localizer;
+ }
+
+ public class Payload
+ {
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public bool Persist { get; set; }
+ }
+
+ [AllowAnonymous]
+ [HttpPost("~/_/account/login")]
+ public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) {
+ var user = _database.Users.FirstOrDefault(u => u.Username == request.Username);
+ if (user == default || !user.VerifyPassword(request.Password)) {
+ return KnownProblem(_localizer["Invalid username or password"]);
+ }
+
+ if (user.Deleted) {
+ return KnownProblem(_localizer["This user is deleted, please contact support@greatoffice.life if you think this is an error"]);
+ }
+
+ await _userService.LogInUserAsync(HttpContext, user, request.Persist, cancellationToken);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs
new file mode 100644
index 0000000..295d9f6
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs
@@ -0,0 +1,22 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class LogoutRoute : RouteBaseAsync.WithoutRequest.WithActionResult
+{
+ private readonly UserService _userService;
+
+ public LogoutRoute(UserService userService) {
+ _userService = userService;
+ }
+
+ /// <summary>
+ /// Logout a user.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpGet("~/_/account/logout")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ await _userService.LogOutUser(HttpContext, cancellationToken);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs
new file mode 100644
index 0000000..1081240
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs
@@ -0,0 +1,59 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
+
+public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdateAccountRoute.Payload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public UpdateAccountRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _localizer = localizer;
+ }
+
+ public class Payload
+ {
+ public string Username { get; set; }
+
+ public string Password { get; set; }
+ }
+
+ [HttpPost("~/_/account/update")]
+ public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) {
+ var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ await HttpContext.SignOutAsync();
+ return Unauthorized();
+ }
+
+ if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) {
+ return KnownProblem(_localizer["Invalid request"], _localizer["No data was submitted"]);
+ }
+
+ var problem = new KnownProblemModel();
+
+ if (request.Password.HasValue() && request.Password.Length < 6) {
+ problem.AddError("password", _localizer["The new password must contain at least 6 characters"]);
+ }
+
+ if (request.Password.HasValue()) {
+ user.HashAndSetPassword(request.Password);
+ }
+
+ if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) {
+ problem.AddError("username", _localizer["The new username does not look like a valid email address"]);
+ }
+
+ if (problem.Errors.Any()) {
+ problem.Title = _localizer["Invalid form"];
+ problem.Subtitle = _localizer["One or more validation errors occured"];
+ return KnownProblem(problem);
+ }
+
+ if (request.Username.HasValue()) {
+ user.Username = request.Username.Trim();
+ }
+
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/_calls.http b/code/api/src/Endpoints/Internal/Account/_calls.http
new file mode 100644
index 0000000..78380f5
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Account/_calls.http
@@ -0,0 +1,9 @@
+### Login
+POST http://localhost:5000/_/account/login
+Content-Type: application/json
+
+{
+ "username": "i@oiee.no",
+ "password": "ivar123",
+ "persist": false
+}
diff --git a/code/api/src/Endpoints/Internal/INT_EndpointBase.cs b/code/api/src/Endpoints/Internal/INT_EndpointBase.cs
new file mode 100644
index 0000000..699a976
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/INT_EndpointBase.cs
@@ -0,0 +1,9 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal;
+
+[Authorize]
+[ApiExplorerSettings(IgnoreApi = true)]
+[ApiVersionNeutral]
+public class INT_EndpointBase : EndpointBase
+{
+
+}
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs
new file mode 100644
index 0000000..c6ed417
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs
@@ -0,0 +1,40 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
+
+public class CreateResetRequestRoute : RouteBaseAsync.WithRequest<CreateResetRequestRoute.Payload>.WithActionResult
+{
+ private readonly ILogger<CreateResetRequestRoute> _logger;
+ private readonly PasswordResetService _passwordResetService;
+ private readonly MainAppDatabase _database;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateResetRequestRoute(ILogger<CreateResetRequestRoute> logger, PasswordResetService passwordResetService, MainAppDatabase database, IStringLocalizer<SharedResources> localizer) {
+ _logger = logger;
+ _passwordResetService = passwordResetService;
+ _database = database;
+ _localizer = localizer;
+ }
+
+ public class Payload
+ {
+ public string Email { get; set; }
+ }
+
+ [AllowAnonymous]
+ [HttpPost("~/_/password-reset-request/create")]
+ public override async Task<ActionResult> HandleAsync(Payload payload, CancellationToken cancellationToken = default) {
+ if (payload.Email.IsNullOrWhiteSpace()) {
+ return KnownProblem(_localizer["Invalid form"],
+ _localizer["One or more fields is invalid"],
+ new() {{"email", new string[] {_localizer["Email is a required field"]}}}
+ );
+ }
+
+ var tz = GetRequestTimeZone(_logger);
+ _logger.LogInformation("Creating forgot password request with local date time: " + tz.LocalDateTime.ToString("u"));
+ var user = _database.Users.FirstOrDefault(c => c.Username.Equals(payload.Email));
+ // Don't inform the caller that the user does not exist.
+ if (user == default) return Ok();
+ await _passwordResetService.AddRequestAsync(user, tz.TimeZoneInfo, cancellationToken);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs
new file mode 100644
index 0000000..9cd92bb
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs
@@ -0,0 +1,36 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
+
+public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest<FulfillResetRequestRoute.Payload>.WithActionResult
+{
+ private readonly IStringLocalizer<SharedResources> _localizer;
+ private readonly PasswordResetService _passwordResetService;
+
+ public FulfillResetRequestRoute(PasswordResetService passwordResetService, IStringLocalizer<SharedResources> localizer) {
+ _passwordResetService = passwordResetService;
+ _localizer = localizer;
+ }
+
+ public class Payload
+ {
+ public Guid Id { get; set; }
+ public string NewPassword { get; set; }
+ }
+
+ [AllowAnonymous]
+ [HttpPost("~/_/password-reset-request/fulfill")]
+ public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) {
+ if (request.NewPassword.Length < 6) {
+ return KnownProblem(_localizer["Invalid form"],
+ _localizer["One or more fields is invalid"],
+ new Dictionary<string, string[]> {{"newPassword", new string[] {_localizer["The new password needs to be atleast 6 characters"]}}}
+ );
+ }
+
+ return await _passwordResetService.FulfillRequestAsync(request.Id, request.NewPassword, cancellationToken) switch {
+ FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND => NotFound(),
+ FulfillPasswordResetRequestResult.USER_NOT_FOUND => NotFound(),
+ FulfillPasswordResetRequestResult.FULFILLED => Ok(),
+ _ => StatusCode(500)
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs
new file mode 100644
index 0000000..a87c0a9
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs
@@ -0,0 +1,26 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
+
+public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest<Guid>.WithActionResult<IsResetRequestValidRoute.ResponseModel>
+{
+ private readonly PasswordResetService _passwordResetService;
+
+ public IsResetRequestValidRoute(PasswordResetService passwordResetService) {
+ _passwordResetService = passwordResetService;
+ }
+
+ public class ResponseModel
+ {
+ public ResponseModel(bool isValid) {
+ IsValid = isValid;
+ }
+
+ public bool IsValid { get; }
+ }
+
+ [AllowAnonymous]
+ [HttpGet("~/_/password-reset-request/is-valid")]
+ public override async Task<ActionResult<ResponseModel>> HandleAsync(Guid id, CancellationToken cancellationToken = default) {
+ var request = await _passwordResetService.GetRequestAsync(id, cancellationToken);
+ return Ok(request == default ? new ResponseModel(false) : new ResponseModel(!request.IsExpired));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http b/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http
new file mode 100644
index 0000000..cfd2d58
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http
@@ -0,0 +1,22 @@
+### Create request
+POST http://localhost:5000/_/password-reset-request/create
+Accept: application/json
+Content-Type: application/json
+
+{
+ "email": ""
+}
+
+### Fulfill request
+POST http://localhost:5000/_/password-reset-request/fulfill
+Accept: application/json
+Content-Type: application/json
+
+{
+ "id": "",
+ "newPassword": ""
+}
+
+### Is request valid
+GET http://localhost:5000/_/password-reset-request/is-valid?id=
+Accept: application/json
diff --git a/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs b/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs
new file mode 100644
index 0000000..82bbb11
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs
@@ -0,0 +1,64 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
+
+public class GetSessionRoute : RouteBaseSync.WithoutRequest.WithActionResult<GetSessionRoute.SessionResponse>
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<GetSessionRoute> _logger;
+
+ public GetSessionRoute(MainAppDatabase database, ILogger<GetSessionRoute> logger) {
+ _database = database;
+ _logger = logger;
+ }
+
+ public class SessionResponse
+ {
+ public string Username { get; set; }
+ public string DisplayName { get; set; }
+ public Guid UserId { get; set; }
+ public SessionTenant CurrentTenant { get; set; }
+ public List<SessionTenant> AvailableTenants { get; set; }
+
+ public class SessionTenant
+ {
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ }
+ }
+
+ [Authorize]
+ [HttpGet("~/_/session-data")]
+ public override ActionResult<SessionResponse> Handle() {
+ var user = _database.Users.Include(c => c.Tenants)
+ .Select(c => new User() {
+ Id = c.Id,
+ Username = c.Username,
+ FirstName = c.FirstName,
+ LastName = c.LastName,
+ Tenants = c.Tenants
+ }).FirstOrDefault(c => c.Id == LoggedInUser.Id);
+
+ if (user == default) {
+ return NotFound();
+ }
+
+ var currentTenant = user.Tenants.FirstOrDefault(c => c.Id == LoggedInUser.TenantId);
+ if (currentTenant == default) {
+ _logger.LogInformation("Could not find current tenant ({tenantId}) for user {userId}", LoggedInUser.TenantId, LoggedInUser.Id);
+ return NotFound();
+ }
+
+ return Ok(new SessionResponse() {
+ Username = user.Username,
+ DisplayName = user.DisplayName(),
+ UserId = user.Id,
+ CurrentTenant = new SessionResponse.SessionTenant() {
+ Id = currentTenant.Id,
+ Name = currentTenant.Name
+ },
+ AvailableTenants = user.Tenants.Select(c => new SessionResponse.SessionTenant() {
+ Id = c.Id,
+ Name = c.Name
+ }).ToList()
+ });
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs b/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs
new file mode 100644
index 0000000..7bb0a86
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs
@@ -0,0 +1,10 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
+
+public class IsAuthenticatedRoute : RouteBaseSync.WithoutRequest.WithActionResult
+{
+ [Authorize]
+ [HttpGet("~/_/is-authenticated")]
+ public override ActionResult Handle() {
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs
new file mode 100644
index 0000000..7270fd8
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs
@@ -0,0 +1,17 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
+
+public class ReadConfigurationRoute : RouteBaseSync.WithoutRequest.WithActionResult
+{
+ private readonly VaultService _vaultService;
+
+ public ReadConfigurationRoute(VaultService vaultService) {
+ _vaultService = vaultService;
+ }
+
+ [AllowAnonymous]
+ [HttpGet("~/_/configuration")]
+ public override ActionResult Handle() {
+ var config = _vaultService.GetCurrentAppConfiguration();
+ return Content(JsonSerializer.Serialize(config.GetPublicVersion()), "application/json");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs
new file mode 100644
index 0000000..fde4832
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs
@@ -0,0 +1,15 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
+
+public class RefreshConfigurationRoute : RouteBaseSync.WithoutRequest.WithoutResult
+{
+ private readonly VaultService _vaultService;
+
+ public RefreshConfigurationRoute(VaultService vaultService) {
+ _vaultService = vaultService;
+ }
+
+ [HttpGet("~/_/refresh-configuration")]
+ public override void Handle() {
+ _vaultService.RefreshCurrentAppConfigurationAsync();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs
new file mode 100644
index 0000000..8f0882d
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs
@@ -0,0 +1,39 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
+
+public class ValidateRoute : RouteBaseSync.WithRequest<ValidateRoute.QueryParams>.WithActionResult
+{
+ private readonly EmailValidationService _emailValidation;
+ private readonly string CanonicalFrontendUrl;
+ private readonly ILogger<ValidateRoute> _logger;
+
+ public ValidateRoute(VaultService vaultService, EmailValidationService emailValidation, ILogger<ValidateRoute> logger) {
+ _emailValidation = emailValidation;
+ _logger = logger;
+ var c = vaultService.GetCurrentAppConfiguration();
+ CanonicalFrontendUrl = c.CANONICAL_FRONTEND_URL;
+ }
+
+ public class QueryParams
+ {
+ [FromQuery]
+ public Guid Id { get; set; }
+ }
+
+ [HttpGet("~/_/validate")]
+ public override ActionResult Handle([FromQuery] QueryParams request) {
+ var isFulfilled = _emailValidation.FulfillEmailValidationRequest(request.Id, LoggedInUser.Id);
+ if (!isFulfilled) {
+ _logger.LogError("Email validation fulfillment failed for request {requestId} and user {userId}", request.Id, LoggedInUser.Id);
+ return StatusCode(400, $"""
+<html>
+<body>
+<h3>The validation could not be completed</h3>
+<p>We are working on fixing this, in the meantime, have patience.</p>
+<a href="{CanonicalFrontendUrl}">Click here to go back to {CanonicalFrontendUrl}</a>
+</body>
+""");
+ }
+
+ return Redirect(CanonicalFrontendUrl + "/portal?msg=emailValidated");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/RouteBaseAsync.cs b/code/api/src/Endpoints/Internal/RouteBaseAsync.cs
new file mode 100644
index 0000000..a87facf
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/RouteBaseAsync.cs
@@ -0,0 +1,73 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public static class RouteBaseAsync
+{
+ public static class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : INT_EndpointBase
+ {
+ public abstract Task<TResponse> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : INT_EndpointBase
+ {
+ public abstract Task HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : INT_EndpointBase
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : INT_EndpointBase
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : INT_EndpointBase
+ {
+ public abstract Task<TResponse> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : INT_EndpointBase
+ {
+ public abstract Task HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : INT_EndpointBase
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : INT_EndpointBase
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+}
diff --git a/code/api/src/Endpoints/Internal/RouteBaseSync.cs b/code/api/src/Endpoints/Internal/RouteBaseSync.cs
new file mode 100644
index 0000000..9d9bd5a
--- /dev/null
+++ b/code/api/src/Endpoints/Internal/RouteBaseSync.cs
@@ -0,0 +1,53 @@
+namespace IOL.GreatOffice.Api.Endpoints.Internal;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public static class RouteBaseSync
+{
+ public static class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : INT_EndpointBase
+ {
+ public abstract TResponse Handle(TRequest request);
+ }
+
+ public abstract class WithoutResult : INT_EndpointBase
+ {
+ public abstract void Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult<TResponse> : INT_EndpointBase
+ {
+ public abstract ActionResult<TResponse> Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult : INT_EndpointBase
+ {
+ public abstract ActionResult Handle(TRequest request);
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : INT_EndpointBase
+ {
+ public abstract TResponse Handle();
+ }
+
+ public abstract class WithoutResult : INT_EndpointBase
+ {
+ public abstract void Handle();
+ }
+
+ public abstract class WithActionResult<TResponse> : INT_EndpointBase
+ {
+ public abstract ActionResult<TResponse> Handle();
+ }
+
+ public abstract class WithActionResult : INT_EndpointBase
+ {
+ public abstract ActionResult Handle();
+ }
+ }
+}
diff --git a/code/api/src/Endpoints/V1/ApiSpecV1.cs b/code/api/src/Endpoints/V1/ApiSpecV1.cs
new file mode 100644
index 0000000..e4f9cc9
--- /dev/null
+++ b/code/api/src/Endpoints/V1/ApiSpecV1.cs
@@ -0,0 +1,18 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1;
+
+public static class ApiSpecV1
+{
+ private const int MAJOR = 1;
+ private const int MINOR = 0;
+ public const string VERSION_STRING = "1.0";
+
+ public static ApiSpecDocument Document => new() {
+ Version = new ApiVersion(MAJOR, MINOR),
+ VersionName = VERSION_STRING,
+ SwaggerPath = $"/swagger/{VERSION_STRING}/swagger.json",
+ OpenApiInfo = new OpenApiInfo {
+ Title = AppConstants.API_NAME,
+ Version = VERSION_STRING
+ }
+ };
+}
diff --git a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
new file mode 100644
index 0000000..163ddb6
--- /dev/null
+++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
@@ -0,0 +1,58 @@
+using System.Text;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
+
+public class CreateTokenRoute : RouteBaseSync.WithRequest<CreateTokenRoute.Payload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly AppConfiguration _configuration;
+ private readonly ILogger<CreateTokenRoute> _logger;
+
+ public CreateTokenRoute(MainAppDatabase database, VaultService vaultService, ILogger<CreateTokenRoute> logger) {
+ _database = database;
+ _configuration = vaultService.GetCurrentAppConfiguration();
+ _logger = logger;
+ }
+
+ public class Payload
+ {
+ public DateTime ExpiryDate { get; set; }
+ public bool AllowRead { get; set; }
+ public bool AllowCreate { get; set; }
+ public bool AllowUpdate { get; set; }
+ public bool AllowDelete { get; set; }
+ }
+
+ /// <summary>
+ /// Create a new api token with the provided claims.
+ /// </summary>
+ /// <param name="request">The claims to set on the api token</param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpPost("~/v{version:apiVersion}/api-tokens/create")]
+ public override ActionResult Handle(Payload request) {
+ var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ return NotFound(new KnownProblemModel("User does not exist"));
+ }
+
+ var token_entropy = _configuration.APP_AES_KEY;
+ if (token_entropy.IsNullOrWhiteSpace()) {
+ _logger.LogWarning("No token entropy is available, Basic auth is disabled");
+ return NotFound();
+ }
+
+ var accessToken = new ApiAccessToken() {
+ User = user,
+ ExpiryDate = request.ExpiryDate.ToUniversalTime(),
+ AllowCreate = request.AllowCreate,
+ AllowRead = request.AllowRead,
+ AllowDelete = request.AllowDelete,
+ AllowUpdate = request.AllowUpdate
+ };
+
+ _database.AccessTokens.Add(accessToken);
+ _database.SaveChanges();
+ return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(token_entropy))));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
new file mode 100644
index 0000000..ee19e40
--- /dev/null
+++ b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
@@ -0,0 +1,31 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
+
+public class DeleteTokenRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<DeleteTokenRoute> _logger;
+
+ public DeleteTokenRoute(MainAppDatabase database, ILogger<DeleteTokenRoute> logger) {
+ _database = database;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Delete an api token, rendering it unusable
+ /// </summary>
+ /// <param name="id">Id of the token to delete</param>
+ /// <returns>Nothing</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")]
+ public override ActionResult Handle(Guid id) {
+ var token = _database.AccessTokens.SingleOrDefault(c => c.Id == id);
+ if (token == default) {
+ _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received.");
+ return NotFound();
+ }
+
+ _database.AccessTokens.Remove(token);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
new file mode 100644
index 0000000..ee46b34
--- /dev/null
+++ b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
@@ -0,0 +1,36 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
+
+public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult<ActionResult<List<GetTokensRoute.ResponseModel>>>
+{
+ private readonly MainAppDatabase _database;
+
+ public GetTokensRoute(MainAppDatabase database) {
+ _database = database;
+ }
+
+ public class ResponseModel
+ {
+ public DateTime ExpiryDate { get; set; }
+ public bool AllowRead { get; set; }
+ public bool AllowCreate { get; set; }
+ public bool AllowUpdate { get; set; }
+ public bool AllowDelete { get; set; }
+ public bool HasExpired => ExpiryDate < AppDateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Get all tokens, both active and inactive.
+ /// </summary>
+ /// <returns>A list of tokens</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpGet("~/v{version:apiVersion}/api-tokens")]
+ public override ActionResult<List<ResponseModel>> Handle() {
+ return Ok(_database.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => new ResponseModel() {
+ AllowCreate = c.AllowCreate,
+ AllowRead = c.AllowRead,
+ AllowDelete = c.AllowDelete,
+ AllowUpdate = c.AllowUpdate,
+ ExpiryDate = c.ExpiryDate
+ }));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
new file mode 100644
index 0000000..e58aa37
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
@@ -0,0 +1,66 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1.Customers;
+
+public class CreateCustomerRoute : RouteBaseAsync.WithRequest<CreateCustomerPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<CreateCustomerRoute> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateCustomerRoute(MainAppDatabase database, ILogger<CreateCustomerRoute> logger, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _logger = logger;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/customers/create")]
+ public override async Task<ActionResult> HandleAsync(CreateCustomerPayload request, CancellationToken cancellationToken = default) {
+ var problem = new KnownProblemModel();
+ if (request.Name.Trim().IsNullOrEmpty()) problem.AddError("name", _localizer["Name is a required field"]);
+ if (problem.Errors.Any()) {
+ problem.Title = _localizer["Invalid form"];
+ problem.Subtitle = _localizer["One or more validation errors occured"];
+ return KnownProblem(problem);
+ }
+
+ var customer = new Customer(LoggedInUser) {
+ CustomerNumber = request.CustomerNumber,
+ Name = request.Name,
+ Description = request.Description,
+ Address1 = request.Address1,
+ Address2 = request.Address2,
+ Country = request.Country,
+ Currency = request.Currency,
+ Email = request.Email,
+ Phone = request.Phone,
+ PostalCity = request.PostalCity,
+ PostalCode = request.PostalCode,
+ VATNumber = request.VATNumber,
+ ORGNumber = request.ORGNumber,
+ DefaultReference = request.DefaultReference,
+ Website = request.Website
+ };
+ customer.SetOwnerIds(default, LoggedInUser.TenantId);
+ _database.Customers.Add(customer);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateCustomerPayload
+{
+ public string CustomerNumber { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Address1 { get; set; }
+ public string Address2 { get; set; }
+ public string PostalCode { get; set; }
+ public string PostalCity { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Email { get; set; }
+ public string VATNumber { get; set; }
+ public string ORGNumber { get; set; }
+ public string DefaultReference { get; set; }
+ public string Website { get; set; }
+ public string Currency { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
new file mode 100644
index 0000000..bd37faf
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
@@ -0,0 +1,83 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class CreateProjectRoute : RouteBaseAsync.WithRequest<CreateProjectPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateProjectRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/projects/create")]
+ public override async Task<ActionResult> HandleAsync(CreateProjectPayload request, CancellationToken cancellationToken = default) {
+ var problem = new KnownProblemModel();
+
+ if (request.Name.IsNullOrEmpty()) {
+ problem.AddError("name", _localizer["Name is a required field"]);
+ }
+
+ var project = new Project(LoggedInUser) {
+ Name = request.Name,
+ Description = request.Description,
+ Start = request.Start,
+ Stop = request.Stop,
+ };
+
+ project.SetOwnerIds(default, LoggedInUser.TenantId);
+
+ foreach (var customerId in request.CustomerIds) {
+ var customer = _database.Customers.FirstOrDefault(c => c.Id == customerId);
+ if (customer == default) {
+ problem.AddError("customer_" + customerId, _localizer["Customer not found"]);
+ continue;
+ }
+
+ project.Customers.Add(customer);
+ }
+
+ foreach (var member in request.Members) {
+ var user = _database.Users.FirstOrDefault(c => c.Id == member.UserId);
+ if (user == default) {
+ problem.AddError("members_" + member.UserId, _localizer["User not found"]);
+ continue;
+ }
+
+ project.Members.Add(new ProjectMember() {
+ Project = project,
+ User = user,
+ Role = member.Role
+ });
+ }
+
+ if (problem.Errors.Any()) {
+ problem.Title = _localizer["Invalid form"];
+ problem.Subtitle = _localizer["One or more validation errors occured"];
+ return KnownProblem(problem);
+ }
+
+ _database.Projects.Add(project);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateProjectResponse
+{ }
+
+public class CreateProjectPayload
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public DateTime Start { get; set; }
+ public DateTime Stop { get; set; }
+ public List<Guid> CustomerIds { get; set; }
+ public List<ProjectMember> Members { get; set; }
+
+ public class ProjectMember
+ {
+ public Guid UserId { get; set; }
+ public ProjectRole Role { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
new file mode 100644
index 0000000..8fe70a6
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
@@ -0,0 +1,40 @@
+using MR.AspNetCore.Pagination;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class GetProjectsRoute : RouteBaseAsync.WithRequest<GetProjectsQueryParameters>.WithActionResult<KeysetPaginationResult<GetProjectsResponseDto>>
+{
+ private readonly MainAppDatabase _database;
+ private readonly PaginationService _pagination;
+
+ public GetProjectsRoute(MainAppDatabase database, PaginationService pagination) {
+ _database = database;
+ _pagination = pagination;
+ }
+
+ [HttpGet("~/v{version:apiVersion}/projects")]
+ public override async Task<ActionResult<KeysetPaginationResult<GetProjectsResponseDto>>> HandleAsync([FromQuery] GetProjectsQueryParameters request, CancellationToken cancellationToken = default) {
+ var result = await _pagination.KeysetPaginateAsync(
+ _database.Projects.ForTenant(LoggedInUser).ConditionalWhere(() => request.NameQuery.HasValue(), p => p.Name.Contains(request.NameQuery)),
+ b => b.Descending(x => x.CreatedAt),
+ async id => await _database.Projects.FindAsync(id),
+ query => query.Select(p => new GetProjectsResponseDto() {
+ Id = p.Id,
+ Name = p.Name
+ })
+ );
+ return Ok(result);
+ }
+}
+
+public class GetProjectsResponseDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+}
+
+public class GetProjectsQueryParameters
+{
+ [FromQuery(Name = "name")]
+ public string NameQuery { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/_calls.http b/code/api/src/Endpoints/V1/Projects/_calls.http
new file mode 100644
index 0000000..af0eba6
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/_calls.http
@@ -0,0 +1,12 @@
+### Create Project
+GET https://localhost:5001/v1/projects/create
+Accept: application/json
+Content-Type: application/json
+Cookie: ""
+
+{
+ "": ""
+}
+
+### Get Projects
+POST http://localhost
diff --git a/code/api/src/Endpoints/V1/RouteBaseAsync.cs b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
new file mode 100644
index 0000000..33b6f5f
--- /dev/null
+++ b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
@@ -0,0 +1,73 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public class RouteBaseAsync
+{
+ public class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : V1_EndpointBase
+ {
+ public abstract Task<TResponse> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : V1_EndpointBase
+ {
+ public abstract Task HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : V1_EndpointBase
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : V1_EndpointBase
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ TRequest request,
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : V1_EndpointBase
+ {
+ public abstract Task<TResponse> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithoutResult : V1_EndpointBase
+ {
+ public abstract Task HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult<TResponse> : V1_EndpointBase
+ {
+ public abstract Task<ActionResult<TResponse>> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+
+ public abstract class WithActionResult : V1_EndpointBase
+ {
+ public abstract Task<ActionResult> HandleAsync(
+ CancellationToken cancellationToken = default
+ );
+ }
+ }
+}
diff --git a/code/api/src/Endpoints/V1/RouteBaseSync.cs b/code/api/src/Endpoints/V1/RouteBaseSync.cs
new file mode 100644
index 0000000..6a86074
--- /dev/null
+++ b/code/api/src/Endpoints/V1/RouteBaseSync.cs
@@ -0,0 +1,53 @@
+namespace IOL.GreatOffice.Api.Endpoints.V1;
+
+/// <summary>
+/// A base class for an endpoint that accepts parameters.
+/// </summary>
+public static class RouteBaseSync
+{
+ public static class WithRequest<TRequest>
+ {
+ public abstract class WithResult<TResponse> : V1_EndpointBase
+ {
+ public abstract TResponse Handle(TRequest request);
+ }
+
+ public abstract class WithoutResult : V1_EndpointBase
+ {
+ public abstract void Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult<TResponse> : V1_EndpointBase
+ {
+ public abstract ActionResult<TResponse> Handle(TRequest request);
+ }
+
+ public abstract class WithActionResult : V1_EndpointBase
+ {
+ public abstract ActionResult Handle(TRequest request);
+ }
+ }
+
+ public static class WithoutRequest
+ {
+ public abstract class WithResult<TResponse> : V1_EndpointBase
+ {
+ public abstract TResponse Handle();
+ }
+
+ public abstract class WithoutResult : V1_EndpointBase
+ {
+ public abstract void Handle();
+ }
+
+ public abstract class WithActionResult<TResponse> : V1_EndpointBase
+ {
+ public abstract ActionResult<TResponse> Handle();
+ }
+
+ public abstract class WithActionResult : V1_EndpointBase
+ {
+ public abstract ActionResult Handle();
+ }
+ }
+}
diff --git a/code/api/src/Endpoints/V1/V1_EndpointBase.cs b/code/api/src/Endpoints/V1/V1_EndpointBase.cs
new file mode 100644
index 0000000..08ce4ab
--- /dev/null
+++ b/code/api/src/Endpoints/V1/V1_EndpointBase.cs
@@ -0,0 +1,29 @@
+using System.Net.Http.Headers;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1;
+
+[ApiVersion(ApiSpecV1.VERSION_STRING)]
+[Authorize(AuthenticationSchemes = AuthSchemes)]
+public class V1_EndpointBase : EndpointBase
+{
+ private const string AuthSchemes = CookieAuthenticationDefaults.AuthenticationScheme + "," + AppConstants.BASIC_AUTH_SCHEME;
+
+ protected bool IsApiCall() {
+ if (!Request.Headers.ContainsKey("Authorization")) return false;
+ try {
+ var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
+ if (authHeader.Parameter == null) return false;
+ } catch {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected bool HasApiPermission(string permission_key) {
+ var permission_claim = User.Claims.SingleOrDefault(c => c.Type == permission_key);
+ return permission_claim is {
+ Value: "True"
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/IOL.GreatOffice.Api.csproj b/code/api/src/IOL.GreatOffice.Api.csproj
new file mode 100644
index 0000000..15e5ebf
--- /dev/null
+++ b/code/api/src/IOL.GreatOffice.Api.csproj
@@ -0,0 +1,71 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <UserSecretsId>ed5ff3e5-46e2-4d7e-8272-7081f5abfee4</UserSecretsId>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <ImplicitUsings>true</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ <NoWarn>CS1591</NoWarn>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Duende.IdentityServer" Version="6.2.1" />
+ <PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="6.2.1" />
+ <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
+ <PackageReference Include="IOL.Helpers" Version="3.1.0" />
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="7.0.2" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="MR.AspNetCore.Pagination" Version="2.0.0" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" />
+ <PackageReference Include="Quartz.Extensions.Hosting" Version="3.5.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
+ <PackageReference Include="Serilog.Sinks.Seq" Version="5.2.2" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
+ <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
+ <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
+ </ItemGroup>
+
+ <ItemGroup Condition="'$(Configuration)' == 'Release'">
+ <Content Remove="AppData" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="..\build_and_push.sh">
+ <Link>build_and_push.sh</Link>
+ </Content>
+ <Content Include="..\Dockerfile">
+ <Link>Dockerfile</Link>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Update="Resources\SharedResources.en.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SharedResources.en.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\SharedResources.nb.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SharedResources.nb.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="Resources\SharedResources.en.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>SharedResource.en.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\SharedResources.nb.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>SharedResources.nb.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+
+</Project>
diff --git a/code/api/src/Jobs/AccessTokenCleanupJob.cs b/code/api/src/Jobs/AccessTokenCleanupJob.cs
new file mode 100644
index 0000000..20b450c
--- /dev/null
+++ b/code/api/src/Jobs/AccessTokenCleanupJob.cs
@@ -0,0 +1,20 @@
+namespace IOL.GreatOffice.Api.Jobs;
+
+public class AccessTokenCleanupJob : IJob
+{
+ private readonly ILogger<AccessTokenCleanupJob> _logger;
+ private readonly MainAppDatabase _context;
+
+ public AccessTokenCleanupJob(ILogger<AccessTokenCleanupJob> logger, MainAppDatabase context) {
+ _logger = logger;
+ _context = context;
+ }
+
+ public Task Execute(IJobExecutionContext context) {
+ var staleTokens = _context.AccessTokens.Where(c => c.ExpiryDate < AppDateTime.UtcNow).ToList();
+ if (staleTokens.IsNullOrEmpty()) return Task.CompletedTask;
+ _logger.LogInformation("Removing {0} stale tokens", staleTokens.Count());
+ _context.AccessTokens.RemoveRange(staleTokens);
+ return Task.CompletedTask;
+ }
+}
diff --git a/code/api/src/Jobs/JobRegister.cs b/code/api/src/Jobs/JobRegister.cs
new file mode 100644
index 0000000..1da7d5b
--- /dev/null
+++ b/code/api/src/Jobs/JobRegister.cs
@@ -0,0 +1,23 @@
+namespace IOL.GreatOffice.Api.Jobs;
+
+public static class JobRegister
+{
+ private static readonly JobKey AccessTokenCleanupKey = new("AccessTokenCleanupKey");
+ private static readonly JobKey VaultTokenRenewalKey = new("VaultTokenRenewalKey");
+
+ public static IServiceCollectionQuartzConfigurator RegisterJobs(this IServiceCollectionQuartzConfigurator configurator) {
+ configurator.AddJob<AccessTokenCleanupJob>(AccessTokenCleanupKey);
+ configurator.AddJob<VaultTokenRenewalJob>(VaultTokenRenewalKey);
+ configurator.AddTrigger(options => {
+ options.ForJob(AccessTokenCleanupKey)
+ .WithIdentity(AccessTokenCleanupKey.Name + "-trigger")
+ .WithCronSchedule("0 0 0/1 ? * * *");
+ });
+ configurator.AddTrigger(options => {
+ options.ForJob(VaultTokenRenewalKey)
+ .WithIdentity(VaultTokenRenewalKey.Name + "-trigger")
+ .WithCronSchedule("0 0 0/1 ? * * *");
+ });
+ return configurator;
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Jobs/VaultTokenRenewalJob.cs b/code/api/src/Jobs/VaultTokenRenewalJob.cs
new file mode 100644
index 0000000..1768629
--- /dev/null
+++ b/code/api/src/Jobs/VaultTokenRenewalJob.cs
@@ -0,0 +1,24 @@
+namespace IOL.GreatOffice.Api.Jobs;
+
+public class VaultTokenRenewalJob : IJob
+{
+ private readonly ILogger<VaultTokenRenewalJob> _logger;
+ private readonly VaultService _vaultService;
+
+ public VaultTokenRenewalJob(ILogger<VaultTokenRenewalJob> logger, VaultService vaultService) {
+ _logger = logger;
+ _vaultService = vaultService;
+ }
+
+ public async Task Execute(IJobExecutionContext context) {
+ _logger.LogInformation("Starting vault token renewal");
+ var renew = await _vaultService.RenewTokenAsync();
+ if (renew == default) {
+ _logger.LogCritical("Renewal did not succeed");
+ return;
+ }
+
+ var token = await _vaultService.LookupTokenAsync();
+ _logger.LogInformation("Token was renewed, new expire time {expires}", token.Data.ExpireTime);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs b/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs
new file mode 100644
index 0000000..da3c3ec
--- /dev/null
+++ b/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs
@@ -0,0 +1,238 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20210517202115_InitialMigration")]
+ partial class InitialMigration
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Relational:MaxIdentifierLength", 63)
+ .HasAnnotation("ProductVersion", "5.0.6")
+ .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"),
+ Created = new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868),
+ Password = "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==",
+ Username = "admin@ivarlovlie.no"
+ });
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20210517202115_InitialMigration.cs b/code/api/src/Migrations/20210517202115_InitialMigration.cs
new file mode 100644
index 0000000..8bfaf61
--- /dev/null
+++ b/code/api/src/Migrations/20210517202115_InitialMigration.cs
@@ -0,0 +1,162 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class InitialMigration : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "time_categories",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: false),
+ created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_categories", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "time_labels",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: false),
+ created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_labels", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "users",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ username = table.Column<string>(type: "text", nullable: true),
+ password = table.Column<string>(type: "text", nullable: true),
+ created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_users", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "time_entries",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ start = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ stop = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ note = table.Column<string>(type: "text", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: false),
+ category_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_entries", x => x.id);
+ table.ForeignKey(
+ name: "fk_time_entries_time_categories_category_id",
+ column: x => x.category_id,
+ principalTable: "time_categories",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "forgot_password_requests",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_forgot_password_requests", x => x.id);
+ table.ForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "time_entry_time_label",
+ columns: table => new
+ {
+ entries_id = table.Column<Guid>(type: "uuid", nullable: false),
+ labels_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_entry_time_label", x => new { x.entries_id, x.labels_id });
+ table.ForeignKey(
+ name: "fk_time_entry_time_label_time_entries_entries_id",
+ column: x => x.entries_id,
+ principalTable: "time_entries",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_time_entry_time_label_time_labels_labels_id",
+ column: x => x.labels_id,
+ principalTable: "time_labels",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "users",
+ columns: new[] { "id", "created", "password", "username" },
+ values: new object[] { new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"), new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868), "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==", "admin@ivarlovlie.no" });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_forgot_password_requests_user_id",
+ table: "forgot_password_requests",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_category_id",
+ table: "time_entries",
+ column: "category_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entry_time_label_labels_id",
+ table: "time_entry_time_label",
+ column: "labels_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "forgot_password_requests");
+
+ migrationBuilder.DropTable(
+ name: "time_entry_time_label");
+
+ migrationBuilder.DropTable(
+ name: "users");
+
+ migrationBuilder.DropTable(
+ name: "time_entries");
+
+ migrationBuilder.DropTable(
+ name: "time_labels");
+
+ migrationBuilder.DropTable(
+ name: "time_categories");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs
new file mode 100644
index 0000000..28408c7
--- /dev/null
+++ b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs
@@ -0,0 +1,229 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20210522165932_RenameNoteToDescription")]
+ partial class RenameNoteToDescription
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("Relational:MaxIdentifierLength", 63)
+ .HasAnnotation("ProductVersion", "5.0.6")
+ .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("created");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp without time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20210522165932_RenameNoteToDescription.cs b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.cs
new file mode 100644
index 0000000..e5bae54
--- /dev/null
+++ b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class RenameNoteToDescription : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DeleteData(
+ table: "users",
+ keyColumn: "id",
+ keyValue: new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"));
+
+ migrationBuilder.RenameColumn(
+ name: "note",
+ table: "time_entries",
+ newName: "description");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "description",
+ table: "time_entries",
+ newName: "note");
+
+ migrationBuilder.InsertData(
+ table: "users",
+ columns: new[] { "id", "created", "password", "username" },
+ values: new object[] { new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"), new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868), "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==", "admin@ivarlovlie.no" });
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs b/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs
new file mode 100644
index 0000000..50f461e
--- /dev/null
+++ b/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs
@@ -0,0 +1,233 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20211002113037_V6Migration")]
+ partial class V6Migration
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.0-rc.1.21452.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20211002113037_V6Migration.cs b/code/api/src/Migrations/20211002113037_V6Migration.cs
new file mode 100644
index 0000000..c7ac971
--- /dev/null
+++ b/code/api/src/Migrations/20211002113037_V6Migration.cs
@@ -0,0 +1,130 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class V6Migration : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("SET TimeZone='UTC'");
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_labels",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "stop",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "start",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_categories",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "forgot_password_requests",
+ type: "timestamp with time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp without time zone");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("SET TimeZone='UTC'");
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "users",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_labels",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "stop",
+ table: "time_entries",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "start",
+ table: "time_entries",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_entries",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "time_categories",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "created",
+ table: "forgot_password_requests",
+ type: "timestamp without time zone",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "timestamp with time zone");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs b/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs
new file mode 100644
index 0000000..6c59262
--- /dev/null
+++ b/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs
@@ -0,0 +1,270 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220225143559_GithubUserMappings")]
+ partial class GithubUserMappings
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220225143559_GithubUserMappings.cs b/code/api/src/Migrations/20220225143559_GithubUserMappings.cs
new file mode 100644
index 0000000..fc30c7a
--- /dev/null
+++ b/code/api/src/Migrations/20220225143559_GithubUserMappings.cs
@@ -0,0 +1,43 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class GithubUserMappings : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "github_user_mappings",
+ columns: table => new
+ {
+ github_id = table.Column<string>(type: "text", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ refresh_token = table.Column<string>(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_github_user_mappings", x => x.github_id);
+ table.ForeignKey(
+ name: "fk_github_user_mappings_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_github_user_mappings_user_id",
+ table: "github_user_mappings",
+ column: "user_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "github_user_mappings");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs b/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs
new file mode 100644
index 0000000..2f96cdc
--- /dev/null
+++ b/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs
@@ -0,0 +1,270 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220319135910_RenameCreated")]
+ partial class RenameCreated
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319135910_RenameCreated.cs b/code/api/src/Migrations/20220319135910_RenameCreated.cs
new file mode 100644
index 0000000..6571e50
--- /dev/null
+++ b/code/api/src/Migrations/20220319135910_RenameCreated.cs
@@ -0,0 +1,65 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class RenameCreated : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "created",
+ table: "users",
+ newName: "created_at");
+
+ migrationBuilder.RenameColumn(
+ name: "created",
+ table: "time_labels",
+ newName: "created_at");
+
+ migrationBuilder.RenameColumn(
+ name: "created",
+ table: "time_entries",
+ newName: "created_at");
+
+ migrationBuilder.RenameColumn(
+ name: "created",
+ table: "time_categories",
+ newName: "created_at");
+
+ migrationBuilder.RenameColumn(
+ name: "created",
+ table: "forgot_password_requests",
+ newName: "created_at");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ table: "users",
+ newName: "created");
+
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ table: "time_labels",
+ newName: "created");
+
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ table: "time_entries",
+ newName: "created");
+
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ table: "time_categories",
+ newName: "created");
+
+ migrationBuilder.RenameColumn(
+ name: "created_at",
+ table: "forgot_password_requests",
+ newName: "created");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs b/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs
new file mode 100644
index 0000000..349c4ad
--- /dev/null
+++ b/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs
@@ -0,0 +1,290 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220319144958_ModifiedAt")]
+ partial class ModifiedAt
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319144958_ModifiedAt.cs b/code/api/src/Migrations/20220319144958_ModifiedAt.cs
new file mode 100644
index 0000000..028473d
--- /dev/null
+++ b/code/api/src/Migrations/20220319144958_ModifiedAt.cs
@@ -0,0 +1,66 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class ModifiedAt : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "time_labels",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "time_categories",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "forgot_password_requests",
+ type: "timestamp with time zone",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "forgot_password_requests");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319203018_UserBase.Designer.cs b/code/api/src/Migrations/20220319203018_UserBase.Designer.cs
new file mode 100644
index 0000000..c918d3d
--- /dev/null
+++ b/code/api/src/Migrations/20220319203018_UserBase.Designer.cs
@@ -0,0 +1,322 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220319203018_UserBase")]
+ partial class UserBase
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.Property<string>("password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220319203018_UserBase.cs b/code/api/src/Migrations/20220319203018_UserBase.cs
new file mode 100644
index 0000000..14d3f4b
--- /dev/null
+++ b/code/api/src/Migrations/20220319203018_UserBase.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class UserBase : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "modified_at",
+ table: "forgot_password_requests");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories",
+ column: "user_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "modified_at",
+ table: "forgot_password_requests",
+ type: "timestamp with time zone",
+ nullable: true);
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220320115601_Update1.Designer.cs b/code/api/src/Migrations/20220320115601_Update1.Designer.cs
new file mode 100644
index 0000000..3b6a63a
--- /dev/null
+++ b/code/api/src/Migrations/20220320115601_Update1.Designer.cs
@@ -0,0 +1,342 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220320115601_Update1")]
+ partial class Update1
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.Property<string>("password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Categories")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany("Entries")
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Entries")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Labels")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Navigation("Entries");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Navigation("Categories");
+
+ b.Navigation("Entries");
+
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220320115601_Update1.cs b/code/api/src/Migrations/20220320115601_Update1.cs
new file mode 100644
index 0000000..8b06fb7
--- /dev/null
+++ b/code/api/src/Migrations/20220320115601_Update1.cs
@@ -0,0 +1,139 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class Update1 : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs
new file mode 100644
index 0000000..8dae7c8
--- /dev/null
+++ b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs
@@ -0,0 +1,344 @@
+// <auto-generated />
+
+
+#nullable disable
+
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220320132220_UpdatedForgotPasswordRequests")]
+ partial class UpdatedForgotPasswordRequests
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Categories")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category")
+ .WithMany("Entries")
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Entries")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.User", "User")
+ .WithMany("Labels")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b =>
+ {
+ b.Navigation("Entries");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b =>
+ {
+ b.Navigation("Categories");
+
+ b.Navigation("Entries");
+
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs
new file mode 100644
index 0000000..df7a195
--- /dev/null
+++ b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs
@@ -0,0 +1,57 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class UpdatedForgotPasswordRequests : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "forgot_password_requests",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "forgot_password_requests",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs b/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs
new file mode 100644
index 0000000..3dc01a7
--- /dev/null
+++ b/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs
@@ -0,0 +1,401 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220529190359_ApiAccessTokens")]
+ partial class ApiAccessTokens
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany("Categories")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany("Entries")
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany("Entries")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany("Labels")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Navigation("Entries");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Categories");
+
+ b.Navigation("Entries");
+
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220529190359_ApiAccessTokens.cs b/code/api/src/Migrations/20220529190359_ApiAccessTokens.cs
new file mode 100644
index 0000000..dc44bee
--- /dev/null
+++ b/code/api/src/Migrations/20220529190359_ApiAccessTokens.cs
@@ -0,0 +1,48 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class ApiAccessTokens : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "api_access_tokens",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ expiry_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ allow_read = table.Column<bool>(type: "boolean", nullable: false),
+ allow_create = table.Column<bool>(type: "boolean", nullable: false),
+ allow_update = table.Column<bool>(type: "boolean", nullable: false),
+ allow_delete = table.Column<bool>(type: "boolean", nullable: false),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_api_access_tokens", x => x.id);
+ table.ForeignKey(
+ name: "fk_api_access_tokens_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_api_access_tokens_user_id",
+ table: "api_access_tokens",
+ column: "user_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "api_access_tokens");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220530174741_Tenants.Designer.cs b/code/api/src/Migrations/20220530174741_Tenants.Designer.cs
new file mode 100644
index 0000000..aadbec9
--- /dev/null
+++ b/code/api/src/Migrations/20220530174741_Tenants.Designer.cs
@@ -0,0 +1,710 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220530174741_Tenants")]
+ partial class Tenants
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TenantId1")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id1");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_tenants_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_tenants_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_tenants_modified_by_id");
+
+ b.HasIndex("TenantId1")
+ .HasDatabaseName("ix_tenants_tenant_id1");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_categories_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_categories_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_categories_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_categories_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_entries_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_entries_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_entries_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_entries_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_labels_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_labels_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_labels_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.Property<Guid>("EntriesId")
+ .HasColumnType("uuid")
+ .HasColumnName("entries_id");
+
+ b.Property<Guid>("LabelsId")
+ .HasColumnType("uuid")
+ .HasColumnName("labels_id");
+
+ b.HasKey("EntriesId", "LabelsId")
+ .HasName("pk_time_entry_time_label");
+
+ b.HasIndex("LabelsId")
+ .HasDatabaseName("ix_time_entry_time_label_labels_id");
+
+ b.ToTable("time_entry_time_label", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId1")
+ .HasConstraintName("fk_tenants_tenants_tenant_id1");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany("Entries")
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("TimeEntryTimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany()
+ .HasForeignKey("EntriesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeLabel", null)
+ .WithMany()
+ .HasForeignKey("LabelsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Navigation("Entries");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220530174741_Tenants.cs b/code/api/src/Migrations/20220530174741_Tenants.cs
new file mode 100644
index 0000000..ea02ddd
--- /dev/null
+++ b/code/api/src/Migrations/20220530174741_Tenants.cs
@@ -0,0 +1,481 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class Tenants : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<Guid>(
+ name: "created_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "tenant_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "created_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "tenant_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "created_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "tenant_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.CreateTable(
+ name: "tenants",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ contact_email = table.Column<string>(type: "text", nullable: true),
+ master_user_id = table.Column<Guid>(type: "uuid", nullable: false),
+ master_user_password = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: false),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
+ tenant_id1 = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: false),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: false),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_tenants", x => x.id);
+ table.ForeignKey(
+ name: "fk_tenants_tenants_tenant_id1",
+ column: x => x.tenant_id1,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ column: x => x.created_by_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ column: x => x.deleted_by_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ column: x => x.modified_by_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_tenants_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_created_by_id",
+ table: "time_labels",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_created_by_id",
+ table: "time_entries",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_created_by_id",
+ table: "time_categories",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_created_by_id",
+ table: "tenants",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_deleted_by_id",
+ table: "tenants",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_modified_by_id",
+ table: "tenants",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_tenant_id1",
+ table: "tenants",
+ column: "tenant_id1");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_user_id",
+ table: "tenants",
+ column: "user_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropTable(
+ name: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ table: "time_categories");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs
new file mode 100644
index 0000000..f87309f
--- /dev/null
+++ b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs
@@ -0,0 +1,686 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220530175322_RemoveUnusedNavs")]
+ partial class RemoveUnusedNavs
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TenantId1")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id1");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_tenants_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_tenants_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_tenants_modified_by_id");
+
+ b.HasIndex("TenantId1")
+ .HasDatabaseName("ix_tenants_tenant_id1");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_categories_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_categories_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_categories_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_categories_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_entries_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_entries_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_entries_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_entries_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_labels_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_labels_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_labels_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_labels_tenant_id");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId1")
+ .HasConstraintName("fk_tenants_tenants_tenant_id1");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.cs b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.cs
new file mode 100644
index 0000000..36b3cf1
--- /dev/null
+++ b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.cs
@@ -0,0 +1,78 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class RemoveUnusedNavs : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "time_entry_time_label");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "time_entry_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_time_entry_id",
+ table: "time_labels",
+ column: "time_entry_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_time_entries_time_entry_id",
+ table: "time_labels",
+ column: "time_entry_id",
+ principalTable: "time_entries",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_time_entries_time_entry_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_time_entry_id",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "time_entry_id",
+ table: "time_labels");
+
+ migrationBuilder.CreateTable(
+ name: "time_entry_time_label",
+ columns: table => new
+ {
+ entries_id = table.Column<Guid>(type: "uuid", nullable: false),
+ labels_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_entry_time_label", x => new { x.entries_id, x.labels_id });
+ table.ForeignKey(
+ name: "fk_time_entry_time_label_time_entries_entries_id",
+ column: x => x.entries_id,
+ principalTable: "time_entries",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_time_entry_time_label_time_labels_labels_id",
+ column: x => x.labels_id,
+ principalTable: "time_labels",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entry_time_label_labels_id",
+ table: "time_entry_time_label",
+ column: "labels_id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs
new file mode 100644
index 0000000..ddbe9d2
--- /dev/null
+++ b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs
@@ -0,0 +1,656 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220602214238_NullableOptionalBaseFields")]
+ partial class NullableOptionalBaseFields
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TenantId1")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id1");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_tenants_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_tenants_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_tenants_modified_by_id");
+
+ b.HasIndex("TenantId1")
+ .HasDatabaseName("ix_tenants_tenant_id1");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_categories_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_categories_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_categories_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_categories_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_entries_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_entries_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_entries_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_entries_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("CreatedById")
+ .HasDatabaseName("ix_time_labels_created_by_id");
+
+ b.HasIndex("DeletedById")
+ .HasDatabaseName("ix_time_labels_deleted_by_id");
+
+ b.HasIndex("ModifiedById")
+ .HasDatabaseName("ix_time_labels_modified_by_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_labels_tenant_id");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .HasConstraintName("fk_tenants_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .HasConstraintName("fk_tenants_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .HasConstraintName("fk_tenants_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId1")
+ .HasConstraintName("fk_tenants_tenants_tenant_id1");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .HasConstraintName("fk_time_categories_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .HasConstraintName("fk_time_categories_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .HasConstraintName("fk_time_categories_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_categories_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .HasConstraintName("fk_time_entries_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .HasConstraintName("fk_time_entries_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .HasConstraintName("fk_time_entries_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_entries_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .HasConstraintName("fk_time_labels_users_created_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy")
+ .WithMany()
+ .HasForeignKey("DeletedById")
+ .HasConstraintName("fk_time_labels_users_deleted_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .HasConstraintName("fk_time_labels_users_modified_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Tenant");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.cs b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.cs
new file mode 100644
index 0000000..eebab5c
--- /dev/null
+++ b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.cs
@@ -0,0 +1,649 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class NullableOptionalBaseFields : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "tenant_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "modified_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "deleted_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "created_by_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs b/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs
new file mode 100644
index 0000000..5c4d3a3
--- /dev/null
+++ b/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs
@@ -0,0 +1,510 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220606232346_FleshOutNewModules")]
+ partial class FleshOutNewModules
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.5")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220606232346_FleshOutNewModules.cs b/code/api/src/Migrations/20220606232346_FleshOutNewModules.cs
new file mode 100644
index 0000000..49a36b8
--- /dev/null
+++ b/code/api/src/Migrations/20220606232346_FleshOutNewModules.cs
@@ -0,0 +1,630 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class FleshOutNewModules : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_tenants_tenant_id1",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_user_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_created_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_deleted_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_modified_by_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_created_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_deleted_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_modified_by_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_created_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_deleted_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_modified_by_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_created_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_deleted_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_modified_by_id",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_tenant_id1",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_user_id",
+ table: "tenants");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id1",
+ table: "tenants");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn<string>(
+ name: "email",
+ table: "users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "first_name",
+ table: "users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn<string>(
+ name: "last_name",
+ table: "users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "time_labels",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "time_entries",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "time_categories",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "tenants",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn<bool>(
+ name: "deleted",
+ table: "api_access_tokens",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.CreateTable(
+ name: "tenant_user",
+ columns: table => new
+ {
+ tenants_id = table.Column<Guid>(type: "uuid", nullable: false),
+ users_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_tenant_user", x => new { x.tenants_id, x.users_id });
+ table.ForeignKey(
+ name: "fk_tenant_user_tenants_tenants_id",
+ column: x => x.tenants_id,
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_tenant_user_users_users_id",
+ column: x => x.users_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenant_user_users_id",
+ table: "tenant_user",
+ column: "users_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "tenant_user");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "email",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "first_name",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "last_name",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "tenants");
+
+ migrationBuilder.DropColumn(
+ name: "deleted",
+ table: "api_access_tokens");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_labels",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_entries",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "time_categories",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "user_id",
+ table: "tenants",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "uuid",
+ oldNullable: true);
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "tenant_id1",
+ table: "tenants",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_created_by_id",
+ table: "time_labels",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_created_by_id",
+ table: "time_entries",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_created_by_id",
+ table: "time_categories",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_created_by_id",
+ table: "tenants",
+ column: "created_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_deleted_by_id",
+ table: "tenants",
+ column: "deleted_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_modified_by_id",
+ table: "tenants",
+ column: "modified_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_tenant_id1",
+ table: "tenants",
+ column: "tenant_id1");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_user_id",
+ table: "tenants",
+ column: "user_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_tenants_tenant_id1",
+ table: "tenants",
+ column: "tenant_id1",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_created_by_id",
+ table: "tenants",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_deleted_by_id",
+ table: "tenants",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_modified_by_id",
+ table: "tenants",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_user_id",
+ table: "tenants",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_created_by_id",
+ table: "time_categories",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_deleted_by_id",
+ table: "time_categories",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_modified_by_id",
+ table: "time_categories",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_created_by_id",
+ table: "time_entries",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_deleted_by_id",
+ table: "time_entries",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_modified_by_id",
+ table: "time_entries",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_created_by_id",
+ table: "time_labels",
+ column: "created_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_deleted_by_id",
+ table: "time_labels",
+ column: "deleted_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_modified_by_id",
+ table: "time_labels",
+ column: "modified_by_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs b/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs
new file mode 100644
index 0000000..b89e68d
--- /dev/null
+++ b/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs
@@ -0,0 +1,533 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220616170311_DataProtectionKeys")]
+ partial class DataProtectionKeys
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.Property<string>("GithubId")
+ .HasColumnType("text")
+ .HasColumnName("github_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("RefreshToken")
+ .HasColumnType("text")
+ .HasColumnName("refresh_token");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("GithubId")
+ .HasName("pk_github_user_mappings");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_github_user_mappings_user_id");
+
+ b.ToTable("github_user_mappings", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_github_user_mappings_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220616170311_DataProtectionKeys.cs b/code/api/src/Migrations/20220616170311_DataProtectionKeys.cs
new file mode 100644
index 0000000..bc3c673
--- /dev/null
+++ b/code/api/src/Migrations/20220616170311_DataProtectionKeys.cs
@@ -0,0 +1,33 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class DataProtectionKeys : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "data_protection_keys",
+ columns: table => new
+ {
+ id = table.Column<int>(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ friendly_name = table.Column<string>(type: "text", nullable: true),
+ xml = table.Column<string>(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_data_protection_keys", x => x.id);
+ });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "data_protection_keys");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs
new file mode 100644
index 0000000..0ba742c
--- /dev/null
+++ b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs
@@ -0,0 +1,496 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20220819203816_RemoveGithubUsers")]
+ partial class RemoveGithubUsers
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20220819203816_RemoveGithubUsers.cs b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.cs
new file mode 100644
index 0000000..d301f67
--- /dev/null
+++ b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.cs
@@ -0,0 +1,43 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class RemoveGithubUsers : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "github_user_mappings");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "github_user_mappings",
+ columns: table => new
+ {
+ github_id = table.Column<string>(type: "text", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ refresh_token = table.Column<string>(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_github_user_mappings", x => x.github_id);
+ table.ForeignKey(
+ name: "fk_github_user_mappings_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_github_user_mappings_user_id",
+ table: "github_user_mappings",
+ column: "user_id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs
new file mode 100644
index 0000000..284ea89
--- /dev/null
+++ b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs
@@ -0,0 +1,1074 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030080515_InitialProjectAndCustomer")]
+ partial class InitialProjectAndCustomer
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_member");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_member_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_member_user_id");
+
+ b.ToTable("project_member", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_member_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_member_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs
new file mode 100644
index 0000000..8a856cb
--- /dev/null
+++ b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs
@@ -0,0 +1,304 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class InitialProjectAndCustomer : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "customer_groups",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_groups", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "projects",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ start = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ stop = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_projects", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customers",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_number = table.Column<string>(type: "text", nullable: true),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ address1 = table.Column<string>(type: "text", nullable: true),
+ address2 = table.Column<string>(type: "text", nullable: true),
+ postal_code = table.Column<string>(type: "text", nullable: true),
+ postal_city = table.Column<string>(type: "text", nullable: true),
+ country = table.Column<string>(type: "text", nullable: true),
+ phone = table.Column<string>(type: "text", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ vat_number = table.Column<string>(type: "text", nullable: true),
+ org_number = table.Column<string>(type: "text", nullable: true),
+ default_reference = table.Column<string>(type: "text", nullable: true),
+ website = table.Column<string>(type: "text", nullable: true),
+ currency = table.Column<string>(type: "text", nullable: true),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customers", x => x.id);
+ table.ForeignKey(
+ name: "fk_customers_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_customers_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "project_labels",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ value = table.Column<string>(type: "text", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_project_labels", x => x.id);
+ table.ForeignKey(
+ name: "fk_project_labels_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "project_member",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ role = table.Column<int>(type: "integer", nullable: false),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_project_member", x => x.id);
+ table.ForeignKey(
+ name: "fk_project_member_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_project_member_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_contacts",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_id = table.Column<Guid>(type: "uuid", nullable: true),
+ first_name = table.Column<string>(type: "text", nullable: true),
+ last_name = table.Column<string>(type: "text", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ phone = table.Column<string>(type: "text", nullable: true),
+ work_title = table.Column<string>(type: "text", nullable: true),
+ note = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_contacts", x => x.id);
+ table.ForeignKey(
+ name: "fk_customer_contacts_customers_customer_id",
+ column: x => x.customer_id,
+ principalTable: "customers",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_customer_group",
+ columns: table => new
+ {
+ customers_id = table.Column<Guid>(type: "uuid", nullable: false),
+ groups_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_customer_group", x => new { x.customers_id, x.groups_id });
+ table.ForeignKey(
+ name: "fk_customer_customer_group_customer_groups_groups_id",
+ column: x => x.groups_id,
+ principalTable: "customer_groups",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_customer_customer_group_customers_customers_id",
+ column: x => x.customers_id,
+ principalTable: "customers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_events",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_id = table.Column<Guid>(type: "uuid", nullable: true),
+ title = table.Column<string>(type: "text", nullable: true),
+ note = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_events", x => x.id);
+ table.ForeignKey(
+ name: "fk_customer_events_customers_customer_id",
+ column: x => x.customer_id,
+ principalTable: "customers",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_contacts_customer_id",
+ table: "customer_contacts",
+ column: "customer_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_customer_group_groups_id",
+ table: "customer_customer_group",
+ column: "groups_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_events_customer_id",
+ table: "customer_events",
+ column: "customer_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_project_id",
+ table: "customers",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_user_id",
+ table: "customers",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_labels_project_id",
+ table: "project_labels",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_member_project_id",
+ table: "project_member",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_member_user_id",
+ table: "project_member",
+ column: "user_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "customer_contacts");
+
+ migrationBuilder.DropTable(
+ name: "customer_customer_group");
+
+ migrationBuilder.DropTable(
+ name: "customer_events");
+
+ migrationBuilder.DropTable(
+ name: "project_labels");
+
+ migrationBuilder.DropTable(
+ name: "project_member");
+
+ migrationBuilder.DropTable(
+ name: "customer_groups");
+
+ migrationBuilder.DropTable(
+ name: "customers");
+
+ migrationBuilder.DropTable(
+ name: "projects");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs b/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs
new file mode 100644
index 0000000..78f9454
--- /dev/null
+++ b/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs
@@ -0,0 +1,1126 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030081459_DeletedAt")]
+ partial class DeletedAt
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_member");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_member_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_member_user_id");
+
+ b.ToTable("project_member", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_member_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_member_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030081459_DeletedAt.cs b/code/api/src/Migrations/20221030081459_DeletedAt.cs
new file mode 100644
index 0000000..8132a6b
--- /dev/null
+++ b/code/api/src/Migrations/20221030081459_DeletedAt.cs
@@ -0,0 +1,146 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class DeletedAt : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_labels",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_categories",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "tenants",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "projects",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "project_member",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "project_labels",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customers",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_groups",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_events",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_contacts",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "api_access_tokens",
+ type: "timestamp with time zone",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "tenants");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "projects");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "project_member");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "project_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customers");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_groups");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_events");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_contacts");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "api_access_tokens");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs b/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs
new file mode 100644
index 0000000..1527d91
--- /dev/null
+++ b/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs
@@ -0,0 +1,1126 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030084716_MinorChanges")]
+ partial class MinorChanges
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030084716_MinorChanges.cs b/code/api/src/Migrations/20221030084716_MinorChanges.cs
new file mode 100644
index 0000000..e708caf
--- /dev/null
+++ b/code/api/src/Migrations/20221030084716_MinorChanges.cs
@@ -0,0 +1,105 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class MinorChanges : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_member_projects_project_id",
+ table: "project_member");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_member_users_user_id",
+ table: "project_member");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_project_member",
+ table: "project_member");
+
+ migrationBuilder.RenameTable(
+ name: "project_member",
+ newName: "project_members");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_member_user_id",
+ table: "project_members",
+ newName: "ix_project_members_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_member_project_id",
+ table: "project_members",
+ newName: "ix_project_members_project_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_project_members",
+ table: "project_members",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_members_projects_project_id",
+ table: "project_members",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_members_users_user_id",
+ table: "project_members",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_members_projects_project_id",
+ table: "project_members");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_members_users_user_id",
+ table: "project_members");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_project_members",
+ table: "project_members");
+
+ migrationBuilder.RenameTable(
+ name: "project_members",
+ newName: "project_member");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_members_user_id",
+ table: "project_member",
+ newName: "ix_project_member_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_members_project_id",
+ table: "project_member",
+ newName: "ix_project_member_project_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_project_member",
+ table: "project_member",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_member_projects_project_id",
+ table: "project_member",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_member_users_user_id",
+ table: "project_member",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs b/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs
new file mode 100644
index 0000000..ff1dfb2
--- /dev/null
+++ b/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs
@@ -0,0 +1,1148 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030090557_MoreMinorChanges")]
+ partial class MoreMinorChanges
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs b/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs
new file mode 100644
index 0000000..5ebd664
--- /dev/null
+++ b/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs
@@ -0,0 +1,78 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class MoreMinorChanges : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_customers_projects_project_id",
+ table: "customers");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customers_project_id",
+ table: "customers");
+
+ migrationBuilder.DropColumn(
+ name: "project_id",
+ table: "customers");
+
+ migrationBuilder.CreateTable(
+ name: "customer_project",
+ columns: table => new
+ {
+ customers_id = table.Column<Guid>(type: "uuid", nullable: false),
+ projects_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_project", x => new { x.customers_id, x.projects_id });
+ table.ForeignKey(
+ name: "fk_customer_project_customers_customers_id",
+ column: x => x.customers_id,
+ principalTable: "customers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_customer_project_projects_projects_id",
+ column: x => x.projects_id,
+ principalTable: "projects",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_project_projects_id",
+ table: "customer_project",
+ column: "projects_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "customer_project");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "project_id",
+ table: "customers",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_project_id",
+ table: "customers",
+ column: "project_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customers_projects_project_id",
+ table: "customers",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.Designer.cs b/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.Designer.cs
new file mode 100644
index 0000000..1909d36
--- /dev/null
+++ b/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.Designer.cs
@@ -0,0 +1,1863 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221031165813_TodoAndOwnerNavigations")]
+ partial class TodoAndOwnerNavigations
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_categories_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_categories_user_id");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_entries_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_entries_user_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_time_labels_tenant_id");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_time_labels_user_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_categories_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_categories_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_entries_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_entries_users_user_id");
+
+ b.Navigation("Category");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_time_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_time_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.cs b/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.cs
new file mode 100644
index 0000000..334913e
--- /dev/null
+++ b/code/api/src/Migrations/20221031165813_TodoAndOwnerNavigations.cs
@@ -0,0 +1,1092 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class TodoAndOwnerNavigations : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "tenant_user");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "time_labels",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "time_labels",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "time_labels",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "time_entries",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "time_entries",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "time_entries",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "time_categories",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "time_categories",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "time_categories",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "tenants",
+ newName: "owning_tenant_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "tenants",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "tenants",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "projects",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "projects",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "projects",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "project_labels",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "project_labels",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "project_labels",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "customers",
+ newName: "owner_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "customers",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "customers",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "customer_groups",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "customer_groups",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "customer_groups",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "customer_events",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "customer_events",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "customer_events",
+ newName: "created_by");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by_id",
+ table: "customer_contacts",
+ newName: "modified_by");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by_id",
+ table: "customer_contacts",
+ newName: "deleted_by");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by_id",
+ table: "customer_contacts",
+ newName: "created_by");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "tenant_id",
+ table: "users",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "created_by",
+ table: "tenants",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "created_by",
+ table: "customers",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "todo_collections",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ visibility = table.Column<int>(type: "integer", nullable: false),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_todo_collections", x => x.id);
+ table.ForeignKey(
+ name: "fk_todo_collections_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_collections_tenants_tenant_id",
+ column: x => x.tenant_id,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_collections_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "todo_collection_access_controls",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ collection_id = table.Column<Guid>(type: "uuid", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ can_browse = table.Column<bool>(type: "boolean", nullable: false),
+ can_submit = table.Column<bool>(type: "boolean", nullable: false),
+ can_comment = table.Column<bool>(type: "boolean", nullable: false),
+ can_edit = table.Column<bool>(type: "boolean", nullable: false),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_todo_collection_access_controls", x => x.id);
+ table.ForeignKey(
+ name: "fk_todo_collection_access_controls_todo_collections_collection",
+ column: x => x.collection_id,
+ principalTable: "todo_collections",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_collection_access_controls_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "todos",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ public_id = table.Column<string>(type: "text", nullable: true),
+ assigned_to_id = table.Column<Guid>(type: "uuid", nullable: true),
+ closed_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ collection_id = table.Column<Guid>(type: "uuid", nullable: false),
+ closed_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ title = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_todos", x => x.id);
+ table.ForeignKey(
+ name: "fk_todos_tenants_tenant_id",
+ column: x => x.tenant_id,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todos_todo_projects_collection_id",
+ column: x => x.collection_id,
+ principalTable: "todo_collections",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_todos_users_assigned_to_id",
+ column: x => x.assigned_to_id,
+ principalTable: "users",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todos_users_closed_by_id",
+ column: x => x.closed_by_id,
+ principalTable: "users",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todos_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "todo_comments",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ value = table.Column<string>(type: "text", nullable: true),
+ todo_id = table.Column<Guid>(type: "uuid", nullable: true),
+ closing_statement = table.Column<int>(type: "integer", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_todo_comments", x => x.id);
+ table.ForeignKey(
+ name: "fk_todo_comments_tenants_tenant_id",
+ column: x => x.tenant_id,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_comments_todos_todo_id",
+ column: x => x.todo_id,
+ principalTable: "todos",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_comments_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "todo_labels",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ todo_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_todo_labels", x => x.id);
+ table.ForeignKey(
+ name: "fk_todo_labels_tenants_tenant_id",
+ column: x => x.tenant_id,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_labels_todos_todo_id",
+ column: x => x.todo_id,
+ principalTable: "todos",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_todo_labels_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_users_tenant_id",
+ table: "users",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_owning_tenant_id",
+ table: "tenants",
+ column: "owning_tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenants_user_id",
+ table: "tenants",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_projects_tenant_id",
+ table: "projects",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_projects_user_id",
+ table: "projects",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_labels_tenant_id",
+ table: "project_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_labels_user_id",
+ table: "project_labels",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_owner_id",
+ table: "customers",
+ column: "owner_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_tenant_id",
+ table: "customers",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_groups_tenant_id",
+ table: "customer_groups",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_groups_user_id",
+ table: "customer_groups",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_events_tenant_id",
+ table: "customer_events",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_events_user_id",
+ table: "customer_events",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_contacts_tenant_id",
+ table: "customer_contacts",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_contacts_user_id",
+ table: "customer_contacts",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_collection_access_controls_collection_id",
+ table: "todo_collection_access_controls",
+ column: "collection_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_collection_access_controls_user_id",
+ table: "todo_collection_access_controls",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_collections_project_id",
+ table: "todo_collections",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_collections_tenant_id",
+ table: "todo_collections",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_collections_user_id",
+ table: "todo_collections",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_comments_tenant_id",
+ table: "todo_comments",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_comments_todo_id",
+ table: "todo_comments",
+ column: "todo_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_comments_user_id",
+ table: "todo_comments",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_labels_tenant_id",
+ table: "todo_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_labels_todo_id",
+ table: "todo_labels",
+ column: "todo_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todo_labels_user_id",
+ table: "todo_labels",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todos_assigned_to_id",
+ table: "todos",
+ column: "assigned_to_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todos_closed_by_id",
+ table: "todos",
+ column: "closed_by_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todos_collection_id",
+ table: "todos",
+ column: "collection_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todos_tenant_id",
+ table: "todos",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_todos_user_id",
+ table: "todos",
+ column: "user_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_contacts_tenants_tenant_id",
+ table: "customer_contacts",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_contacts_users_user_id",
+ table: "customer_contacts",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_events_tenants_tenant_id",
+ table: "customer_events",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_events_users_user_id",
+ table: "customer_events",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_groups_tenants_tenant_id",
+ table: "customer_groups",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customer_groups_users_user_id",
+ table: "customer_groups",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customers_tenants_tenant_id",
+ table: "customers",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customers_users_owner_id",
+ table: "customers",
+ column: "owner_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_labels_tenants_tenant_id",
+ table: "project_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_labels_users_user_id",
+ table: "project_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_projects_tenants_tenant_id",
+ table: "projects",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_projects_users_user_id",
+ table: "projects",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_tenants_owning_tenant_id",
+ table: "tenants",
+ column: "owning_tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_tenants_users_user_id",
+ table: "tenants",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_users_tenants_tenant_id",
+ table: "users",
+ column: "tenant_id",
+ principalTable: "tenants",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_contacts_tenants_tenant_id",
+ table: "customer_contacts");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_contacts_users_user_id",
+ table: "customer_contacts");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_events_tenants_tenant_id",
+ table: "customer_events");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_events_users_user_id",
+ table: "customer_events");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_groups_tenants_tenant_id",
+ table: "customer_groups");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customer_groups_users_user_id",
+ table: "customer_groups");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customers_tenants_tenant_id",
+ table: "customers");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_customers_users_owner_id",
+ table: "customers");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_labels_tenants_tenant_id",
+ table: "project_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_labels_users_user_id",
+ table: "project_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_projects_tenants_tenant_id",
+ table: "projects");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_projects_users_user_id",
+ table: "projects");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_tenants_owning_tenant_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_tenants_users_user_id",
+ table: "tenants");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_categories_users_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_entries_users_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_time_labels_users_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_users_tenants_tenant_id",
+ table: "users");
+
+ migrationBuilder.DropTable(
+ name: "todo_collection_access_controls");
+
+ migrationBuilder.DropTable(
+ name: "todo_comments");
+
+ migrationBuilder.DropTable(
+ name: "todo_labels");
+
+ migrationBuilder.DropTable(
+ name: "todos");
+
+ migrationBuilder.DropTable(
+ name: "todo_collections");
+
+ migrationBuilder.DropIndex(
+ name: "ix_users_tenant_id",
+ table: "users");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_owning_tenant_id",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_tenants_user_id",
+ table: "tenants");
+
+ migrationBuilder.DropIndex(
+ name: "ix_projects_tenant_id",
+ table: "projects");
+
+ migrationBuilder.DropIndex(
+ name: "ix_projects_user_id",
+ table: "projects");
+
+ migrationBuilder.DropIndex(
+ name: "ix_project_labels_tenant_id",
+ table: "project_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_project_labels_user_id",
+ table: "project_labels");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customers_owner_id",
+ table: "customers");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customers_tenant_id",
+ table: "customers");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_groups_tenant_id",
+ table: "customer_groups");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_groups_user_id",
+ table: "customer_groups");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_events_tenant_id",
+ table: "customer_events");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_events_user_id",
+ table: "customer_events");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_contacts_tenant_id",
+ table: "customer_contacts");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customer_contacts_user_id",
+ table: "customer_contacts");
+
+ migrationBuilder.DropColumn(
+ name: "tenant_id",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "created_by",
+ table: "tenants");
+
+ migrationBuilder.DropColumn(
+ name: "created_by",
+ table: "customers");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "time_labels",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "time_labels",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "time_labels",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "time_entries",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "time_entries",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "time_entries",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "time_categories",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "time_categories",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "time_categories",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "owning_tenant_id",
+ table: "tenants",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "tenants",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "tenants",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "projects",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "projects",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "projects",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "project_labels",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "project_labels",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "project_labels",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "owner_id",
+ table: "customers",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "customers",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "customers",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "customer_groups",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "customer_groups",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "customer_groups",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "customer_events",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "customer_events",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "customer_events",
+ newName: "created_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "modified_by",
+ table: "customer_contacts",
+ newName: "modified_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "deleted_by",
+ table: "customer_contacts",
+ newName: "deleted_by_id");
+
+ migrationBuilder.RenameColumn(
+ name: "created_by",
+ table: "customer_contacts",
+ newName: "created_by_id");
+
+ migrationBuilder.CreateTable(
+ name: "tenant_user",
+ columns: table => new
+ {
+ tenants_id = table.Column<Guid>(type: "uuid", nullable: false),
+ users_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_tenant_user", x => new { x.tenants_id, x.users_id });
+ table.ForeignKey(
+ name: "fk_tenant_user_tenants_tenants_id",
+ column: x => x.tenants_id,
+ principalTable: "tenants",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_tenant_user_users_users_id",
+ column: x => x.users_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_tenant_user_users_id",
+ table: "tenant_user",
+ column: "users_id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221114034213_RemoveTimeTracker.Designer.cs b/code/api/src/Migrations/20221114034213_RemoveTimeTracker.Designer.cs
new file mode 100644
index 0000000..c649575
--- /dev/null
+++ b/code/api/src/Migrations/20221114034213_RemoveTimeTracker.Designer.cs
@@ -0,0 +1,1589 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221114034213_RemoveTimeTracker")]
+ partial class RemoveTimeTracker
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221114034213_RemoveTimeTracker.cs b/code/api/src/Migrations/20221114034213_RemoveTimeTracker.cs
new file mode 100644
index 0000000..70b2539
--- /dev/null
+++ b/code/api/src/Migrations/20221114034213_RemoveTimeTracker.cs
@@ -0,0 +1,177 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class RemoveTimeTracker : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "time_labels");
+
+ migrationBuilder.DropTable(
+ name: "time_entries");
+
+ migrationBuilder.DropTable(
+ name: "time_categories");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "time_categories",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ tenantid = table.Column<Guid>(name: "tenant_id", type: "uuid", nullable: true),
+ userid = table.Column<Guid>(name: "user_id", type: "uuid", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false),
+ createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ deletedat = table.Column<DateTime>(name: "deleted_at", type: "timestamp with time zone", nullable: true),
+ deletedby = table.Column<Guid>(name: "deleted_by", type: "uuid", nullable: true),
+ modifiedat = table.Column<DateTime>(name: "modified_at", type: "timestamp with time zone", nullable: true),
+ modifiedby = table.Column<Guid>(name: "modified_by", type: "uuid", nullable: true),
+ name = table.Column<string>(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_categories", x => x.id);
+ table.ForeignKey(
+ name: "fk_time_categories_tenants_tenant_id",
+ column: x => x.tenantid,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_time_categories_users_user_id",
+ column: x => x.userid,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "time_entries",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ categoryid = table.Column<Guid>(name: "category_id", type: "uuid", nullable: true),
+ tenantid = table.Column<Guid>(name: "tenant_id", type: "uuid", nullable: true),
+ userid = table.Column<Guid>(name: "user_id", type: "uuid", nullable: true),
+ createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false),
+ createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ deletedat = table.Column<DateTime>(name: "deleted_at", type: "timestamp with time zone", nullable: true),
+ deletedby = table.Column<Guid>(name: "deleted_by", type: "uuid", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ modifiedat = table.Column<DateTime>(name: "modified_at", type: "timestamp with time zone", nullable: true),
+ modifiedby = table.Column<Guid>(name: "modified_by", type: "uuid", nullable: true),
+ start = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ stop = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_entries", x => x.id);
+ table.ForeignKey(
+ name: "fk_time_entries_tenants_tenant_id",
+ column: x => x.tenantid,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_time_entries_time_categories_category_id",
+ column: x => x.categoryid,
+ principalTable: "time_categories",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_time_entries_users_user_id",
+ column: x => x.userid,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "time_labels",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ tenantid = table.Column<Guid>(name: "tenant_id", type: "uuid", nullable: true),
+ userid = table.Column<Guid>(name: "user_id", type: "uuid", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false),
+ createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ deletedat = table.Column<DateTime>(name: "deleted_at", type: "timestamp with time zone", nullable: true),
+ deletedby = table.Column<Guid>(name: "deleted_by", type: "uuid", nullable: true),
+ modifiedat = table.Column<DateTime>(name: "modified_at", type: "timestamp with time zone", nullable: true),
+ modifiedby = table.Column<Guid>(name: "modified_by", type: "uuid", nullable: true),
+ name = table.Column<string>(type: "text", nullable: true),
+ timeentryid = table.Column<Guid>(name: "time_entry_id", type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_time_labels", x => x.id);
+ table.ForeignKey(
+ name: "fk_time_labels_tenants_tenant_id",
+ column: x => x.tenantid,
+ principalTable: "tenants",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_time_labels_time_entries_time_entry_id",
+ column: x => x.timeentryid,
+ principalTable: "time_entries",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_time_labels_users_user_id",
+ column: x => x.userid,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_tenant_id",
+ table: "time_categories",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_categories_user_id",
+ table: "time_categories",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_category_id",
+ table: "time_entries",
+ column: "category_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_tenant_id",
+ table: "time_entries",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_entries_user_id",
+ table: "time_entries",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_tenant_id",
+ table: "time_labels",
+ column: "tenant_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_time_entry_id",
+ table: "time_labels",
+ column: "time_entry_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_time_labels_user_id",
+ table: "time_labels",
+ column: "user_id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.Designer.cs b/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.Designer.cs
new file mode 100644
index 0000000..feb9805
--- /dev/null
+++ b/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.Designer.cs
@@ -0,0 +1,1589 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221114035223_RenameForgotPasswordRequests")]
+ partial class RenameForgotPasswordRequests
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.cs b/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.cs
new file mode 100644
index 0000000..8b256e7
--- /dev/null
+++ b/code/api/src/Migrations/20221114035223_RenameForgotPasswordRequests.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class RenameForgotPasswordRequests : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_forgot_password_requests",
+ table: "forgot_password_requests");
+
+ migrationBuilder.RenameTable(
+ name: "forgot_password_requests",
+ newName: "password_reset_requests");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_forgot_password_requests_user_id",
+ table: "password_reset_requests",
+ newName: "ix_password_reset_requests_user_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_password_reset_requests",
+ table: "password_reset_requests",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_password_reset_requests_users_user_id",
+ table: "password_reset_requests",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_password_reset_requests_users_user_id",
+ table: "password_reset_requests");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_password_reset_requests",
+ table: "password_reset_requests");
+
+ migrationBuilder.RenameTable(
+ name: "password_reset_requests",
+ newName: "forgot_password_requests");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_password_reset_requests_user_id",
+ table: "forgot_password_requests",
+ newName: "ix_forgot_password_requests_user_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_forgot_password_requests",
+ table: "forgot_password_requests",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_forgot_password_requests_users_user_id",
+ table: "forgot_password_requests",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221209041908_TenantSlug.Designer.cs b/code/api/src/Migrations/20221209041908_TenantSlug.Designer.cs
new file mode 100644
index 0000000..d23568d
--- /dev/null
+++ b/code/api/src/Migrations/20221209041908_TenantSlug.Designer.cs
@@ -0,0 +1,1593 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221209041908_TenantSlug")]
+ partial class TenantSlug
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<string>("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221209041908_TenantSlug.cs b/code/api/src/Migrations/20221209041908_TenantSlug.cs
new file mode 100644
index 0000000..9dc8be8
--- /dev/null
+++ b/code/api/src/Migrations/20221209041908_TenantSlug.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class TenantSlug : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "slug",
+ table: "tenants",
+ type: "text",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "slug",
+ table: "tenants");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs
new file mode 100644
index 0000000..aa2d7f4
--- /dev/null
+++ b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs
@@ -0,0 +1,1618 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221209043806_ValidationEmailQueue")]
+ partial class ValidationEmailQueue
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Queues.ValidationEmail", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("EmailSentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_sent_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_validation_emails");
+
+ b.ToTable("validation_emails", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<string>("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime>("EmailLastValidated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_last_validated");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs
new file mode 100644
index 0000000..3599a37
--- /dev/null
+++ b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs
@@ -0,0 +1,46 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class ValidationEmailQueue : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<DateTime>(
+ name: "email_last_validated",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.CreateTable(
+ name: "validation_emails",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ emailsentat = table.Column<DateTime>(name: "email_sent_at", type: "timestamp with time zone", nullable: false),
+ userid = table.Column<Guid>(name: "user_id", type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_validation_emails", x => x.id);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "validation_emails");
+
+ migrationBuilder.DropColumn(
+ name: "email_last_validated",
+ table: "users");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221214143556_AddUserDeletedBy.Designer.cs b/code/api/src/Migrations/20221214143556_AddUserDeletedBy.Designer.cs
new file mode 100644
index 0000000..de4debc
--- /dev/null
+++ b/code/api/src/Migrations/20221214143556_AddUserDeletedBy.Designer.cs
@@ -0,0 +1,1622 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221214143556_AddUserDeletedBy")]
+ partial class AddUserDeletedBy
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<string>("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime>("EmailLastValidated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_last_validated");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ValidationEmail", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("EmailSentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_sent_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_validation_emails");
+
+ b.ToTable("validation_emails", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221214143556_AddUserDeletedBy.cs b/code/api/src/Migrations/20221214143556_AddUserDeletedBy.cs
new file mode 100644
index 0000000..e929b88
--- /dev/null
+++ b/code/api/src/Migrations/20221214143556_AddUserDeletedBy.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddUserDeletedBy : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<Guid>(
+ name: "deleted_by",
+ table: "users",
+ type: "uuid",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "deleted_by",
+ table: "users");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/AppDbContextModelSnapshot.cs b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 0000000..e0a5cfb
--- /dev/null
+++ b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,1619 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<string>("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime>("EmailLastValidated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_last_validated");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ValidationEmail", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("EmailSentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_sent_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_validation_emails");
+
+ b.ToTable("validation_emails", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Models/Database/Api/ApiAccessToken.cs b/code/api/src/Models/Database/Api/ApiAccessToken.cs
new file mode 100644
index 0000000..9359fc4
--- /dev/null
+++ b/code/api/src/Models/Database/Api/ApiAccessToken.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ApiAccessToken : Base
+{
+ public User User { get; set; }
+ public DateTime ExpiryDate { get; set; }
+ public bool AllowRead { get; set; }
+ public bool AllowCreate { get; set; }
+ public bool AllowUpdate { get; set; }
+ public bool AllowDelete { get; set; }
+ public bool HasExpired => ExpiryDate < AppDateTime.UtcNow;
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Base.cs b/code/api/src/Models/Database/Base.cs
new file mode 100644
index 0000000..0b16b12
--- /dev/null
+++ b/code/api/src/Models/Database/Base.cs
@@ -0,0 +1,22 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public abstract class Base
+{
+ protected Base() {
+ Id = Guid.NewGuid();
+ CreatedAt = AppDateTime.UtcNow;
+ }
+
+ public Guid Id { get; init; }
+ public DateTime CreatedAt { get; init; }
+ public DateTime? ModifiedAt { get; private set; }
+ public DateTime? DeletedAt { get; private set; }
+ public bool Deleted { get; private set; }
+
+ public void SetModified() => ModifiedAt = AppDateTime.UtcNow;
+
+ public void SetDeleted() {
+ Deleted = true;
+ DeletedAt = AppDateTime.UtcNow;
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/BaseWithOwner.cs b/code/api/src/Models/Database/BaseWithOwner.cs
new file mode 100644
index 0000000..7e9f6c1
--- /dev/null
+++ b/code/api/src/Models/Database/BaseWithOwner.cs
@@ -0,0 +1,40 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+/// <summary>
+/// Base class for all entities with ownership.
+/// </summary>
+public abstract class BaseWithOwner : Base
+{
+ protected BaseWithOwner() { }
+
+ protected BaseWithOwner(Guid createdBy) {
+ CreatedBy = createdBy;
+ }
+
+ protected BaseWithOwner(LoggedInUserModel loggedInUser) {
+ CreatedBy = loggedInUser.Id;
+ }
+
+ public Guid? UserId { get; private set; }
+ public Guid? TenantId { get; private set; }
+ public Guid? ModifiedBy { get; private set; }
+ public Guid? CreatedBy { get; private set; }
+ public Guid? DeletedBy { get; private set; }
+ public User OwningUser { get; set; }
+ public Tenant OwningTenant { get; set; }
+
+ public void SetDeleted(Guid userId) {
+ DeletedBy = userId;
+ base.SetDeleted();
+ }
+
+ public void SetModified(Guid userId) {
+ ModifiedBy = userId;
+ base.SetModified();
+ }
+
+ public void SetOwnerIds(Guid userId = default, Guid tenantId = default) {
+ if (tenantId != default) TenantId = tenantId;
+ if (userId != default) UserId = userId;
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Customer/Customer.cs b/code/api/src/Models/Database/Customer/Customer.cs
new file mode 100644
index 0000000..8e153c6
--- /dev/null
+++ b/code/api/src/Models/Database/Customer/Customer.cs
@@ -0,0 +1,29 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Customer : BaseWithOwner
+{
+ public Customer() { }
+ public Customer(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+
+ public string CustomerNumber { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Address1 { get; set; }
+ public string Address2 { get; set; }
+ public string PostalCode { get; set; }
+ public string PostalCity { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Email { get; set; }
+ public string VATNumber { get; set; }
+ public string ORGNumber { get; set; }
+ public string DefaultReference { get; set; }
+ public string Website { get; set; }
+ public string Currency { get; set; }
+ public Guid? OwnerId { get; set; }
+ public User Owner { get; set; }
+ public ICollection<CustomerGroup> Groups { get; set; }
+ public ICollection<CustomerContact> Contacts { get; set; }
+ public ICollection<CustomerEvent> Events { get; set; }
+ public ICollection<Project> Projects { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Customer/CustomerContact.cs b/code/api/src/Models/Database/Customer/CustomerContact.cs
new file mode 100644
index 0000000..f5a951d
--- /dev/null
+++ b/code/api/src/Models/Database/Customer/CustomerContact.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerContact : BaseWithOwner
+{
+ public Customer Customer { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public string Phone { get; set; }
+ public string WorkTitle { get; set; }
+ public string Note { get; set; }
+}
diff --git a/code/api/src/Models/Database/Customer/CustomerEvent.cs b/code/api/src/Models/Database/Customer/CustomerEvent.cs
new file mode 100644
index 0000000..a87da4c
--- /dev/null
+++ b/code/api/src/Models/Database/Customer/CustomerEvent.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerEvent : BaseWithOwner
+{
+ public Customer Customer { get; set; }
+ public string Title { get; set; }
+ public string Note { get; set; }
+}
diff --git a/code/api/src/Models/Database/Customer/CustomerGroup.cs b/code/api/src/Models/Database/Customer/CustomerGroup.cs
new file mode 100644
index 0000000..9438f3c
--- /dev/null
+++ b/code/api/src/Models/Database/Customer/CustomerGroup.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerGroup : BaseWithOwner
+{
+ public string Name { get; set; }
+ public ICollection<Customer> Customers { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Customer/CustomerGroupMembership.cs b/code/api/src/Models/Database/Customer/CustomerGroupMembership.cs
new file mode 100644
index 0000000..ec0d4af
--- /dev/null
+++ b/code/api/src/Models/Database/Customer/CustomerGroupMembership.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerGroupMembership : Base
+{
+ public Customer Customer { get; set; }
+ public CustomerGroup Group { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Internal/PasswordResetRequest.cs b/code/api/src/Models/Database/Internal/PasswordResetRequest.cs
new file mode 100644
index 0000000..ee73fd2
--- /dev/null
+++ b/code/api/src/Models/Database/Internal/PasswordResetRequest.cs
@@ -0,0 +1,23 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class PasswordResetRequest
+{
+ public PasswordResetRequest() { }
+
+ public PasswordResetRequest(User user) {
+ CreatedAt = AppDateTime.UtcNow;
+ Id = Guid.NewGuid();
+ User = user;
+ }
+
+ public Guid Id { get; set; }
+ public Guid UserId { get; set; }
+ public User User { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ [NotMapped]
+ public DateTime ExpirationDate => CreatedAt.AddMinutes(15);
+
+ [NotMapped]
+ public bool IsExpired => DateTime.Compare(ExpirationDate, AppDateTime.UtcNow) < 0;
+}
diff --git a/code/api/src/Models/Database/Internal/Tenant.cs b/code/api/src/Models/Database/Internal/Tenant.cs
new file mode 100644
index 0000000..471164d
--- /dev/null
+++ b/code/api/src/Models/Database/Internal/Tenant.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Tenant : BaseWithOwner
+{
+ public string Slug { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string ContactEmail { get; set; }
+ public Guid MasterUserId { get; set; }
+ public string MasterUserPassword { get; set; }
+ public ICollection<User> Users { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Internal/User.cs b/code/api/src/Models/Database/Internal/User.cs
new file mode 100644
index 0000000..f4d08ff
--- /dev/null
+++ b/code/api/src/Models/Database/Internal/User.cs
@@ -0,0 +1,44 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class User : Base
+{
+ public User() { }
+
+ public User(string username) {
+ Username = username;
+ }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public DateTime EmailLastValidated { get; set; }
+ public ICollection<Tenant> Tenants { get; set; }
+ public Guid? DeletedBy { get; set; }
+
+ public string DisplayName(bool isForGreeting = false) {
+ if (!isForGreeting && FirstName.HasValue() && LastName.HasValue()) return FirstName + " " + LastName;
+ return FirstName.HasValue() ? FirstName : Username ?? Email;
+ }
+
+ public void HashAndSetPassword(string password) {
+ Password = PasswordHelper.HashPassword(password);
+ }
+
+ public bool VerifyPassword(string password) {
+ return PasswordHelper.Verify(password, Password);
+ }
+
+ public void SetDeleted(Guid userId) {
+ base.SetDeleted();
+ DeletedBy = userId;
+ }
+
+ public IEnumerable<Claim> DefaultClaims() {
+ return new Claim[] {
+ new(AppClaims.USER_ID, Id.ToString()),
+ new(AppClaims.NAME, Username),
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/MainAppDatabase.cs b/code/api/src/Models/Database/MainAppDatabase.cs
new file mode 100644
index 0000000..2b42fe0
--- /dev/null
+++ b/code/api/src/Models/Database/MainAppDatabase.cs
@@ -0,0 +1,107 @@
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class MainAppDatabase : DbContext, IDataProtectionKeyContext
+{
+ public MainAppDatabase(DbContextOptions<MainAppDatabase> options) : base(options) { }
+ public DbSet<User> Users { get; set; }
+ public DbSet<PasswordResetRequest> PasswordResetRequests { get; set; }
+ public DbSet<ApiAccessToken> AccessTokens { get; set; }
+ public DbSet<Tenant> Tenants { get; set; }
+ public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
+ public DbSet<Project> Projects { get; set; }
+ public DbSet<ProjectLabel> ProjectLabels { get; set; }
+ public DbSet<Customer> Customers { get; set; }
+ public DbSet<CustomerContact> CustomersContacts { get; set; }
+ public DbSet<CustomerEvent> CustomerEvents { get; set; }
+ public DbSet<CustomerGroup> CustomerGroups { get; set; }
+ public DbSet<TodoLabel> TodoLabels { get; set; }
+ public DbSet<TodoCollectionAccessControl> TodoProjectAccessControls { get; set; }
+ public DbSet<TodoCollection> TodoProjects { get; set; }
+ public DbSet<TodoComment> TodoComments { get; set; }
+ public DbSet<Todo> Todos { get; set; }
+ public DbSet<ValidationEmail> ValidationEmails { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder) {
+ modelBuilder.Entity<User>(e => {
+ e.HasMany(n => n.Tenants);
+ e.ToTable("users");
+ });
+ modelBuilder.Entity<PasswordResetRequest>(e => {
+ e.HasOne(c => c.User);
+ e.ToTable("password_reset_requests");
+ });
+ modelBuilder.Entity<ApiAccessToken>(e => {
+ e.HasOne(n => n.User);
+ e.ToTable("api_access_tokens");
+ });
+ modelBuilder.Entity<Tenant>(e => {
+ e.HasMany(n => n.Users);
+ e.ToTable("tenants");
+ });
+ modelBuilder.Entity<Project>(e => {
+ e.HasMany(n => n.Members);
+ e.HasMany(n => n.Customers);
+ e.ToTable("projects");
+ });
+ modelBuilder.Entity<ProjectMember>(e => {
+ e.HasOne(n => n.Project);
+ e.HasOne(n => n.User);
+ e.ToTable("project_members");
+ });
+ modelBuilder.Entity<ProjectLabel>(e => {
+ e.HasOne(n => n.Project);
+ e.ToTable("project_labels");
+ });
+ modelBuilder.Entity<Customer>(e => {
+ e.HasOne(n => n.Owner);
+ e.HasMany(n => n.Events);
+ e.HasMany(n => n.Contacts);
+ e.HasMany(n => n.Groups);
+ e.HasMany(n => n.Projects);
+ e.ToTable("customers");
+ });
+ modelBuilder.Entity<CustomerContact>(e => {
+ e.HasOne(n => n.Customer);
+ e.ToTable("customer_contacts");
+ });
+ modelBuilder.Entity<CustomerEvent>(e => {
+ e.HasOne(n => n.Customer);
+ e.ToTable("customer_events");
+ });
+ modelBuilder.Entity<CustomerGroup>(e => {
+ e.HasMany(n => n.Customers);
+ e.ToTable("customer_groups");
+ });
+ modelBuilder.Entity<Todo>(e => {
+ e.HasOne(n => n.Collection);
+ e.HasOne(n => n.AssignedTo);
+ e.HasOne(n => n.ClosedBy);
+ e.HasMany(n => n.Labels);
+ e.HasMany(n => n.Comments);
+ e.ToTable("todos");
+ });
+ modelBuilder.Entity<TodoCollection>(e => {
+ e.HasOne(n => n.Project);
+ e.HasMany(n => n.AccessControls);
+ e.ToTable("todo_collections");
+ });
+ modelBuilder.Entity<TodoComment>(e => {
+ e.HasOne(n => n.Todo);
+ e.ToTable("todo_comments");
+ });
+ modelBuilder.Entity<TodoLabel>(e => {
+ e.HasOne(n => n.Todo);
+ e.ToTable("todo_labels");
+ });
+ modelBuilder.Entity<TodoCollectionAccessControl>(e => {
+ e.HasOne(n => n.User);
+ e.HasOne(n => n.Collection);
+ e.ToTable("todo_collection_access_controls");
+ });
+ modelBuilder.Entity<ValidationEmail>(e => { e.ToTable("validation_emails"); });
+
+ base.OnModelCreating(modelBuilder);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Project/Project.cs b/code/api/src/Models/Database/Project/Project.cs
new file mode 100644
index 0000000..de9e2cb
--- /dev/null
+++ b/code/api/src/Models/Database/Project/Project.cs
@@ -0,0 +1,15 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Project : BaseWithOwner
+{
+ public Project() { }
+
+ public Project(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public DateTime? Start { get; set; }
+ public DateTime? Stop { get; set; }
+ public ICollection<Customer> Customers { get; set; }
+ public ICollection<ProjectMember> Members { get; set; }
+ public ICollection<ProjectLabel> Labels { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Project/ProjectLabel.cs b/code/api/src/Models/Database/Project/ProjectLabel.cs
new file mode 100644
index 0000000..0e1dc5d
--- /dev/null
+++ b/code/api/src/Models/Database/Project/ProjectLabel.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ProjectLabel : BaseWithOwner
+{
+ public string Value { get; set; }
+ public string Color { get; set; }
+ public Project Project { get; set; }
+}
diff --git a/code/api/src/Models/Database/Project/ProjectMember.cs b/code/api/src/Models/Database/Project/ProjectMember.cs
new file mode 100644
index 0000000..a5e0682
--- /dev/null
+++ b/code/api/src/Models/Database/Project/ProjectMember.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ProjectMember : Base
+{
+ public Project Project { get; set; }
+ public User User { get; set; }
+ public ProjectRole Role { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Queues/ValidationEmail.cs b/code/api/src/Models/Database/Queues/ValidationEmail.cs
new file mode 100644
index 0000000..0457768
--- /dev/null
+++ b/code/api/src/Models/Database/Queues/ValidationEmail.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ValidationEmail
+{
+ public Guid Id { get; set; }
+ public DateTime EmailSentAt { get; set; }
+ public Guid UserId { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Todo/Todo.cs b/code/api/src/Models/Database/Todo/Todo.cs
new file mode 100644
index 0000000..2d7f109
--- /dev/null
+++ b/code/api/src/Models/Database/Todo/Todo.cs
@@ -0,0 +1,17 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Todo : BaseWithOwner
+{
+ public string PublicId { get; set; }
+ public Guid? AssignedToId { get; set; }
+ public Guid? ClosedById { get; set; }
+ public Guid CollectionId { get; set; }
+ public DateTime? ClosedAt { get; set; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public ICollection<TodoLabel> Labels { get; set; }
+ public ICollection<TodoComment> Comments { get; set; }
+ public User AssignedTo { get; set; }
+ public User ClosedBy { get; set; }
+ public TodoCollection Collection { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Todo/TodoCollection.cs b/code/api/src/Models/Database/Todo/TodoCollection.cs
new file mode 100644
index 0000000..470e5e7
--- /dev/null
+++ b/code/api/src/Models/Database/Todo/TodoCollection.cs
@@ -0,0 +1,11 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoCollection : BaseWithOwner
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public TodoVisibility Visibility { get; set; }
+ public Guid? ProjectId { get; set; }
+ public Project Project { get; set; }
+ public ICollection<TodoCollectionAccessControl> AccessControls { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Todo/TodoCollectionAccessControl.cs b/code/api/src/Models/Database/Todo/TodoCollectionAccessControl.cs
new file mode 100644
index 0000000..1676c06
--- /dev/null
+++ b/code/api/src/Models/Database/Todo/TodoCollectionAccessControl.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoCollectionAccessControl : Base
+{
+ public TodoCollection Collection { get; set; }
+ public User User { get; set; }
+ public Guid? UserId { get; set; }
+ public bool CanBrowse { get; set; }
+ public bool CanSubmit { get; set; }
+ public bool CanComment { get; set; }
+ public bool CanEdit { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Todo/TodoComment.cs b/code/api/src/Models/Database/Todo/TodoComment.cs
new file mode 100644
index 0000000..32ac3a3
--- /dev/null
+++ b/code/api/src/Models/Database/Todo/TodoComment.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoComment : BaseWithOwner
+{
+ public string Value { get; set; }
+ public Todo Todo { get; set; }
+ public TodoClosingStatement? ClosingStatement { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Database/Todo/TodoLabel.cs b/code/api/src/Models/Database/Todo/TodoLabel.cs
new file mode 100644
index 0000000..7753ade
--- /dev/null
+++ b/code/api/src/Models/Database/Todo/TodoLabel.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoLabel : BaseWithOwner
+{
+ public string Name { get; set; }
+ public string Color { get; set; }
+ public Todo Todo { get; set; }
+}
diff --git a/code/api/src/Models/Enums/FulfillPasswordResetRequestResult.cs b/code/api/src/Models/Enums/FulfillPasswordResetRequestResult.cs
new file mode 100644
index 0000000..2a84c48
--- /dev/null
+++ b/code/api/src/Models/Enums/FulfillPasswordResetRequestResult.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum FulfillPasswordResetRequestResult
+{
+ REQUEST_NOT_FOUND,
+ USER_NOT_FOUND,
+ FULFILLED
+} \ No newline at end of file
diff --git a/code/api/src/Models/Enums/PasswordResetRequestStatus.cs b/code/api/src/Models/Enums/PasswordResetRequestStatus.cs
new file mode 100644
index 0000000..5629e6f
--- /dev/null
+++ b/code/api/src/Models/Enums/PasswordResetRequestStatus.cs
@@ -0,0 +1,6 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum PasswordResetRequestStatus
+{
+
+} \ No newline at end of file
diff --git a/code/api/src/Models/Enums/ProjectRole.cs b/code/api/src/Models/Enums/ProjectRole.cs
new file mode 100644
index 0000000..c4a3f29
--- /dev/null
+++ b/code/api/src/Models/Enums/ProjectRole.cs
@@ -0,0 +1,9 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum ProjectRole
+{
+ EXTERNAL,
+ RESOURCE,
+ LEADER,
+ OWNER
+} \ No newline at end of file
diff --git a/code/api/src/Models/Enums/TodoClosingStatement.cs b/code/api/src/Models/Enums/TodoClosingStatement.cs
new file mode 100644
index 0000000..d838031
--- /dev/null
+++ b/code/api/src/Models/Enums/TodoClosingStatement.cs
@@ -0,0 +1,13 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum TodoClosingStatement
+{
+ REPORTED,
+ RESOLVED,
+ FIXED,
+ IMPLEMENTED,
+ WONT_FIX,
+ BY_DESIGN,
+ INVALID,
+ DUPLICATE
+} \ No newline at end of file
diff --git a/code/api/src/Models/Enums/TodoVisibility.cs b/code/api/src/Models/Enums/TodoVisibility.cs
new file mode 100644
index 0000000..2c8fa83
--- /dev/null
+++ b/code/api/src/Models/Enums/TodoVisibility.cs
@@ -0,0 +1,10 @@
+
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum TodoVisibility
+{
+ PRIVATE = 0,
+ UNLISTED = 1,
+ INTERNAL = 2,
+ PUBLIC = 3,
+} \ No newline at end of file
diff --git a/code/api/src/Models/Misc/ApiSpecDocument.cs b/code/api/src/Models/Misc/ApiSpecDocument.cs
new file mode 100644
index 0000000..1c7d936
--- /dev/null
+++ b/code/api/src/Models/Misc/ApiSpecDocument.cs
@@ -0,0 +1,9 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class ApiSpecDocument
+{
+ public string VersionName { get; set; }
+ public string SwaggerPath { get; set; }
+ public ApiVersion Version { get; set; }
+ public OpenApiInfo OpenApiInfo { get; set; }
+}
diff --git a/code/api/src/Models/Misc/AppConfiguration.cs b/code/api/src/Models/Misc/AppConfiguration.cs
new file mode 100644
index 0000000..31e5726
--- /dev/null
+++ b/code/api/src/Models/Misc/AppConfiguration.cs
@@ -0,0 +1,118 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class AppConfiguration
+{
+ /// <summary>
+ /// An reachable ip address or url that points to a postgres database.
+ /// </summary>
+ public string DB_HOST { get; set; }
+
+ /// <summary>
+ /// The port number to use with DB_HOST to point to the postgres database.
+ /// </summary>
+ public string DB_PORT { get; set; }
+
+ /// <summary>
+ /// The database user to authenticate against postgres with.
+ /// </summary>
+ public string DB_USER { get; set; }
+
+ /// <summary>
+ /// The password for the database user to authenticate against postgres with.
+ /// </summary>
+ public string DB_PASSWORD { get; set; }
+
+ /// <summary>
+ /// The name of the main app database.
+ /// </summary>
+ public string DB_NAME { get; set; }
+
+ /// <summary>
+ /// An reachable ip address or url that points to a postgres(quartz) database.
+ /// </summary>
+ public string QUARTZ_DB_HOST { get; set; }
+
+ /// <summary>
+ /// The port number to use with QUARTZ_DB_HOST to point to the postgres(quartz) database.
+ /// </summary>
+ public string QUARTZ_DB_PORT { get; set; }
+
+ /// <summary>
+ /// The database user to authenticate against postgres(quartz) with.
+ /// </summary>
+ public string QUARTZ_DB_USER { get; set; }
+
+ /// <summary>
+ /// The password for the database user to authenticate against postgres(quartz) with.
+ /// </summary>
+ public string QUARTZ_DB_PASSWORD { get; set; }
+
+ /// <summary>
+ /// The name of the quartz database.
+ /// </summary>
+ public string QUARTZ_DB_NAME { get; set; }
+
+ /// <summary>
+ /// API key to use when pushing logs to SEQ
+ /// </summary>
+ public string SEQ_API_KEY { get; set; }
+
+ /// <summary>
+ /// Url pointing to the seq instance that processes server logs
+ /// </summary>
+ public string SEQ_API_URL { get; set; }
+
+ /// <summary>
+ /// A token used when sending email via Postmakr
+ /// </summary>
+ public string POSTMARK_TOKEN { get; set; }
+
+ /// <summary>
+ /// The address to send emails from, needs to be setup as a sender in postmark
+ /// </summary>
+ public string EMAIL_FROM_ADDRESS { get; set; }
+
+ /// <summary>
+ /// The absolute url to the frontend app
+ /// </summary>
+ public string CANONICAL_FRONTEND_URL { get; set; }
+
+ /// <summary>
+ /// The absolute url to the backend api
+ /// </summary>
+ public string CANONICAL_BACKEND_URL { get; set; }
+
+ /// <summary>
+ /// A random string used to encrypt/decrypt for general purposes
+ /// </summary>
+ public string APP_AES_KEY { get; set; }
+
+ /// <summary>
+ /// A base64 string containing a passwordless pfx cert
+ /// </summary>
+ public string APP_CERT { get; set; }
+
+ public X509Certificate2 CERT1() => new(Convert.FromBase64String(APP_CERT));
+
+ public object GetPublicVersion() {
+ return new {
+ DB_HOST,
+ DB_PORT,
+ DB_USER,
+ DB_PASSWORD = DB_PASSWORD.Obfuscate() ?? "",
+ QUARTZ_DB_HOST,
+ QUARTZ_DB_PORT,
+ QUARTZ_DB_USER,
+ QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate() ?? "",
+ SEQ_API_KEY = SEQ_API_KEY.Obfuscate() ?? "",
+ SEQ_API_URL,
+ POSTMARK_TOKEN = POSTMARK_TOKEN.Obfuscate() ?? "",
+ EMAIL_FROM_ADDRESS,
+ APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "",
+ CERT1 = CERT1().PublicKey.Oid.FriendlyName,
+ CANONICAL_FRONTEND_URL
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Misc/AppPath.cs b/code/api/src/Models/Misc/AppPath.cs
new file mode 100644
index 0000000..e47e48c
--- /dev/null
+++ b/code/api/src/Models/Misc/AppPath.cs
@@ -0,0 +1,23 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public sealed record AppPath
+{
+ public string HostPath { get; init; }
+ public string WebPath { get; init; }
+
+ public string GetHostPathForFilename(string filename, string fallback = "") {
+ if (filename.IsNullOrWhiteSpace()) {
+ return fallback;
+ }
+
+ return Path.Combine(HostPath, filename);
+ }
+
+ public string GetWebPathForFilename(string filename, string fallback = "") {
+ if (filename.IsNullOrWhiteSpace()) {
+ return fallback;
+ }
+
+ return Path.Combine(WebPath, filename);
+ }
+}
diff --git a/code/api/src/Models/Misc/KnownProblemModel.cs b/code/api/src/Models/Misc/KnownProblemModel.cs
new file mode 100644
index 0000000..9acc85c
--- /dev/null
+++ b/code/api/src/Models/Misc/KnownProblemModel.cs
@@ -0,0 +1,26 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class KnownProblemModel
+{
+ public KnownProblemModel(string title = default, string subtitle = default, Dictionary<string, string[]> errors = default) {
+ Title = title;
+ Subtitle = subtitle;
+ Errors = errors ?? new();
+ }
+
+ public string Title { get; set; }
+ public string Subtitle { get; set; }
+ public Dictionary<string, string[]> Errors { get; set; }
+ public string TraceId { get; set; }
+
+ public void AddError(string field, string errorText) {
+ if (!Errors.ContainsKey(field)) {
+ Errors.Add(field, new[] {errorText});
+ } else {
+ var currentErrors = Errors[field];
+ var newErrors = currentErrors.Concat(new[] {errorText});
+ Errors.Remove(field);
+ Errors.Add(field, newErrors.ToArray());
+ }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Misc/LoggedInUserModel.cs b/code/api/src/Models/Misc/LoggedInUserModel.cs
new file mode 100644
index 0000000..541d4a5
--- /dev/null
+++ b/code/api/src/Models/Misc/LoggedInUserModel.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class LoggedInUserModel
+{
+ public Guid Id { get; set; }
+ public string Username { get; set; }
+ public Guid TenantId { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Misc/RequestTimeZoneInfo.cs b/code/api/src/Models/Misc/RequestTimeZoneInfo.cs
new file mode 100644
index 0000000..4c5aa13
--- /dev/null
+++ b/code/api/src/Models/Misc/RequestTimeZoneInfo.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class RequestTimeZoneInfo
+{
+ public TimeZoneInfo TimeZoneInfo { get; set; }
+ public int Offset { get; set; }
+ public DateTime LocalDateTime { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Static/AppClaims.cs b/code/api/src/Models/Static/AppClaims.cs
new file mode 100644
index 0000000..6569700
--- /dev/null
+++ b/code/api/src/Models/Static/AppClaims.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppClaims
+{
+ public const string USER_ID = "user_id";
+ public const string NAME = "name";
+}
diff --git a/code/api/src/Models/Static/AppConstants.cs b/code/api/src/Models/Static/AppConstants.cs
new file mode 100644
index 0000000..d0a888b
--- /dev/null
+++ b/code/api/src/Models/Static/AppConstants.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppConstants
+{
+ public const string API_NAME = "Greatoffice API";
+ public const string BASIC_AUTH_SCHEME = "BasicAuthenticationScheme";
+ public const string TOKEN_ALLOW_READ = "TOKEN_ALLOW_READ";
+ public const string TOKEN_ALLOW_CREATE = "TOKEN_ALLOW_CREATE";
+ public const string TOKEN_ALLOW_UPDATE = "TOKEN_ALLOW_UPDATE";
+ public const string TOKEN_ALLOW_DELETE = "TOKEN_ALLOW_DELETE";
+ public const string VAULT_CACHE_KEY = "VAULT_CACHE_KEY";
+}
diff --git a/code/api/src/Models/Static/AppCookies.cs b/code/api/src/Models/Static/AppCookies.cs
new file mode 100644
index 0000000..e307b83
--- /dev/null
+++ b/code/api/src/Models/Static/AppCookies.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppCookies
+{
+ public const string Locale = "go_locale";
+ public const string Session = "go_session";
+} \ No newline at end of file
diff --git a/code/api/src/Models/Static/AppDateTime.cs b/code/api/src/Models/Static/AppDateTime.cs
new file mode 100644
index 0000000..880d2a8
--- /dev/null
+++ b/code/api/src/Models/Static/AppDateTime.cs
@@ -0,0 +1,16 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppDateTime
+{
+ private static DateTime? dateTime;
+
+ public static DateTime UtcNow => dateTime ?? DateTime.UtcNow;
+
+ public static void Set(DateTime setDateTime) {
+ dateTime = setDateTime;
+ }
+
+ public static void Reset() {
+ dateTime = null;
+ }
+}
diff --git a/code/api/src/Models/Static/AppEnvironmentVariables.cs b/code/api/src/Models/Static/AppEnvironmentVariables.cs
new file mode 100644
index 0000000..c3f821d
--- /dev/null
+++ b/code/api/src/Models/Static/AppEnvironmentVariables.cs
@@ -0,0 +1,21 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppEnvironmentVariables
+{
+ /// <summary>
+ /// An access token that can be used to access the Hashicorp Vault instance that is available at VAULT_URL
+ /// </summary>
+ public const string VAULT_TOKEN = "VAULT_TOKEN";
+ /// <summary>
+ /// An url pointing to the Hashicorp Vault instance the app should use
+ /// </summary>
+ public const string VAULT_URL = "VAULT_URL";
+ /// <summary>
+ /// The duration of which to keep a local cached version of the configuration
+ /// </summary>
+ public const string VAULT_CACHE_TTL = "VAULT_CACHE_TTL";
+ /// <summary>
+ /// The vault key name for the main configuration json object, described by <see cref="AppConfiguration"/>
+ /// </summary>
+ public const string MAIN_CONFIG_SHEET = "MAIN_CONFIG_SHEET";
+}
diff --git a/code/api/src/Models/Static/AppHeaders.cs b/code/api/src/Models/Static/AppHeaders.cs
new file mode 100644
index 0000000..d534aba
--- /dev/null
+++ b/code/api/src/Models/Static/AppHeaders.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppHeaders
+{
+ public const string BROWSER_TIME_ZONE = "X-TimeZone";
+ public const string IS_KNOWN_PROBLEM = "X-IsKnownProblem";
+}
diff --git a/code/api/src/Models/Static/AppPaths.cs b/code/api/src/Models/Static/AppPaths.cs
new file mode 100644
index 0000000..a24f5af
--- /dev/null
+++ b/code/api/src/Models/Static/AppPaths.cs
@@ -0,0 +1,17 @@
+
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppPaths
+{
+ public static AppPath AppData => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData")
+ };
+
+ public static AppPath DataProtectionKeys => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "dp-keys")
+ };
+
+ public static AppPath Frontend => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "Frontend")
+ };
+}
diff --git a/code/api/src/Models/Static/JsonSettings.cs b/code/api/src/Models/Static/JsonSettings.cs
new file mode 100644
index 0000000..a163c11
--- /dev/null
+++ b/code/api/src/Models/Static/JsonSettings.cs
@@ -0,0 +1,11 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class JsonSettings
+{
+ public static Action<JsonOptions> Default { get; } = options => {
+ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
+ options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
+ options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ };
+}
diff --git a/code/api/src/Program.cs b/code/api/src/Program.cs
new file mode 100644
index 0000000..3da1111
--- /dev/null
+++ b/code/api/src/Program.cs
@@ -0,0 +1,249 @@
+global using System;
+global using System.Linq;
+global using System.IO;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using System.Collections.Generic;
+global using System.ComponentModel.DataAnnotations.Schema;
+global using System.Security.Claims;
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
+global using IOL.GreatOffice.Api.Data.Database;
+global using IOL.GreatOffice.Api.Data.Enums;
+global using IOL.GreatOffice.Api.Data.Models;
+global using IOL.GreatOffice.Api.Data.Static;
+global using IOL.GreatOffice.Api.Services;
+global using IOL.GreatOffice.Api.Utilities;
+global using IOL.Helpers;
+global using Microsoft.OpenApi.Models;
+global using Microsoft.AspNetCore.Authentication.Cookies;
+global using Microsoft.AspNetCore.Builder;
+global using Microsoft.AspNetCore.DataProtection;
+global using Microsoft.AspNetCore.Authorization;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Authentication;
+global using Microsoft.AspNetCore.Mvc;
+global using Microsoft.EntityFrameworkCore;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.Localization;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using Serilog;
+global using Quartz;
+global using IOL.GreatOffice.Api.Resources;
+using IOL.GreatOffice.Api.Endpoints.V1;
+using IOL.GreatOffice.Api.Jobs;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Localization;
+using Microsoft.AspNetCore.Mvc.Versioning;
+using Serilog.Events;
+
+namespace IOL.GreatOffice.Api;
+
+public static class Program
+{
+ public static WebApplicationBuilder CreateAppBuilder(string[] args) {
+ var builder = WebApplication.CreateBuilder(args);
+ builder.Services.AddLogging();
+ builder.Services.AddHttpClient();
+ builder.Services.AddMemoryCache();
+ builder.Services.AddScoped<MailService>();
+ builder.Services.AddScoped<PasswordResetService>();
+ builder.Services.AddScoped<UserService>();
+ builder.Services.AddScoped<TenantService>();
+ builder.Services.AddScoped<EmailValidationService>();
+ builder.Services.AddSingleton<VaultService>();
+ builder.Services.AddHttpClient<VaultService>();
+ builder.Services.AddHttpClient<MailService>();
+ var vaultService = builder.Services.BuildServiceProvider().GetRequiredService<VaultService>();
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ var logger = new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
+ .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
+ .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Information)
+ .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
+ .WriteTo.Console();
+
+ if (!builder.Environment.IsDevelopment() && configuration.SEQ_API_KEY.HasValue() && configuration.SEQ_API_URL.HasValue()) {
+ logger.WriteTo.Seq(configuration.SEQ_API_URL, apiKey: configuration.SEQ_API_KEY);
+ }
+
+ Log.Logger = logger.CreateLogger();
+ Log.Information("Starting web host, "
+ + JsonSerializer.Serialize(configuration.GetPublicVersion(),
+ new JsonSerializerOptions() {
+ WriteIndented = true
+ }));
+
+ builder.Host.UseSerilog(Log.Logger);
+
+ if (builder.Environment.IsDevelopment()) {
+ builder.Services.AddCors();
+ }
+
+ if (builder.Environment.IsProduction()) {
+ builder.Services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; });
+ }
+
+ builder.Services.AddLocalization();
+ builder.Services.AddRequestLocalization(options => {
+ var supportedCultures = new[] {"en", "nb"};
+ options.SetDefaultCulture(supportedCultures[0])
+ .AddSupportedCultures(supportedCultures)
+ .AddSupportedUICultures(supportedCultures);
+ options.ApplyCurrentCultureToResponseHeaders = true;
+ });
+
+ builder.Services.Configure<RequestLocalizationOptions>(options => {
+ options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
+ // Get culture from specific cookie
+ await Task.FromResult(new ProviderCultureResult(context.Request.Cookies[AppCookies.Locale] ?? "en")))
+ );
+ });
+
+ builder.Services
+ .AddDataProtection()
+ .ProtectKeysWithCertificate(configuration.CERT1())
+ .PersistKeysToDbContext<MainAppDatabase>();
+
+ builder.Services.Configure(JsonSettings.Default);
+ builder.Services.AddQuartz(options => {
+ options.UsePersistentStore(o => {
+ o.UsePostgres(builder.Configuration.GetQuartzDatabaseConnectionString(vaultService.GetCurrentAppConfiguration));
+ o.UseSerializer<QuartzJsonSerializer>();
+ });
+ options.UseMicrosoftDependencyInjectionJobFactory();
+ options.RegisterJobs();
+ });
+
+ builder.Services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; });
+ builder.Services.AddAuthentication(options => {
+ options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ })
+ .AddCookie(options => {
+ options.Cookie.Name = AppCookies.Session;
+ options.Cookie.Domain = builder.Environment.IsDevelopment() ? "localhost" : ".greatoffice.app";
+ options.Cookie.HttpOnly = true;
+ options.Cookie.IsEssential = true;
+ options.SlidingExpiration = true;
+ options.Events.OnRedirectToAccessDenied =
+ options.Events.OnRedirectToLogin = c => {
+ c.Response.StatusCode = StatusCodes.Status401Unauthorized;
+ return Task.FromResult<object>(null);
+ };
+ })
+ .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AppConstants.BASIC_AUTH_SCHEME, default);
+
+ builder.Services.AddDbContext<MainAppDatabase>(options => {
+ options.UseNpgsql(builder.Configuration.GetAppDatabaseConnectionString(vaultService.GetCurrentAppConfiguration),
+ npgsqlDbContextOptionsBuilder => {
+ npgsqlDbContextOptionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
+ npgsqlDbContextOptionsBuilder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), default);
+ })
+ .UseSnakeCaseNamingConvention();
+ if (builder.Environment.IsDevelopment()) {
+ options.EnableSensitiveDataLogging();
+ }
+ });
+
+ builder.Services.AddApiVersioning(options => {
+ options.ApiVersionReader = new UrlSegmentApiVersionReader();
+ options.ReportApiVersions = true;
+ options.AssumeDefaultVersionWhenUnspecified = false;
+ });
+ builder.Services.AddVersionedApiExplorer(options => { options.SubstituteApiVersionInUrl = true; });
+ builder.Services.AddSwaggerGen(options => {
+ options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "IOL.GreatOffice.Api.xml"));
+ options.UseApiEndpoints();
+ options.OperationFilter<SwaggerDefaultValues>();
+ options.OperationFilter<PaginationOperationFilter>();
+ options.SwaggerDoc(ApiSpecV1.Document.VersionName, ApiSpecV1.Document.OpenApiInfo);
+ options.AddSecurityDefinition("Basic",
+ new OpenApiSecurityScheme {
+ Name = "Authorization",
+ Type = SecuritySchemeType.ApiKey,
+ Scheme = "Basic",
+ BearerFormat = "Basic",
+ In = ParameterLocation.Header,
+ Description =
+ "Enter your token in the text input below.\r\n\r\nExample: \"Basic 12345abcdef\"",
+ });
+
+ options.AddSecurityRequirement(new OpenApiSecurityRequirement {
+ {
+ new OpenApiSecurityScheme {
+ Reference = new OpenApiReference {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Basic"
+ }
+ },
+ Array.Empty<string>()
+ }
+ });
+ });
+
+ builder.Services.AddPagination(options => {
+ options.DefaultSize = 50;
+ options.MaxSize = 100;
+ options.CanChangeSizeFromQuery = true;
+ });
+
+ builder.Services
+ .AddControllers()
+ .AddDataAnnotationsLocalization()
+ .AddJsonOptions(JsonSettings.Default);
+
+ return builder;
+ }
+
+ public static WebApplication CreateWebApplication(WebApplicationBuilder builder) {
+ var app = builder.Build();
+ if (app.Environment.IsDevelopment()) {
+ app.UseDeveloperExceptionPage();
+ app.UseCors(cors => {
+ cors.AllowAnyMethod();
+ cors.AllowAnyHeader();
+ cors.SetIsOriginAllowed(_ => true);
+ cors.AllowCredentials();
+ cors.WithExposedHeaders(AppHeaders.IS_KNOWN_PROBLEM);
+ });
+ }
+
+ if (app.Environment.IsProduction()) {
+ app.UseForwardedHeaders();
+ }
+
+ app.UseDefaultFiles()
+ .UseStaticFiles()
+ .UseRequestLocalization()
+ .UseRouting()
+ .UseSerilogRequestLogging()
+ .UseStatusCodePages()
+ .UseAuthentication()
+ .UseAuthorization()
+ .UseSwagger()
+ .UseSwaggerUI(options => {
+ options.SwaggerEndpoint(ApiSpecV1.Document.SwaggerPath, ApiSpecV1.Document.VersionName);
+ options.DocumentTitle = AppConstants.API_NAME;
+ })
+ .UseEndpoints(endpoints => { endpoints.MapControllers(); });
+ return app;
+ }
+
+ public static int Main(string[] args) {
+ try {
+ CreateWebApplication(CreateAppBuilder(args)).Run();
+ return 0;
+ } catch (Exception ex) {
+ Log.Fatal(ex, "Unhandled exception");
+ return 1;
+ }
+ finally {
+ Log.Information("Shut down complete, flushing logs...");
+ Log.CloseAndFlush();
+ }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Properties/launchSettings.json b/code/api/src/Properties/launchSettings.json
new file mode 100644
index 0000000..ebd333b
--- /dev/null
+++ b/code/api/src/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "IOL.GreatOffice": {
+ "commandName": "Project",
+ "dotnetRunMessages": "true",
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/code/api/src/Resources/SharedResources.cs b/code/api/src/Resources/SharedResources.cs
new file mode 100644
index 0000000..f98895b
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.cs
@@ -0,0 +1,4 @@
+namespace IOL.GreatOffice.Api.Resources;
+
+public class SharedResources
+{ } \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.en.Designer.cs b/code/api/src/Resources/SharedResources.en.Designer.cs
new file mode 100644
index 0000000..02ada48
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.en.Designer.cs
@@ -0,0 +1,84 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace IOL.GreatOffice.Api.Resources {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SharedResources_en {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SharedResources_en() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("IOL.GreatOffice.Api.Resources.SharedResources_en", typeof(SharedResources_en).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Name_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Name is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Start_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Start is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_form {
+ get {
+ return ResourceManager.GetString("Invalid form", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_validation_errors_occured {
+ get {
+ return ResourceManager.GetString("One or more validation errors occured", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_fields_is_invalid {
+ get {
+ return ResourceManager.GetString("One or more fields is invalid", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_username_or_password {
+ get {
+ return ResourceManager.GetString("Invalid username or password", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/code/api/src/Resources/SharedResources.en.resx b/code/api/src/Resources/SharedResources.en.resx
new file mode 100644
index 0000000..87e46e0
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.en.resx
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Name is a required field</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start is a required field</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Invalid form</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more validation errors occured" xml:space="preserve">
+ <value>One or more validation errors occured</value>
+ <comment>One or more validation errors occured</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <value>One or more fields is invalid</value>
+ <comment>One or more fields is invalid</comment>
+ </data>
+ <data name="Invalid username or password" xml:space="preserve">
+ <value>Invalid username or password</value>
+ <comment>Invalid username or password</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.nb.Designer.cs b/code/api/src/Resources/SharedResources.nb.Designer.cs
new file mode 100644
index 0000000..d6095f3
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.nb.Designer.cs
@@ -0,0 +1,120 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace IOL.GreatOffice.Api.Resources {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SharedResources_nb {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SharedResources_nb() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("IOL.GreatOffice.Api.Resources.SharedResources_nb", typeof(SharedResources_nb).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Name_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Name is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Start_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Start is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_form {
+ get {
+ return ResourceManager.GetString("Invalid form", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_fields_is_invalid {
+ get {
+ return ResourceManager.GetString("One or more fields is invalid", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_username_or_password {
+ get {
+ return ResourceManager.GetString("Invalid username or password", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_validation_errors_occured {
+ get {
+ return ResourceManager.GetString("One or more validation errors occured", resourceCulture);
+ }
+ }
+
+ internal static string Greatoffice_Email_Validation {
+ get {
+ return ResourceManager.GetString("Greatoffice Email Validation", resourceCulture);
+ }
+ }
+
+ internal static string Reset_password___Greatoffice {
+ get {
+ return ResourceManager.GetString("Reset password - Greatoffice", resourceCulture);
+ }
+ }
+
+ internal static string The_password_requires_6_or_more_characters_ {
+ get {
+ return ResourceManager.GetString("The password requires 6 or more characters.", resourceCulture);
+ }
+ }
+
+ internal static string There_is_already_a_user_registered_with_username___username_ {
+ get {
+ return ResourceManager.GetString("There is already a user registered with username: {username}", resourceCulture);
+ }
+ }
+
+ internal static string _username__does_not_look_like_a_valid_email {
+ get {
+ return ResourceManager.GetString("{username} does not look like a valid email", resourceCulture);
+ }
+ }
+
+ internal static string Could_not_create_your_account__try_again_soon_ {
+ get {
+ return ResourceManager.GetString("Could not create your account, try again soon.", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/code/api/src/Resources/SharedResources.nb.resx b/code/api/src/Resources/SharedResources.nb.resx
new file mode 100644
index 0000000..b3513c9
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.nb.resx
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+ </value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+ </value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Navn er et påkrevd felt</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start er et påkrevd felt</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Ugyldig skjema</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <comment>One or more fields is invalid</comment>
+ <value>En eller flere felt er ugyldige</value>
+ </data>
+ <data name="Invalid username or password" xml:space="preserve">
+ <value>Ugyldig brukernavn eller passord</value>
+ <comment>Invalid username or password</comment>
+ </data>
+ <data name="One or more validation errors occured" xml:space="preserve">
+ <value>En eller flere valideringsfeil har oppstått</value>
+ </data>
+ <data name="Hello, {0}.&#xA;&#xA;Validate your email address by opening this link in a browser {1}"
+ xml:space="preserve">
+ <value>Hei, {0}.
+
+Bekreft din e-postadresse ved å åpne denne lenken i en nettleser {1}</value>
+ <comment>Hello, {0}.
+
+ Validate your email address by opening this link in a browser {1}</comment>
+ </data>
+ <data name="Greatoffice Email Validation" xml:space="preserve">
+ <value>Greatoffice e-postaddresse bekreftelse</value>
+ <comment>Greatoffice Email Validation</comment>
+ </data>
+ <data name="Hi {0},&#xA;&#xA;Go to the following link to set a new password.&#xA;&#xA;{1}/reset-password/{2}&#xA;&#xA;The link expires at {3}.&#xA;If you did not request a password reset, no action is required."
+ xml:space="preserve">
+ <value>Hei {0},
+
+Åpne denne lenken i en nettleser for å sette nytt passord.
+
+{1}/reset-password/{2}
+
+Lenken utgår {3}.
+Hvis du ikke vet hva dette gjelder, kan du se bort i fra mailen.</value>
+ <comment>Hi {0},
+
+ Go to the following link to set a new password.
+
+ {1}/reset-password/{2}
+
+ The link expires at {3}.
+ If you did not request a password reset, no action is required.</comment>
+ </data>
+ <data name="Reset password - Greatoffice" xml:space="preserve">
+ <value>Sett nytt passord - Greatoffice</value>
+ <comment>Reset password - Greatoffice</comment>
+ </data>
+ <data name="The password requires 6 or more characters." xml:space="preserve">
+ <value>Passordet må være 6 eller flere tegn.</value>
+ <comment>The password requires 6 or more characters.</comment>
+ </data>
+ <data name="There is already a user registered with username: {username}" xml:space="preserve">
+ <value>Det finnes allerede en bruker med brukernavnet: {username}</value>
+ <comment>There is already a user registered with username: {username}</comment>
+ </data>
+ <data name="{username} does not look like a valid email" xml:space="preserve">
+ <value>{username} ser ikke ut som en gyldig e-postadresse</value>
+ <comment>{username} does not look like a valid email</comment>
+ </data>
+ <data name="Could not create your account, try again soon." xml:space="preserve">
+ <value>Kunne ikke lage din konto, prøv igjen snart.</value>
+ <comment>Could not create your account, try again soon.</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.resx b/code/api/src/Resources/SharedResources.resx
new file mode 100644
index 0000000..833428b
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.resx
@@ -0,0 +1,33 @@
+<root>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Name is a required field</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start is a required field</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Invalid form</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <value>One or more fields is invalid</value>
+ <comment>One or more fields is invalid</comment>
+ </data>
+ <data name="Invalid username or password" xml:space="preserve">
+ <value />
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Services/EmailValidationService.cs b/code/api/src/Services/EmailValidationService.cs
new file mode 100644
index 0000000..5552c4f
--- /dev/null
+++ b/code/api/src/Services/EmailValidationService.cs
@@ -0,0 +1,67 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class EmailValidationService
+{
+ private readonly IStringLocalizer<SharedResources> _localizer;
+ private readonly MainAppDatabase _database;
+ private readonly MailService _mailService;
+ private readonly ILogger<EmailValidationService> _logger;
+ private readonly string EmailValidationUrl;
+
+ public EmailValidationService(IStringLocalizer<SharedResources> localizer, MainAppDatabase database, MailService mailService, ILogger<EmailValidationService> logger, VaultService vaultService) {
+ _localizer = localizer;
+ _database = database;
+ _mailService = mailService;
+ _logger = logger;
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/_/validate";
+ }
+
+ public bool FulfillEmailValidationRequest(Guid id, Guid userId) {
+ var item = _database.ValidationEmails.FirstOrDefault(c => c.Id == id);
+ if (item == default) {
+ _logger.LogDebug("Did not find email validation request with id: {requestId}", id);
+ return false;
+ }
+
+ if (item.UserId != userId) {
+ _logger.LogInformation("An unknown user tried to validate the email validation request {requestId}", id);
+ return false;
+ }
+
+ var user = _database.Users.FirstOrDefault(c => c.Id == item.UserId);
+ if (user == default) {
+ _database.ValidationEmails.Remove(item);
+ _database.SaveChanges();
+ _logger.LogInformation("Deleting request {requestId} because user does not exist anymore", id);
+ return false;
+ }
+
+ user.EmailLastValidated = AppDateTime.UtcNow;
+ _database.ValidationEmails.Remove(item);
+ _database.Users.Update(user);
+ _database.SaveChanges();
+ _logger.LogInformation("Successfully validated the email for user {userId}", user.Id);
+ return true;
+ }
+
+ public async Task SendValidationEmailAsync(User user) {
+ var queueItem = new ValidationEmail() {
+ UserId = user.Id,
+ Id = Guid.NewGuid()
+ };
+ var email = new MailService.PostmarkEmail() {
+ To = user.Username,
+ Subject = _localizer["Greatoffice Email Validation"],
+ TextBody = _localizer["""
+Hello {0},
+
+Validate your email address by opening this link in a browser {1}
+""", user.DisplayName(true), EmailValidationUrl + "?id=" + queueItem.Id]
+ };
+ queueItem.EmailSentAt = AppDateTime.UtcNow;
+ _database.ValidationEmails.Add(queueItem);
+ await _database.SaveChangesAsync();
+ Task.Run(async () => await _mailService.SendMailAsync(email));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs
new file mode 100644
index 0000000..a6f7db4
--- /dev/null
+++ b/code/api/src/Services/MailService.cs
@@ -0,0 +1,132 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class MailService
+{
+ private readonly ILogger<MailService> _logger;
+ private static string _fromEmail;
+ private readonly HttpClient _httpClient;
+
+ public MailService(VaultService vaultService, ILogger<MailService> logger, HttpClient httpClient) {
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ _fromEmail = configuration.EMAIL_FROM_ADDRESS;
+ _logger = logger;
+ httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", configuration.POSTMARK_TOKEN);
+ _httpClient = httpClient;
+ }
+
+ /// <summary>
+ /// Send an email.
+ /// </summary>
+ /// <param name="message"></param>
+ /// <exception cref="ArgumentException"></exception>
+ public async Task SendMailAsync(PostmarkEmail message) {
+ try {
+ if (message.MessageStream.IsNullOrWhiteSpace()) {
+ message.MessageStream = "outbound";
+ }
+
+ if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) {
+ message.From = _fromEmail;
+ } else {
+ throw new ApplicationException("Not one from-email is available");
+ }
+
+ if (message.To.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message.To), "A recipient should be specified.");
+ }
+
+ if (!message.To.IsValidEmailAddress()) {
+ throw new ArgumentException(nameof(message.To), "To is not a valid email address");
+ }
+
+ if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) {
+ throw new ArgumentNullException(nameof(message), "Both HtmlBody and TextBody is empty, nothing to send");
+ }
+#if DEBUG
+ _logger.LogInformation("Sending email: {0}", JsonSerializer.Serialize(message, new JsonSerializerOptions() {WriteIndented = true}));
+#endif
+ var response = await _httpClient.PostAsJsonAsync("https://api.postmarkapp.com/email", message);
+ _logger.LogInformation("Postmark returned with message: {0}", (await response.Content.ReadFromJsonAsync<PostmarkSendResponse>()).Message);
+ } catch (Exception e) {
+ _logger.LogError(e, "A silent exception occured while trying to send an email");
+ }
+ }
+
+ private class PostmarkSendResponse
+ {
+ /// <summary>
+ /// The Message ID returned from Postmark.
+ /// </summary>
+ public Guid MessageID { get; set; }
+
+ /// <summary>
+ /// The message from the API.
+ /// In the event of an error, this message may contain helpful text.
+ /// </summary>
+ public string Message { get; set; }
+
+ /// <summary>
+ /// The time the request was received by Postmark.
+ /// </summary>
+ public DateTime SubmittedAt { get; set; }
+
+ /// <summary>
+ /// The recipient of the submitted request.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// The error code returned from Postmark.
+ /// This does not map to HTTP status codes.
+ /// </summary>
+ public int ErrorCode { get; set; }
+ }
+
+ public class PostmarkEmail
+ {
+ /// <summary>
+ /// The message subject line.
+ /// </summary>
+ public string Subject { get; set; }
+
+ /// <summary>
+ /// The message body, if the message contains
+ /// </summary>
+ public string HtmlBody { get; set; }
+
+ /// <summary>
+ /// The message body, if the message is plain text.
+ /// </summary>
+ public string TextBody { get; set; }
+
+ /// <summary>
+ /// The message stream used to send this message.
+ /// </summary>
+ public string MessageStream { get; set; }
+
+ /// <summary>
+ /// The sender's email address.
+ /// </summary>
+ public string From { get; set; }
+
+ /// <summary>
+ /// Any recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string To { get; set; }
+
+ /// <summary>
+ /// Any CC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Cc { get; set; }
+
+ /// <summary>
+ /// Any BCC recipients. Separate multiple recipients with a comma.
+ /// </summary>
+ public string Bcc { get; set; }
+
+ /// <summary>
+ /// The email address to reply to. This is optional.
+ /// </summary>
+ public string ReplyTo { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs
new file mode 100644
index 0000000..4c00b81
--- /dev/null
+++ b/code/api/src/Services/PasswordResetService.cs
@@ -0,0 +1,101 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class PasswordResetService
+{
+ private readonly MainAppDatabase _database;
+ private readonly MailService _mailService;
+ private readonly AppConfiguration _configuration;
+ private readonly ILogger<PasswordResetService> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public PasswordResetService(
+ MainAppDatabase database,
+ VaultService vaultService,
+ ILogger<PasswordResetService> logger,
+ MailService mailService, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _configuration = vaultService.GetCurrentAppConfiguration();
+ _logger = logger;
+ _mailService = mailService;
+ _localizer = localizer;
+ }
+
+ public async Task<PasswordResetRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) {
+ var request = await _database.PasswordResetRequests
+ .Include(c => c.User)
+ .SingleOrDefaultAsync(c => c.Id == id, cancellationToken);
+ if (request == default) {
+ return default;
+ }
+
+ _logger.LogInformation($"Found password reset request for user: {request.User.Username}, expires at {request.ExpirationDate} (in {request.ExpirationDate.Subtract(AppDateTime.UtcNow).Minutes} minutes).");
+ return request;
+ }
+
+ public async Task<FulfillPasswordResetRequestResult> FulfillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) {
+ var request = await GetRequestAsync(id, cancellationToken);
+ if (request == default) return FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND;
+ var user = _database.Users.FirstOrDefault(c => c.Id == request.User.Id);
+ if (user == default) return FulfillPasswordResetRequestResult.USER_NOT_FOUND;
+ user.HashAndSetPassword(newPassword);
+ _database.Users.Update(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Fullfilled password reset request for user: {request.User.Username}");
+ await DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ return FulfillPasswordResetRequestResult.FULFILLED;
+ }
+
+ public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) {
+ await DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ var request = new PasswordResetRequest(user);
+ _database.PasswordResetRequests.Add(request);
+ await _database.SaveChangesAsync(cancellationToken);
+ var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id);
+ var message = new MailService.PostmarkEmail() {
+ To = request.User.Username,
+ Subject = _localizer["Reset password - Greatoffice"],
+ TextBody = _localizer["""
+Hi {0},
+
+Go to the following link to set a new password.
+
+{1}/reset-password/{2}
+
+The link expires at {3}.
+If you did not request a password reset, no action is required.
+""", user.DisplayName(true), _configuration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")]
+ };
+
+#pragma warning disable 4014
+ Task.Run(() => {
+#pragma warning restore 4014
+ _mailService.SendMailAsync(message);
+ _logger.LogInformation($"Added password reset request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(AppDateTime.UtcNow)}.");
+ },
+ cancellationToken);
+ }
+
+ public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) {
+ var requestsToRemove = _database.PasswordResetRequests.Where(c => c.UserId == userId).ToList();
+ if (!requestsToRemove.Any()) return;
+ _database.PasswordResetRequests.RemoveRange(requestsToRemove);
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Deleted {requestsToRemove.Count} password reset requests for user: {userId}.");
+ }
+
+ public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) {
+ var deleteCount = 0;
+ foreach (var request in _database.PasswordResetRequests.Where(c => c.IsExpired)) {
+ if (!request.IsExpired) {
+ continue;
+ }
+
+ _database.PasswordResetRequests.Remove(request);
+ deleteCount++;
+ _logger.LogInformation($"Marking password reset request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}.");
+ }
+
+ await _database.SaveChangesAsync(cancellationToken);
+ _logger.LogInformation($"Deleted {deleteCount} stale password reset requests.");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/TenantService.cs b/code/api/src/Services/TenantService.cs
new file mode 100644
index 0000000..0de6f53
--- /dev/null
+++ b/code/api/src/Services/TenantService.cs
@@ -0,0 +1,61 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class TenantService
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<TenantService> _logger;
+
+ public TenantService(MainAppDatabase database, ILogger<TenantService> logger) {
+ _database = database;
+ _logger = logger;
+ }
+
+ public Tenant CreateTenant(string name, Guid masterUserId, string email) {
+ var tenant = new Tenant() {
+ Name = name,
+ Slug = Slug.Generate(true, name),
+ MasterUserId = masterUserId,
+ ContactEmail = email
+ };
+
+ var masterUserExists = _database.Users.Any(c => c.Id == tenant.MasterUserId);
+ if (!masterUserExists) {
+ _logger.LogError("Tried to create a new tenant with a non-existent master user: {masterUserId}", masterUserId);
+ return default;
+ }
+
+ if (!email.IsValidEmailAddress()) {
+ _logger.LogError("Tried to create a new tenant with an invalid email {contactEmail}", email);
+ return default;
+ }
+
+ _database.Tenants.Add(tenant);
+ _database.SaveChanges();
+ return tenant;
+ }
+
+ public void AddUserToTenant(Guid userId, Guid tenantId) {
+ var tenant = _database.Tenants.FirstOrDefault(c => c.Id == tenantId);
+ if (tenant == default) {
+ _logger.LogError("Tried adding user {userId} to tenant {tenantId} but the tenant was not found", userId, tenantId);
+ return;
+ }
+
+ var user = _database.Users.FirstOrDefault(c => c.Id == userId);
+ if (user == default) {
+ _logger.LogError("Tried adding user {userId} to tenant {tenantId} but the user was not found", userId, tenantId);
+ return;
+ }
+
+ if (tenant.Users.Any(c => c.Id == user.Id)) {
+ _logger.LogDebug("User {userId} is already a part of tenant {tenantId}", userId, tenantId);
+ return;
+ }
+
+ tenant.Users.Add(user);
+ tenant.SetModified();
+ _database.Tenants.Update(tenant);
+ _database.SaveChanges();
+ _logger.LogInformation("Added user {userId} to tenant {tenantId}", userId, tenantId);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs
new file mode 100644
index 0000000..4df8ded
--- /dev/null
+++ b/code/api/src/Services/UserService.cs
@@ -0,0 +1,54 @@
+namespace IOL.GreatOffice.Api.Services;
+
+public class UserService
+{
+ private readonly PasswordResetService _passwordResetService;
+ private readonly ILogger<UserService> _logger;
+ private readonly MainAppDatabase _database;
+
+ public UserService(PasswordResetService passwordResetService, ILogger<UserService> logger, MainAppDatabase database) {
+ _passwordResetService = passwordResetService;
+ _logger = logger;
+ _database = database;
+ }
+
+ public async Task LogInUserAsync(HttpContext httpContext, User user, bool persist = false, CancellationToken cancellationToken = default) {
+ var identity = new ClaimsIdentity(user.DefaultClaims(), CookieAuthenticationDefaults.AuthenticationScheme);
+ var principal = new ClaimsPrincipal(identity);
+ var authenticationProperties = new AuthenticationProperties {
+ AllowRefresh = true,
+ IssuedUtc = DateTimeOffset.UtcNow,
+ };
+
+ if (persist) {
+ authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(6);
+ authenticationProperties.IsPersistent = true;
+ }
+
+ await httpContext.SignInAsync(principal, authenticationProperties);
+ await _passwordResetService.DeleteRequestsForUserAsync(user.Id, cancellationToken);
+ _logger.LogInformation("Logged in user {userId}", user.Id);
+ }
+
+ public async Task LogOutUser(HttpContext httpContext, CancellationToken cancellationToken = default) {
+ await httpContext.SignOutAsync();
+ _logger.LogInformation("Logged out user {userId}", httpContext.User.FindFirst(AppClaims.USER_ID));
+ }
+
+ public async Task MarkUserAsDeleted(Guid userId, Guid actorId) {
+ var user = _database.Users.FirstOrDefault(c => c.Id == userId);
+ if (user == default) {
+ _logger.LogInformation("Tried to delete unknown user {userId}", userId);
+ return;
+ }
+
+ if (user.Username is "demo@greatoffice.app" or "tester@greatoffice.app") {
+ _logger.LogInformation("Not deleting user {userId}, because it's username is {username}", user.Id, user.Username);
+ return;
+ }
+
+ await _passwordResetService.DeleteRequestsForUserAsync(user.Id);
+ user.SetDeleted(actorId);
+ await _database.SaveChangesAsync();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs
new file mode 100644
index 0000000..cd2eecf
--- /dev/null
+++ b/code/api/src/Services/VaultService.cs
@@ -0,0 +1,237 @@
+using Microsoft.Extensions.Caching.Memory;
+
+namespace IOL.GreatOffice.Api.Services;
+
+public class VaultService
+{
+ private readonly HttpClient _client;
+ private readonly IMemoryCache _cache;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger<VaultService> _logger;
+ private int CACHE_TTL { get; set; }
+
+ public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) {
+ var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN);
+ var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL);
+ CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty");
+ if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty");
+ client.DefaultRequestHeaders.Add("X-Vault-Token", token);
+ client.BaseAddress = new Uri(vaultUrl);
+ _client = client;
+ _cache = cache;
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ public async Task<T> GetSecretJsonAsync<T>(string path) {
+ var result = await _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY,
+ async cacheEntry => {
+ cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL);
+ var getSecretResponse = await _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path);
+ if (getSecretResponse == null) {
+ return default;
+ }
+
+ Log.Debug("Setting new vault cache, " + new {
+ PATH = path,
+ CACHE_TTL,
+ Data = JsonSerializer.Serialize(getSecretResponse.Data.Data)
+ });
+ return getSecretResponse.Data.Data ?? default;
+ });
+ return result;
+ }
+
+ public Task<T> RefreshAsync<T>(string path) {
+ _cache.Remove(AppConstants.VAULT_CACHE_KEY);
+ CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ return GetSecretJsonAsync<T>(path);
+ }
+
+ public async Task<RenewTokenResponse> RenewTokenAsync() {
+ var response = await _client.PostAsJsonAsync("v1/auth/token/renew-self", new {increment = "2h"});
+ if (response.IsSuccessStatusCode) {
+ return await response.Content.ReadFromJsonAsync<RenewTokenResponse>();
+ }
+
+ return default;
+ }
+
+ public async Task<TokenLookupResponse> LookupTokenAsync() {
+ var response = await _client.GetAsync("v1/auth/token/lookup-self");
+ if (response.IsSuccessStatusCode) {
+ return await response.Content.ReadFromJsonAsync<TokenLookupResponse>();
+ }
+
+ return default;
+ }
+
+ public AppConfiguration GetCurrentAppConfiguration() {
+#if DEBUG
+ var isInFlightMode = true;
+ if (isInFlightMode) {
+ return new AppConfiguration() {
+ EMAIL_FROM_ADDRESS = "heydev@greatoffice.life",
+ DB_HOST = "localhost",
+ DB_PORT = "5432",
+ DB_NAME = "greatoffice_ivar_dev",
+ DB_PASSWORD = "ivar123",
+ DB_USER = "postgres",
+ CANONICAL_FRONTEND_URL = "http://localhost:5173",
+ CANONICAL_BACKEND_URL = "http://localhost:5000",
+ POSTMARK_TOKEN = "b530c311-45c7-43e5-aa48-f2c992886e51",
+ QUARTZ_DB_HOST = "localhost",
+ QUARTZ_DB_PORT = "5432",
+ QUARTZ_DB_NAME = "greatoffice_quartz_ivar_dev",
+ QUARTZ_DB_PASSWORD = "ivar123",
+ QUARTZ_DB_USER = "postgres",
+ APP_CERT = "MIII2QIBAzCCCJ8GCSqGSIb3DQEHAaCCCJAEggiMMIIIiDCCAz8GCSqGSIb3DQEHBqCCAzAwggMsAgEAMIIDJQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI1JebRQOOJekCAggAgIIC+FTMxILwgOWxknEWvucjaHTtf/KUcSPwc/zWg0RoFzu03o1vZkStztz92L2J+nnNrQti7WEx0C/h5ug67qCQAdzkjNHZE9wn1pI2EqaCwKYwZnOTmyJDLV86xQlH9QYTs4/L1F2qTwpKdBoB2lNyTswYgZ8WNYY65fbFKUUVIaHkReGnma/ERm8F38Ymp7cudqQ4G6sh6E+JFSo2IfcTFb260fWO/iMDU3GryNsaUl4amT4aQfsSrNtf6ODy8Ivh7tJeLbR6bqzMtsKPzavT5ZbA6AP2GYAQSVejNP79lqmtoOs8+dz7HaJdxORBESYi02z+j2rb4ssI+ZPx+YtGvgpr79gVf3qM1/VX4ROzLDUUJGWS2RnRDBBY/d0uMqftndUHSPMFuhfvgBUXdzLqhoEqYNMTP3BpOyBQ7f26soLKrc3zup2WIn8WSnQFLi2D8cWPVPS9iAb0EgQ5cBjuaLz2aX1WVMCSzya7a6Z93rLxUf9s3+PEy75ibhoh/cJvAlCMTfiVAhJOaIroR1K4MKkO23ylyLHv49/2GYIaZ8n0WRO57fM5jDUhOfti+ZzPM6hrSJkRSla+pr8DFlpqOqObksGwxGGTqq6ZvWon19kXesFl5n640uJBu7Viq8IdxGAbX/aRkZNlvja7sOgfiNz3Hxomz7DWwgWLKaNKlFSqFMzsTUye+mUByC1AfEn14/SYyyxRTB99PmItxWFyjo9nOsxH5riz7tPTPxUXzhVb4eDt7PjY+ZsEKTC3a/LFqf3k5MWk+qc4p0Kx1sGaGEAPCCE04mZ7NOdqk6dhoP46FNUEh8CmxDDVaMSdThrvyzv9KrclwQnRMJz7BJWVXUemyQl3aModepXIhvLv90nH1qzYlFDQ0H6rxzCB4f1l//GoWPyYFBxGh6UrkunXWx2fopR87zi2OF3azxqscF/qLVU4FHKzhMrec7eE0/dk3d+0If/AxQ4p7Cso92i/5n0Bsg5DWc4EIWBuldodsjVxxq7dKxinKJkwggVBBgkqhkiG9w0BBwGgggUyBIIFLjCCBSowggUmBgsqhkiG9w0BDAoBAqCCBO4wggTqMBwGCiqGSIb3DQEMAQMwDgQIb6GEBS5DxrkCAggABIIEyHcCXqJMUG8t0PhvDwf0dHZo6SiA2WsLz1hM+KgNBrE8YwuXEZTGYzfHy85cEWNB2WLV5kxgu/vtifCnnlS1bvc2kMKT3dIFER/7hOqRh8pNvzMoeNc4zNkiEB1ZXxlctUKDsQozbLUhnRATwNyeaMkt3B0KQuRaMxGuA9riRISnmGd1K5GTm3VZ0I7e6vDqXCllLzfOQ+aoz8WIOFJ1aoN2E5+bDTtcr18xYJMd+kNOMjMcbg5f9kmNZAk1MBRuiEWtUjMhRySYWk1Km/y5WHRNRShHTae/E4ifmpLuUKsfOjX7T/4RDWg8rYCnxUpLfCln+omQ9t0gFSN+Et7Dw+cyW48Kkrw6StnRz/AeLxo3SU/PAXVazmAa6ZkuNe+uasvTniYM+enw4hgcXPzTu90R40fTGHO1Sp8EV3IbvrtwFu9kjCxtgleLQ139HTtpWXjVZ0o1ikmn2uN4f73gxKIKxmSX4xZZN6IDOze3OOY1aalUIzkbwFAYCV74zEL05dJzo3GOOJfdQsp2GNJPfkcAcuMPMvi+mieBk6XjKDCj95b41hSWDqfuMUgPh3xm3/felVD1HRNO9NF0RscosP02NLi44TcNz4LX9j/E9PHpNFF+W4ba1w7v7h4P5/leQFRP7+H90fPHA2M8UOHZ4AwmwdA5sHYXBoXkVS3snbVzhzkvW5GblFn0l1AFj8AO0HLCwGSumZ1uUEvEA021hmluHbs62iIiOYJbacbcT/TUpO5/2tFMPKr042LmpQFDIuEfrurLTC3r1iXuS6fkWbf2IxdjTrtL61AtPqtFagKSGsyHViO7nPu6yhbhTbmQJ4G0t++b6h18RPS+3muwrnSxgAz6OmbBWybNKOlAyTjd4JO3hfCaQ+K/mO2Q9TnSUOTgeobXXZsOEdltPXFJExQ7+cqkr4gKdPeoTZEcv8jRoS+NHasZIvMPGrwYnvOuSJ09qAwtIcvhGaPkEmTZ6b3wQl0mnTMPCQHXGTXztucB0O33kbn8sClfs6P6dg0GdR6ZnNFacwIpe8T8PmLg5q8bu5FL1eNo4+ijzC64lrZkKeRKKT1vBtZfcGQTvE8TTdQQS5MkKcptfL/3HVE9VopNZlzryJGYj89GMeQ1PfABi1Ovs5gjfro+0xBbtVuAWbP8dM5ugozO1//vjTMZYwXml4nIFkHuGe7R4ZpKRIVjVy7RScelCuQ0yNMGIzx/5Dz3FQXWq1Jii669Oxs/R7iupwo+f6O9XmCJAGXIw5a11Yw9cULptVNc9rPHrauAOeNpE77aSQRKEOJZADvdLB8cXjpXFf2mvzFib69Cuks8QxktAK7Yk3fke1CJpoIb75d8iHkY21epOtqavTppezEd+0uq5RJThH+/nMyZVhRI3tSJ0kVDc1HVX2bTquWcXtniuZNOWYklLxKPfQNho8n0pHRk22UmB8DOxMjnAyt3s7xBNpujU+I7D30lK9N3PH4U+Oyc9pIWc2T7pFILvvToxoE3l2flg6eHnBd6a7ENDVbz1ELwwmt36QQAVQytEngTBYkorbJcQC6e2r/RqoqpP2N4dB7+2ZDMVw97VBraMl7ELaYdf9SOdzuis2engAojSiUUO/gdKGaJGnnldOSi5rvnxs+iMjElMCMGCSqGSIb3DQEJFTEWBBRSLC58imQohokANg6rVjq9KE/MxjAxMCEwCQYFKw4DAhoFAAQUILUGtKvqxRY/ywrrlxKrsuLiNLwECCWv9bVh/bZZAgIIAA=="
+ };
+ }
+#endif
+
+ return GetSecretJsonAsync<AppConfiguration>(
+ _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET)
+ ).Result;
+ }
+
+ public async Task RefreshCurrentAppConfigurationAsync() {
+ await RefreshAsync<AppConfiguration>(_configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET));
+ }
+
+ public class RenewTokenResponse
+ {
+ public Guid RequestId { get; set; }
+ public string LeaseId { get; set; }
+ public bool Renewable { get; set; }
+ public long LeaseDuration { get; set; }
+ public object Data { get; set; }
+ public object WrapInfo { get; set; }
+ public List<string> Warnings { get; set; }
+ public Auth Auth { get; set; }
+ }
+
+ public class Auth
+ {
+ public string ClientToken { get; set; }
+ public string Accessor { get; set; }
+ public List<string> Policies { get; set; }
+ public List<string> TokenPolicies { get; set; }
+ public object Metadata { get; set; }
+ public long LeaseDuration { get; set; }
+ public bool Renewable { get; set; }
+ public string EntityId { get; set; }
+ public string TokenType { get; set; }
+ public bool Orphan { get; set; }
+ public object MfaRequirement { get; set; }
+ public long NumUses { get; set; }
+ }
+
+ public class GetSecretResponse<T>
+ {
+ public VaultSecret<T> Data { get; set; }
+ }
+
+ public class VaultSecret<T>
+ {
+ public T Data { get; set; }
+ public VaultSecretMetadata Metadata { get; set; }
+ }
+
+ public class VaultSecretMetadata
+ {
+ public DateTimeOffset CreatedTime { get; set; }
+ public object CustomMetadata { get; set; }
+ public string DeletionTime { get; set; }
+ public bool Destroyed { get; set; }
+ public long Version { get; set; }
+ }
+
+ public class TokenLookupResponse
+ {
+ [JsonPropertyName("request_id")]
+ public Guid RequestId { get; set; }
+
+ [JsonPropertyName("lease_id")]
+ public string LeaseId { get; set; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; set; }
+
+ [JsonPropertyName("lease_duration")]
+ public long LeaseDuration { get; set; }
+
+ [JsonPropertyName("data")]
+ public TokenLookupResponseData Data { get; set; }
+
+ [JsonPropertyName("wrap_info")]
+ public object WrapInfo { get; set; }
+
+ [JsonPropertyName("warnings")]
+ public object Warnings { get; set; }
+
+ [JsonPropertyName("auth")]
+ public object Auth { get; set; }
+ }
+
+ public class TokenLookupResponseData
+ {
+ [JsonPropertyName("accessor")]
+ public string Accessor { get; set; }
+
+ [JsonPropertyName("creation_time")]
+ public long CreationTime { get; set; }
+
+ [JsonPropertyName("creation_ttl")]
+ public long CreationTtl { get; set; }
+
+ [JsonPropertyName("display_name")]
+ public string DisplayName { get; set; }
+
+ [JsonPropertyName("entity_id")]
+ public string EntityId { get; set; }
+
+ [JsonPropertyName("expire_time")]
+ public DateTimeOffset ExpireTime { get; set; }
+
+ [JsonPropertyName("explicit_max_ttl")]
+ public long ExplicitMaxTtl { get; set; }
+
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ [JsonPropertyName("issue_time")]
+ public DateTimeOffset IssueTime { get; set; }
+
+ [JsonPropertyName("last_renewal")]
+ public DateTimeOffset LastRenewal { get; set; }
+
+ [JsonPropertyName("last_renewal_time")]
+ public long LastRenewalTime { get; set; }
+
+ [JsonPropertyName("meta")]
+ public object Meta { get; set; }
+
+ [JsonPropertyName("num_uses")]
+ public long NumUses { get; set; }
+
+ [JsonPropertyName("orphan")]
+ public bool Orphan { get; set; }
+
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ [JsonPropertyName("policies")]
+ public List<string> Policies { get; set; }
+
+ [JsonPropertyName("renewable")]
+ public bool Renewable { get; set; }
+
+ [JsonPropertyName("ttl")]
+ public long Ttl { get; set; }
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/BasicAuthenticationAttribute.cs b/code/api/src/Utilities/BasicAuthenticationAttribute.cs
new file mode 100644
index 0000000..0bfd007
--- /dev/null
+++ b/code/api/src/Utilities/BasicAuthenticationAttribute.cs
@@ -0,0 +1,39 @@
+using System.Net.Http.Headers;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public class BasicAuthenticationAttribute : TypeFilterAttribute
+{
+ public BasicAuthenticationAttribute(string claimPermission) : base(typeof(BasicAuthenticationFilter)) {
+ Arguments = new object[] {
+ new Claim(claimPermission, "True")
+ };
+ }
+}
+
+public class BasicAuthenticationFilter : IAuthorizationFilter
+{
+ private readonly Claim _claim;
+
+ public BasicAuthenticationFilter(Claim claim) {
+ _claim = claim;
+ }
+
+ public void OnAuthorization(AuthorizationFilterContext context) {
+ if (!context.HttpContext.Request.Headers.ContainsKey("Authorization")) return;
+ try {
+ var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]);
+ if (authHeader.Parameter is null) {
+ context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME);
+ }
+
+ var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
+ if (!hasClaim) {
+ context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME);
+ }
+ } catch {
+ // ignore
+ }
+ }
+}
diff --git a/code/api/src/Utilities/BasicAuthenticationHandler.cs b/code/api/src/Utilities/BasicAuthenticationHandler.cs
new file mode 100644
index 0000000..b0a2d1a
--- /dev/null
+++ b/code/api/src/Utilities/BasicAuthenticationHandler.cs
@@ -0,0 +1,79 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Encodings.Web;
+using Microsoft.Extensions.Options;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
+{
+ private readonly MainAppDatabase _context;
+ private readonly AppConfiguration _configuration;
+ private readonly ILogger<BasicAuthenticationHandler> _logger;
+
+ public BasicAuthenticationHandler(
+ IOptionsMonitor<AuthenticationSchemeOptions> options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock clock,
+ MainAppDatabase context,
+ VaultService vaultService
+ ) :
+ base(options, logger, encoder, clock) {
+ _context = context;
+ _configuration = vaultService.GetCurrentAppConfiguration();
+ _logger = logger.CreateLogger<BasicAuthenticationHandler>();
+ }
+
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync() {
+ var endpoint = Context.GetEndpoint();
+ if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
+ return Task.FromResult(AuthenticateResult.NoResult());
+
+ if (!Request.Headers.ContainsKey("Authorization"))
+ return Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header"));
+
+ try {
+ var tokenEntropy = _configuration.APP_AES_KEY;
+ if (tokenEntropy.IsNullOrWhiteSpace()) {
+ _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled");
+ return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
+ }
+
+ var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
+ if (authHeader.Parameter == null) return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
+ var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
+ var decryptedString = Encoding.UTF8.GetString(credentialBytes).DecryptWithAes(tokenEntropy);
+ var tokenIsGuid = Guid.TryParse(decryptedString, out var tokenId);
+
+ if (!tokenIsGuid) {
+ return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
+ }
+
+ var token = _context.AccessTokens.Include(c => c.User).SingleOrDefault(c => c.Id == tokenId);
+ if (token == default) {
+ return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Not Found"));
+ }
+
+ if (token.HasExpired) {
+ return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Expired"));
+ }
+
+ var permissions = new List<Claim>() {
+ new(AppConstants.TOKEN_ALLOW_READ, token.AllowRead.ToString()),
+ new(AppConstants.TOKEN_ALLOW_UPDATE, token.AllowUpdate.ToString()),
+ new(AppConstants.TOKEN_ALLOW_CREATE, token.AllowCreate.ToString()),
+ new(AppConstants.TOKEN_ALLOW_DELETE, token.AllowDelete.ToString()),
+ };
+ var claims = token.User.DefaultClaims().Concat(permissions);
+ var identity = new ClaimsIdentity(claims, AppConstants.BASIC_AUTH_SCHEME);
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, AppConstants.BASIC_AUTH_SCHEME);
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ } catch (Exception e) {
+ _logger.LogError(e, $"An exception occured when challenging {AppConstants.BASIC_AUTH_SCHEME}");
+ return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
+ }
+ }
+}
diff --git a/code/api/src/Utilities/ConfigurationExtensions.cs b/code/api/src/Utilities/ConfigurationExtensions.cs
new file mode 100644
index 0000000..c95e293
--- /dev/null
+++ b/code/api/src/Utilities/ConfigurationExtensions.cs
@@ -0,0 +1,85 @@
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class ConfigurationExtensions
+{
+ public static string GetAppDatabaseConnectionString(this IConfiguration config, AppConfiguration configuration) {
+ var host = configuration.DB_HOST;
+ var port = configuration.DB_PORT;
+ var database = configuration.DB_NAME;
+ var user = configuration.DB_USER;
+ var password = configuration.DB_PASSWORD;
+
+ string result;
+ if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true";
+ } else {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}";
+ }
+
+ Log.Debug("Using app database connection string: " + result);
+ return result;
+ }
+
+ public static string GetAppDatabaseConnectionString(this IConfiguration config, Func<AppConfiguration> configuration) {
+ var _configuration = configuration();
+ var host = _configuration.DB_HOST;
+ var port = _configuration.DB_PORT;
+ var database = _configuration.DB_NAME;
+ var user = _configuration.DB_USER;
+ var password = _configuration.DB_PASSWORD;
+
+ string result;
+ if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true";
+ } else {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}";
+ }
+
+ Log.Debug("Using app database connection string: " + result);
+ return result;
+ }
+
+ public static string GetQuartzDatabaseConnectionString(this IConfiguration config, AppConfiguration configuration) {
+ var host = configuration.QUARTZ_DB_HOST;
+ var port = configuration.QUARTZ_DB_PORT;
+ var database = configuration.QUARTZ_DB_NAME;
+ var user = configuration.QUARTZ_DB_USER;
+ var password = configuration.QUARTZ_DB_PASSWORD;
+
+ string result;
+ if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true";
+ } else {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}";
+ }
+
+ Log.Debug("Using quartz database connection string: " + result);
+ return result;
+ }
+
+ public static string GetQuartzDatabaseConnectionString(this IConfiguration config, Func<AppConfiguration> configuration) {
+ var _configuration = configuration();
+ var host = _configuration.QUARTZ_DB_HOST;
+ var port = _configuration.QUARTZ_DB_PORT;
+ var database = _configuration.QUARTZ_DB_NAME;
+ var user = _configuration.QUARTZ_DB_USER;
+ var password = _configuration.QUARTZ_DB_PASSWORD;
+
+ string result;
+ if (config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true";
+ } else {
+ result = $"Server={host};Port={port};Database={database};User Id={user};Password={password}";
+ }
+
+ Log.Debug("Using quartz database connection string: " + result);
+ return result;
+ }
+
+ public static string GetVersion(this IConfiguration configuration) {
+ var versionFilePath = Path.Combine(AppPaths.AppData.HostPath, "version.txt");
+ if (!File.Exists(versionFilePath)) return "unknown-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT");
+ var versionText = File.ReadAllText(versionFilePath);
+ return versionText + "-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT");
+ }
+}
diff --git a/code/api/src/Utilities/DateTimeExtensions.cs b/code/api/src/Utilities/DateTimeExtensions.cs
new file mode 100644
index 0000000..d25e9a8
--- /dev/null
+++ b/code/api/src/Utilities/DateTimeExtensions.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class DateTimeExtensions
+{
+ public static bool IsNullOrEmpty(this DateTime dateTime) {
+ return (dateTime == default);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/PaginationOperationFilter.cs b/code/api/src/Utilities/PaginationOperationFilter.cs
new file mode 100644
index 0000000..ad02a3d
--- /dev/null
+++ b/code/api/src/Utilities/PaginationOperationFilter.cs
@@ -0,0 +1,39 @@
+using Microsoft.Extensions.Options;
+using MR.AspNetCore.Pagination;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public class PaginationOperationFilter : IOperationFilter
+{
+ private readonly PaginationOptions _paginationOptions;
+
+ public PaginationOperationFilter(
+ IOptions<PaginationOptions> paginationOptions) {
+ _paginationOptions = paginationOptions.Value;
+ }
+
+ public void Apply(OpenApiOperation operation, OperationFilterContext context) {
+ var boolSchema = context.SchemaGenerator.GenerateSchema(typeof(bool), context.SchemaRepository);
+ var intSchema = context.SchemaGenerator.GenerateSchema(typeof(int), context.SchemaRepository);
+
+ if (PaginationActionDetector.IsKeysetPaginationResultAction(context.MethodInfo, out _)) {
+ CreateParameter(_paginationOptions.FirstQueryParameterName, "true if you want the first page", boolSchema);
+ CreateParameter(_paginationOptions.BeforeQueryParameterName, "Id of the reference entity you want results before");
+ CreateParameter(_paginationOptions.AfterQueryParameterName, "Id of the reference entity you want results after");
+ CreateParameter(_paginationOptions.LastQueryParameterName, "true if you want the last page", boolSchema);
+ } else if (PaginationActionDetector.IsOffsetPaginationResultAction(context.MethodInfo, out _)) {
+ CreateParameter(_paginationOptions.PageQueryParameterName, "The page", intSchema);
+ }
+
+ void CreateParameter(string name, string description, OpenApiSchema schema = null) {
+ operation.Parameters.Add(new OpenApiParameter {
+ Required = false,
+ In = ParameterLocation.Query,
+ Name = name,
+ Description = description,
+ Schema = schema,
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/QuartzJsonSerializer.cs b/code/api/src/Utilities/QuartzJsonSerializer.cs
new file mode 100644
index 0000000..164a189
--- /dev/null
+++ b/code/api/src/Utilities/QuartzJsonSerializer.cs
@@ -0,0 +1,16 @@
+using Quartz.Spi;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public class QuartzJsonSerializer : IObjectSerializer
+{
+ public void Initialize() { }
+
+ public byte[] Serialize<T>(T obj) where T : class {
+ return JsonSerializer.SerializeToUtf8Bytes(obj);
+ }
+
+ public T DeSerialize<T>(byte[] data) where T : class {
+ return JsonSerializer.Deserialize<T>(data);
+ }
+}
diff --git a/code/api/src/Utilities/QueryableExtensions.cs b/code/api/src/Utilities/QueryableExtensions.cs
new file mode 100644
index 0000000..bf2bf3b
--- /dev/null
+++ b/code/api/src/Utilities/QueryableExtensions.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class QueryableExtensions
+{
+ public static IQueryable<T> ForTenant<T>(this IQueryable<T> queryable, LoggedInUserModel loggedInUserModel) where T : BaseWithOwner {
+ return queryable.Where(c => c.TenantId == loggedInUserModel.TenantId);
+ }
+
+ public static IQueryable<T> ForUser<T>(this IQueryable<T> queryable, LoggedInUserModel loggedInUserModel) where T : BaseWithOwner {
+ return queryable.Where(c => c.UserId == loggedInUserModel.Id);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/SwaggerDefaultValues.cs b/code/api/src/Utilities/SwaggerDefaultValues.cs
new file mode 100644
index 0000000..5e73fa8
--- /dev/null
+++ b/code/api/src/Utilities/SwaggerDefaultValues.cs
@@ -0,0 +1,58 @@
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+/// <summary>
+/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter.
+/// </summary>
+/// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>.
+/// Once they are fixed and published, this class can be removed.</remarks>
+public class SwaggerDefaultValues : IOperationFilter
+{
+ /// <summary>
+ /// Applies the filter to the specified operation using the given context.
+ /// </summary>
+ /// <param name="operation">The operation to apply the filter to.</param>
+ /// <param name="context">The current operation filter context.</param>
+ public void Apply(OpenApiOperation operation, OperationFilterContext context) {
+ var apiDescription = context.ApiDescription;
+
+ operation.Deprecated |= apiDescription.IsDeprecated();
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
+ foreach (var responseType in context.ApiDescription.SupportedResponseTypes) {
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
+ var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
+ var response = operation.Responses[responseKey];
+
+ foreach (var contentType in response.Content.Keys) {
+ if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType)) {
+ response.Content.Remove(contentType);
+ }
+ }
+ }
+
+ if (operation.Parameters == null) {
+ return;
+ }
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
+ foreach (var parameter in operation.Parameters) {
+ var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
+
+ if (parameter.Description == null) {
+ parameter.Description = description.ModelMetadata.Description;
+ }
+
+ if (parameter.Schema.Default == null && description.DefaultValue != null) {
+ // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
+ var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
+ parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
+ }
+
+ parameter.Required |= description.IsRequired;
+ }
+ }
+}
diff --git a/code/api/src/Utilities/SwaggerGenOptionsExtensions.cs b/code/api/src/Utilities/SwaggerGenOptionsExtensions.cs
new file mode 100644
index 0000000..a3d9036
--- /dev/null
+++ b/code/api/src/Utilities/SwaggerGenOptionsExtensions.cs
@@ -0,0 +1,43 @@
+#nullable enable
+using IOL.GreatOffice.Api.Endpoints;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class SwaggerGenOptionsExtensions
+{
+ /// <summary>
+ /// Updates Swagger document to support ApiEndpoints.<br/><br/>
+ /// For controllers inherited from <see cref="EndpointBase"/>:<br/>
+ /// - Replaces action Tag with <c>[namespace]</c><br/>
+ /// </summary>
+ public static void UseApiEndpoints(this SwaggerGenOptions options) {
+ options.TagActionsBy(EndpointNamespaceOrDefault);
+ }
+
+ private static IList<string?> EndpointNamespaceOrDefault(ApiDescription api) {
+ if (api.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) {
+ throw new InvalidOperationException($"Unable to determine tag for endpoint: {api.ActionDescriptor.DisplayName}");
+ }
+
+ if (actionDescriptor.ControllerTypeInfo.GetBaseTypesAndThis().Any(t => t == typeof(EndpointBase))) {
+ return new[] {
+ actionDescriptor.ControllerTypeInfo.Namespace?.Split('.').Last()
+ };
+ }
+
+ return new[] {
+ actionDescriptor.ControllerName
+ };
+ }
+
+ private static IEnumerable<Type> GetBaseTypesAndThis(this Type type) {
+ var current = type;
+ while (current != null) {
+ yield return current;
+ current = current.BaseType;
+ }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/wwwroot/version.txt b/code/api/src/wwwroot/version.txt
new file mode 100644
index 0000000..9cf68da
--- /dev/null
+++ b/code/api/src/wwwroot/version.txt
@@ -0,0 +1 @@
+v47-server-dev
diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs b/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs
new file mode 100644
index 0000000..10525fd
--- /dev/null
+++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs
@@ -0,0 +1,23 @@
+using IOL.GreatOffice.IntegrationTests.Helpers;
+using Xunit;
+
+namespace IOL.GreatOffice.IntegrationTests.ApplicationTests;
+
+public class LoginPageTests : IClassFixture<WebServerFixture>
+{
+ private readonly WebServerFixture _fixture;
+
+ public LoginPageTests(WebServerFixture fixture) {
+ _fixture = fixture;
+ }
+
+ [Fact]
+ public async Task LoginPageTestsRenders() {
+ var page = await _fixture.Browser.NewPageAsync();
+ await page.GotoAsync(_fixture.BaseUrl);
+
+ var actual = await page.TextContentAsync(Element.ByName("Page Title"));
+
+ Assert.Equal("Welcome", actual);
+ }
+}
diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs
new file mode 100644
index 0000000..da83cc3
--- /dev/null
+++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs
@@ -0,0 +1,6 @@
+namespace IOL.GreatOffice.IntegrationTests.Helpers;
+
+public static class Element
+{
+ public static string ByName(string name) => $"[pw-name='{name}']";
+}
diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs
new file mode 100644
index 0000000..080fa9f
--- /dev/null
+++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Net.Sockets;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Playwright;
+using Xunit;
+using Program = IOL.GreatOffice.Api.Program;
+
+namespace IOL.GreatOffice.IntegrationTests.Helpers;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+public class WebServerFixture : IAsyncLifetime, IDisposable
+{
+ private readonly WebApplication Host;
+ private IPlaywright Playwright { get; set; }
+ public IBrowser Browser { get; private set; }
+ public string BaseUrl { get; } = $"https://localhost:{GetRandomUnusedPort()}";
+
+ public WebServerFixture() {
+ Host = Program.CreateWebApplication(Program.CreateAppBuilder(default));
+ }
+
+ public async Task InitializeAsync() {
+ Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ Browser = await Playwright.Chromium.LaunchAsync();
+ await Host.StartAsync();
+ }
+
+ public async Task DisposeAsync() {
+ await Host.StopAsync();
+ await Host.DisposeAsync();
+ Playwright?.Dispose();
+ }
+
+ public void Dispose() {
+ Host.StopAsync();
+ Host.DisposeAsync();
+ Playwright?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private static int GetRandomUnusedPort() {
+ var listener = new TcpListener(IPAddress.Any, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+}
diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj b/code/api/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj
new file mode 100644
index 0000000..0376a10
--- /dev/null
+++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <ImplicitUsings>true</ImplicitUsings>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
+ <PackageReference Include="Microsoft.Playwright" Version="1.22.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\server\IOL.GreatOffice.Api.csproj" />
+ <ProjectReference Include="..\..\server\src\IOL.GreatOffice.Api.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/code/app/.dockerignore b/code/app/.dockerignore
new file mode 100644
index 0000000..00774fa
--- /dev/null
+++ b/code/app/.dockerignore
@@ -0,0 +1,7 @@
+.env
+.env-example
+.svelte-kit
+.git
+static
+node_modules
+build \ No newline at end of file
diff --git a/code/app/.env-example b/code/app/.env-example
new file mode 100644
index 0000000..270860f
--- /dev/null
+++ b/code/app/.env-example
@@ -0,0 +1,4 @@
+VITE_LOG_LEVEL=DEBUG
+VITE_TESTING=true
+VITE_TEST_USERNAME="ms@test.tld"
+VITE_TEST_PASSWORD="test123" \ No newline at end of file
diff --git a/code/app/.gitignore b/code/app/.gitignore
new file mode 100644
index 0000000..f4401a3
--- /dev/null
+++ b/code/app/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
diff --git a/code/app/.npmrc b/code/app/.npmrc
new file mode 100644
index 0000000..b6f27f1
--- /dev/null
+++ b/code/app/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/code/app/.typesafe-i18n.json b/code/app/.typesafe-i18n.json
new file mode 100644
index 0000000..38fa5ca
--- /dev/null
+++ b/code/app/.typesafe-i18n.json
@@ -0,0 +1,5 @@
+{
+ "adapter": "svelte",
+ "$schema": "https://unpkg.com/typesafe-i18n@5.24.0/schema/typesafe-i18n.json",
+ "outputPath": "src/i18n"
+} \ No newline at end of file
diff --git a/code/app/.version b/code/app/.version
new file mode 100644
index 0000000..626799f
--- /dev/null
+++ b/code/app/.version
@@ -0,0 +1 @@
+v1
diff --git a/code/app/.version-dev b/code/app/.version-dev
new file mode 100644
index 0000000..c227083
--- /dev/null
+++ b/code/app/.version-dev
@@ -0,0 +1 @@
+0 \ No newline at end of file
diff --git a/code/app/Dockerfile b/code/app/Dockerfile
new file mode 100644
index 0000000..e381c8d
--- /dev/null
+++ b/code/app/Dockerfile
@@ -0,0 +1,13 @@
+FROM registry.hub.docker.com/library/node:lts-buster-slim AS builder
+WORKDIR .
+COPY package.json .
+RUN npm i
+COPY . .
+RUN npm run build
+FROM registry.hub.docker.com/library/node:lts-buster-slim
+USER node:node
+WORKDIR .
+COPY --from=builder --chown=node:node build build
+COPY --from=builder --chown=node:node node_modules node_modules
+COPY --chown=node:node package.json .
+CMD ["node","build"] \ No newline at end of file
diff --git a/code/app/build_and_push.sh b/code/app/build_and_push.sh
new file mode 100755
index 0000000..6143419
--- /dev/null
+++ b/code/app/build_and_push.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+
+set -Eueo pipefail
+
+CURRENT_DEV_VERSION=$(cat .version-dev)
+CURRENT_DEV_VERSION_INT=${CURRENT_DEV_VERSION//[!0-9]/}
+CURRENT_VERSION=$(cat .version)
+CURRENT_VERSION_INT=${CURRENT_VERSION//[!0-9]/}
+if [ ${1-prod} == "dev" ]; then
+ NEW_VERSION="v$((CURRENT_DEV_VERSION_INT + 1))-dev"
+ OLD_VERSION=$CURRENT_DEV_VERSION
+else
+ NEW_VERSION="v$((CURRENT_VERSION_INT + 1))"
+ OLD_VERSION=$CURRENT_VERSION
+fi
+IMAGE_NAME="greatoffice/app"
+HUB_NAME="dr.ivar.systems/greatoffice/app"
+
+# Check for uncommited changes and optionally commit them
+if [ "$(git status --untracked-files=no --porcelain)" ]; then
+ echo "Unclean git tree! press CTRL+C to exit or press ENTER to commit and push to the default branch"
+ read -n 1
+
+ read -p "Enter commit message: " COMMIT_MESSAGE
+ git add ..
+ git commit --quiet -m "$COMMIT_MESSAGE"
+fi
+
+if [ ${1-prod} == "dev" ]; then
+ echo $NEW_VERSION >|.version-dev
+ git add .version-dev
+else
+ echo $NEW_VERSION >|.version
+ git add .version
+fi
+
+echo "Starting build of $HUB_NAME:$NEW_VERSION at $(date -u)..."
+echo
+
+# Put version.txt inside of server
+pushd static
+echo "$NEW_VERSION" >version.txt
+git add version.txt
+popd
+
+git commit --quiet -m "chore(release): Bump version"
+
+read -p "Do you want to tag this build? (y/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ read -p "Enter tag message (can be empty): " TAG_MESSAGE
+ git tag -am "$TAG_MESSAGE" $NEW_VERSION
+fi
+
+read -p "Do you want to push the latest commits and tags to origin? (y/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Pushing latest changes to remotes..."
+ echo
+ git push --quiet --follow-tags
+fi
+
+# Build docker image
+echo "Building docker image"
+echo
+
+docker build -t $IMAGE_NAME:$NEW_VERSION .
+
+docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:$NEW_VERSION
+
+if [ ${1-prod} == "dev" ]; then
+ docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest-dev
+fi
+if [ ${1-prod} == "prod" ]; then
+ docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest
+fi
+
+# Optionally push images to docker registry
+echo "Press CTRL+C to exit or press ENTER to push docker image to registry"
+read -n 1
+docker push $HUB_NAME:$NEW_VERSION
+
+if [ ${1-prod} == "dev" ]; then
+ docker push $HUB_NAME:latest-dev
+fi
+
+if [ ${1-prod} == "prod" ]; then
+ docker push $HUB_NAME:latest
+fi
diff --git a/code/app/package.json b/code/app/package.json
new file mode 100644
index 0000000..a868844
--- /dev/null
+++ b/code/app/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "greatoffice-kit",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "npm-run-all --parallel vite typesafe-i18n",
+ "typesafe-i18n": "typesafe-i18n",
+ "vite": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "test": "playwright test"
+ },
+ "devDependencies": {
+ "@faker-js/faker": "^7.6.0",
+ "@playwright/test": "^1.30.0",
+ "@sveltejs/adapter-node": "1.1.7",
+ "@sveltejs/kit": "1.5.0",
+ "@tailwindcss/forms": "^0.5.3",
+ "@types/js-cookie": "^3.0.2",
+ "@vite-pwa/sveltekit": "^0.1.3",
+ "autoprefixer": "^10.4.13",
+ "npm-run-all": "^4.1.5",
+ "pino-pretty": "^9.1.1",
+ "postcss": "^8.4.21",
+ "postcss-load-config": "^4.0.1",
+ "svelte": "^3.55.1",
+ "svelte-check": "^3.0.3",
+ "svelte-preprocess": "^5.0.1",
+ "tailwindcss": "^3.2.6",
+ "tslib": "^2.5.0",
+ "typesafe-i18n": "^5.24.0",
+ "typescript": "^4.9.5",
+ "vite": "^4.1.1",
+ "vite-plugin-pwa": "^0.14.3"
+ },
+ "dependencies": {
+ "@developermuch/dev-svelte-headlessui": "0.0.1",
+ "@rgossiaux/svelte-headlessui": "^1.0.2",
+ "fuzzysort": "^2.0.4",
+ "js-cookie": "^3.0.1",
+ "pino": "^8.9.0",
+ "pino-seq": "^0.9.0",
+ "svelte-headless-table": "^0.17.2",
+ "temporal-polyfill": "^0.0.8",
+ "turbo-query": "^1.9.0"
+ }
+} \ No newline at end of file
diff --git a/code/app/playwright.config.ts b/code/app/playwright.config.ts
new file mode 100644
index 0000000..22c46d9
--- /dev/null
+++ b/code/app/playwright.config.ts
@@ -0,0 +1,10 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+
+const config: PlaywrightTestConfig = {
+ webServer: {
+ command: 'pnpm run build && pnpm run preview',
+ port: 4173
+ }
+};
+
+export default config;
diff --git a/code/app/pnpm-lock.yaml b/code/app/pnpm-lock.yaml
new file mode 100644
index 0000000..04e088e
--- /dev/null
+++ b/code/app/pnpm-lock.yaml
@@ -0,0 +1,4389 @@
+lockfileVersion: 5.4
+
+specifiers:
+ '@developermuch/dev-svelte-headlessui': 0.0.1
+ '@faker-js/faker': ^7.6.0
+ '@playwright/test': ^1.30.0
+ '@rgossiaux/svelte-headlessui': ^1.0.2
+ '@sveltejs/adapter-node': 1.1.7
+ '@sveltejs/kit': 1.5.0
+ '@tailwindcss/forms': ^0.5.3
+ '@types/js-cookie': ^3.0.2
+ '@vite-pwa/sveltekit': ^0.1.3
+ autoprefixer: ^10.4.13
+ fuzzysort: ^2.0.4
+ js-cookie: ^3.0.1
+ npm-run-all: ^4.1.5
+ pino: ^8.9.0
+ pino-pretty: ^9.1.1
+ pino-seq: ^0.9.0
+ postcss: ^8.4.21
+ postcss-load-config: ^4.0.1
+ svelte: ^3.55.1
+ svelte-check: ^3.0.3
+ svelte-headless-table: ^0.17.2
+ svelte-preprocess: ^5.0.1
+ tailwindcss: ^3.2.6
+ temporal-polyfill: ^0.0.8
+ tslib: ^2.5.0
+ turbo-query: ^1.9.0
+ typesafe-i18n: ^5.24.0
+ typescript: ^4.9.5
+ vite: ^4.1.1
+ vite-plugin-pwa: ^0.14.3
+
+dependencies:
+ '@developermuch/dev-svelte-headlessui': 0.0.1_svelte@3.55.1
+ '@rgossiaux/svelte-headlessui': 1.0.2_svelte@3.55.1
+ fuzzysort: 2.0.4
+ js-cookie: 3.0.1
+ pino: 8.9.0
+ pino-seq: 0.9.0
+ svelte-headless-table: 0.17.2_svelte@3.55.1
+ temporal-polyfill: 0.0.8
+ turbo-query: 1.9.0
+
+devDependencies:
+ '@faker-js/faker': 7.6.0
+ '@playwright/test': 1.30.0
+ '@sveltejs/adapter-node': 1.1.7_@sveltejs+kit@1.5.0
+ '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1
+ '@tailwindcss/forms': 0.5.3_tailwindcss@3.2.6
+ '@types/js-cookie': 3.0.2
+ '@vite-pwa/sveltekit': 0.1.3_kkbswr7m3b4a2hnxfqban7fxma
+ autoprefixer: 10.4.13_postcss@8.4.21
+ npm-run-all: 4.1.5
+ pino-pretty: 9.1.1
+ postcss: 8.4.21
+ postcss-load-config: 4.0.1_postcss@8.4.21
+ svelte: 3.55.1
+ svelte-check: 3.0.3_gqx7lw3sljhsd4bstor5m2aa2u
+ svelte-preprocess: 5.0.1_b25a55hc532q56kmuqlrolam2i
+ tailwindcss: 3.2.6_postcss@8.4.21
+ tslib: 2.5.0
+ typesafe-i18n: 5.24.0_typescript@4.9.5
+ typescript: 4.9.5
+ vite: 4.1.1
+ vite-plugin-pwa: 0.14.3_vite@4.1.1
+
+packages:
+
+ /@ampproject/remapping/2.2.0:
+ resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.1.1
+ '@jridgewell/trace-mapping': 0.3.17
+ dev: true
+
+ /@apideck/better-ajv-errors/0.3.6_ajv@8.12.0:
+ resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ ajv: '>=8'
+ dependencies:
+ ajv: 8.12.0
+ json-schema: 0.4.0
+ jsonpointer: 5.0.1
+ leven: 3.1.0
+ dev: true
+
+ /@babel/code-frame/7.18.6:
+ resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/highlight': 7.18.6
+ dev: true
+
+ /@babel/compat-data/7.20.14:
+ resolution: {integrity: sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/core/7.20.12:
+ resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@ampproject/remapping': 2.2.0
+ '@babel/code-frame': 7.18.6
+ '@babel/generator': 7.20.14
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-module-transforms': 7.20.11
+ '@babel/helpers': 7.20.13
+ '@babel/parser': 7.20.15
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.20.13
+ '@babel/types': 7.20.7
+ convert-source-map: 1.9.0
+ debug: 4.3.4
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/generator/7.20.14:
+ resolution: {integrity: sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ '@jridgewell/gen-mapping': 0.3.2
+ jsesc: 2.5.2
+ dev: true
+
+ /@babel/helper-annotate-as-pure/7.18.6:
+ resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9:
+ resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-explode-assignable-expression': 7.18.6
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/compat-data': 7.20.14
+ '@babel/core': 7.20.12
+ '@babel/helper-validator-option': 7.18.6
+ browserslist: 4.21.5
+ lru-cache: 5.1.1
+ semver: 6.3.0
+ dev: true
+
+ /@babel/helper-create-class-features-plugin/7.20.12_@babel+core@7.20.12:
+ resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-member-expression-to-functions': 7.20.7
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/helper-replace-supers': 7.20.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ '@babel/helper-split-export-declaration': 7.18.6
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-create-regexp-features-plugin/7.20.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-annotate-as-pure': 7.18.6
+ regexpu-core: 5.3.0
+ dev: true
+
+ /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==}
+ peerDependencies:
+ '@babel/core': ^7.4.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ debug: 4.3.4
+ lodash.debounce: 4.0.8
+ resolve: 1.22.1
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-environment-visitor/7.18.9:
+ resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-explode-assignable-expression/7.18.6:
+ resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-function-name/7.19.0:
+ resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.20.7
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-hoist-variables/7.18.6:
+ resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-member-expression-to-functions/7.20.7:
+ resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-module-imports/7.18.6:
+ resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-module-transforms/7.20.11:
+ resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/helper-simple-access': 7.20.2
+ '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/helper-validator-identifier': 7.19.1
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.20.13
+ '@babel/types': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-optimise-call-expression/7.18.6:
+ resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-plugin-utils/7.20.2:
+ resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-wrap-function': 7.20.5
+ '@babel/types': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-replace-supers/7.20.7:
+ resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-member-expression-to-functions': 7.20.7
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.20.13
+ '@babel/types': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-simple-access/7.20.2:
+ resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-skip-transparent-expression-wrappers/7.20.0:
+ resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-split-export-declaration/7.18.6:
+ resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/helper-string-parser/7.19.4:
+ resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-validator-identifier/7.19.1:
+ resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-validator-option/7.18.6:
+ resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-wrap-function/7.20.5:
+ resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-function-name': 7.19.0
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.20.13
+ '@babel/types': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helpers/7.20.13:
+ resolution: {integrity: sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.20.7
+ '@babel/traverse': 7.20.13
+ '@babel/types': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/highlight/7.18.6:
+ resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-validator-identifier': 7.19.1
+ chalk: 2.4.2
+ js-tokens: 4.0.0
+ dev: true
+
+ /@babel/parser/7.20.15:
+ resolution: {integrity: sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ '@babel/plugin-proposal-optional-chaining': 7.20.7_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-class-static-block/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.14
+ '@babel/core': 7.20.12
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-optional-chaining/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.12
+ dev: true
+
+ /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-private-property-in-object/7.20.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.20.12:
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.12:
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-import-assertions/7.20.0_@babel+core@7.20.12:
+ resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.12:
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.20.12:
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-arrow-functions/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-async-to-generator/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-block-scoping/7.20.15_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-classes/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-replace-supers': 7.20.7
+ '@babel/helper-split-export-declaration': 7.18.6
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-computed-properties/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/template': 7.20.7
+ dev: true
+
+ /@babel/plugin-transform-destructuring/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.20.12:
+ resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-literals/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-modules-amd/7.20.11_@babel+core@7.20.12:
+ resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-module-transforms': 7.20.11
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-commonjs/7.20.11_@babel+core@7.20.12:
+ resolution: {integrity: sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-module-transforms': 7.20.11
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-simple-access': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-systemjs/7.20.11_@babel+core@7.20.12:
+ resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-hoist-variables': 7.18.6
+ '@babel/helper-module-transforms': 7.20.11
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-validator-identifier': 7.19.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-module-transforms': 7.20.11
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-named-capturing-groups-regex/7.20.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-replace-supers': 7.20.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-parameters/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-regenerator/7.20.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ regenerator-transform: 0.15.1
+ dev: true
+
+ /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-spread/7.20.7_@babel+core@7.20.12:
+ resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ dev: true
+
+ /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.20.12:
+ resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.20.12:
+ resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.20.12:
+ resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/preset-env/7.20.2_@babel+core@7.20.12:
+ resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.14
+ '@babel/core': 7.20.12
+ '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-validator-option': 7.18.6
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-class-static-block': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-logical-assignment-operators': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-object-rest-spread': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-optional-chaining': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-proposal-private-property-in-object': 7.20.5_@babel+core@7.20.12
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12
+ '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.12
+ '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-import-assertions': 7.20.0_@babel+core@7.20.12
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.12
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12
+ '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.12
+ '@babel/plugin-transform-arrow-functions': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-async-to-generator': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-block-scoping': 7.20.15_@babel+core@7.20.12
+ '@babel/plugin-transform-classes': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-computed-properties': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-destructuring': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.20.12
+ '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-modules-amd': 7.20.11_@babel+core@7.20.12
+ '@babel/plugin-transform-modules-commonjs': 7.20.11_@babel+core@7.20.12
+ '@babel/plugin-transform-modules-systemjs': 7.20.11_@babel+core@7.20.12
+ '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5_@babel+core@7.20.12
+ '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-regenerator': 7.20.5_@babel+core@7.20.12
+ '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-spread': 7.20.7_@babel+core@7.20.12
+ '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.20.12
+ '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.12
+ '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/preset-modules': 0.1.5_@babel+core@7.20.12
+ '@babel/types': 7.20.7
+ babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.12
+ babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12
+ babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12
+ core-js-compat: 3.27.2
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/preset-modules/0.1.5_@babel+core@7.20.12:
+ resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12
+ '@babel/types': 7.20.7
+ esutils: 2.0.3
+ dev: true
+
+ /@babel/regjsgen/0.8.0:
+ resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
+ dev: true
+
+ /@babel/runtime/7.20.13:
+ resolution: {integrity: sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.13.11
+ dev: true
+
+ /@babel/template/7.20.7:
+ resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ '@babel/parser': 7.20.15
+ '@babel/types': 7.20.7
+ dev: true
+
+ /@babel/traverse/7.20.13:
+ resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ '@babel/generator': 7.20.14
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-hoist-variables': 7.18.6
+ '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/parser': 7.20.15
+ '@babel/types': 7.20.7
+ debug: 4.3.4
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/types/7.20.7:
+ resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.19.4
+ '@babel/helper-validator-identifier': 7.19.1
+ to-fast-properties: 2.0.0
+ dev: true
+
+ /@developermuch/dev-svelte-headlessui/0.0.1_svelte@3.55.1:
+ resolution: {integrity: sha512-tfBlHliv75oQFRrC430nIsw+A8+iFmr5c2g0A+VTlVD3960nEL9jOE0LDHYKq6VhX5LnOLTFIZwVKC1DxFo0QA==}
+ peerDependencies:
+ svelte: ^3.44.0
+ dependencies:
+ svelte: 3.55.1
+ dev: false
+
+ /@esbuild/android-arm/0.16.17:
+ resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-arm64/0.16.17:
+ resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/android-x64/0.16.17:
+ resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-arm64/0.16.17:
+ resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/darwin-x64/0.16.17:
+ resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-arm64/0.16.17:
+ resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/freebsd-x64/0.16.17:
+ resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm/0.16.17:
+ resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-arm64/0.16.17:
+ resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ia32/0.16.17:
+ resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-loong64/0.16.17:
+ resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-mips64el/0.16.17:
+ resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-ppc64/0.16.17:
+ resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-riscv64/0.16.17:
+ resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-s390x/0.16.17:
+ resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/linux-x64/0.16.17:
+ resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/netbsd-x64/0.16.17:
+ resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/openbsd-x64/0.16.17:
+ resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/sunos-x64/0.16.17:
+ resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-arm64/0.16.17:
+ resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-ia32/0.16.17:
+ resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@esbuild/win32-x64/0.16.17:
+ resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@faker-js/faker/7.6.0:
+ resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==}
+ engines: {node: '>=14.0.0', npm: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/gen-mapping/0.1.1:
+ resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.1.2
+ '@jridgewell/sourcemap-codec': 1.4.14
+ dev: true
+
+ /@jridgewell/gen-mapping/0.3.2:
+ resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.1.2
+ '@jridgewell/sourcemap-codec': 1.4.14
+ '@jridgewell/trace-mapping': 0.3.17
+ dev: true
+
+ /@jridgewell/resolve-uri/3.1.0:
+ resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/set-array/1.1.2:
+ resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/source-map/0.3.2:
+ resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.2
+ '@jridgewell/trace-mapping': 0.3.17
+ dev: true
+
+ /@jridgewell/sourcemap-codec/1.4.14:
+ resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
+ dev: true
+
+ /@jridgewell/trace-mapping/0.3.17:
+ resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.0
+ '@jridgewell/sourcemap-codec': 1.4.14
+ dev: true
+
+ /@nodelib/fs.scandir/2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat/2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk/1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.15.0
+ dev: true
+
+ /@playwright/test/1.30.0:
+ resolution: {integrity: sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ '@types/node': 18.13.0
+ playwright-core: 1.30.0
+ dev: true
+
+ /@polka/url/1.0.0-next.21:
+ resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
+ dev: true
+
+ /@rgossiaux/svelte-headlessui/1.0.2_svelte@3.55.1:
+ resolution: {integrity: sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==}
+ peerDependencies:
+ svelte: ^3.44.0
+ dependencies:
+ svelte: 3.55.1
+ dev: false
+
+ /@rollup/plugin-babel/5.3.1_3dsfpkpoyvuuxyfgdbpn4j4uzm:
+ resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
+ engines: {node: '>= 10.0.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ '@types/babel__core': ^7.1.9
+ rollup: ^1.20.0||^2.0.0
+ peerDependenciesMeta:
+ '@types/babel__core':
+ optional: true
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-module-imports': 7.18.6
+ '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ rollup: 2.79.1
+ dev: true
+
+ /@rollup/plugin-commonjs/24.0.1_rollup@3.14.0:
+ resolution: {integrity: sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^2.68.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@rollup/pluginutils': 5.0.2_rollup@3.14.0
+ commondir: 1.0.1
+ estree-walker: 2.0.2
+ glob: 8.1.0
+ is-reference: 1.2.1
+ magic-string: 0.27.0
+ rollup: 3.14.0
+ dev: true
+
+ /@rollup/plugin-json/6.0.0_rollup@3.14.0:
+ resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@rollup/pluginutils': 5.0.2_rollup@3.14.0
+ rollup: 3.14.0
+ dev: true
+
+ /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.1:
+ resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
+ engines: {node: '>= 10.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0
+ dependencies:
+ '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ '@types/resolve': 1.17.1
+ builtin-modules: 3.3.0
+ deepmerge: 4.3.0
+ is-module: 1.0.0
+ resolve: 1.22.1
+ rollup: 2.79.1
+ dev: true
+
+ /@rollup/plugin-node-resolve/15.0.1_rollup@3.14.0:
+ resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^2.78.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@rollup/pluginutils': 5.0.2_rollup@3.14.0
+ '@types/resolve': 1.20.2
+ deepmerge: 4.3.0
+ is-builtin-module: 3.2.1
+ is-module: 1.0.0
+ resolve: 1.22.1
+ rollup: 3.14.0
+ dev: true
+
+ /@rollup/plugin-replace/2.4.2_rollup@2.79.1:
+ resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
+ peerDependencies:
+ rollup: ^1.20.0 || ^2.0.0
+ dependencies:
+ '@rollup/pluginutils': 3.1.0_rollup@2.79.1
+ magic-string: 0.25.9
+ rollup: 2.79.1
+ dev: true
+
+ /@rollup/plugin-replace/5.0.2_rollup@3.14.0:
+ resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@rollup/pluginutils': 5.0.2_rollup@3.14.0
+ magic-string: 0.27.0
+ rollup: 3.14.0
+ dev: true
+
+ /@rollup/pluginutils/3.1.0_rollup@2.79.1:
+ resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
+ engines: {node: '>= 8.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0
+ dependencies:
+ '@types/estree': 0.0.39
+ estree-walker: 1.0.1
+ picomatch: 2.3.1
+ rollup: 2.79.1
+ dev: true
+
+ /@rollup/pluginutils/5.0.2_rollup@3.14.0:
+ resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ dependencies:
+ '@types/estree': 1.0.0
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+ rollup: 3.14.0
+ dev: true
+
+ /@surma/rollup-plugin-off-main-thread/2.2.3:
+ resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
+ dependencies:
+ ejs: 3.1.8
+ json5: 2.2.3
+ magic-string: 0.25.9
+ string.prototype.matchall: 4.0.8
+ dev: true
+
+ /@sveltejs/adapter-node/1.1.7_@sveltejs+kit@1.5.0:
+ resolution: {integrity: sha512-N93uYDzH8+RlneYdfgS33nudVk9e8JhYy9vPOsJKeMCkWPb5ejJ0D+LUAEuyA1BOyA7ZkaVTupLStAG4nuhT2A==}
+ peerDependencies:
+ '@sveltejs/kit': ^1.0.0
+ dependencies:
+ '@rollup/plugin-commonjs': 24.0.1_rollup@3.14.0
+ '@rollup/plugin-json': 6.0.0_rollup@3.14.0
+ '@rollup/plugin-node-resolve': 15.0.1_rollup@3.14.0
+ '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1
+ rollup: 3.14.0
+ dev: true
+
+ /@sveltejs/kit/1.5.0_svelte@3.55.1+vite@4.1.1:
+ resolution: {integrity: sha512-AkWgCO9i2djZjTqCgIQJ5XfnSzRINowh2w2Gk9wDRuTwxKizSuYe3jNvds/HCDDGHo8XE5E0yWNC9j2XxbrX+g==}
+ engines: {node: ^16.14 || >=18}
+ hasBin: true
+ requiresBuild: true
+ peerDependencies:
+ svelte: ^3.54.0
+ vite: ^4.0.0
+ dependencies:
+ '@sveltejs/vite-plugin-svelte': 2.0.2_svelte@3.55.1+vite@4.1.1
+ '@types/cookie': 0.5.1
+ cookie: 0.5.0
+ devalue: 4.2.3
+ esm-env: 1.0.0
+ kleur: 4.1.5
+ magic-string: 0.27.0
+ mime: 3.0.0
+ sade: 1.8.1
+ set-cookie-parser: 2.5.1
+ sirv: 2.0.2
+ svelte: 3.55.1
+ tiny-glob: 0.2.9
+ undici: 5.18.0
+ vite: 4.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@sveltejs/vite-plugin-svelte/2.0.2_svelte@3.55.1+vite@4.1.1:
+ resolution: {integrity: sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==}
+ engines: {node: ^14.18.0 || >= 16}
+ peerDependencies:
+ svelte: ^3.54.0
+ vite: ^4.0.0
+ dependencies:
+ debug: 4.3.4
+ deepmerge: 4.3.0
+ kleur: 4.1.5
+ magic-string: 0.27.0
+ svelte: 3.55.1
+ svelte-hmr: 0.15.1_svelte@3.55.1
+ vite: 4.1.1
+ vitefu: 0.2.4_vite@4.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@tailwindcss/forms/0.5.3_tailwindcss@3.2.6:
+ resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
+ dependencies:
+ mini-svg-data-uri: 1.4.4
+ tailwindcss: 3.2.6_postcss@8.4.21
+ dev: true
+
+ /@types/cookie/0.5.1:
+ resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
+ dev: true
+
+ /@types/estree/0.0.39:
+ resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
+ dev: true
+
+ /@types/estree/1.0.0:
+ resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
+ dev: true
+
+ /@types/events/3.0.0:
+ resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
+ dev: false
+
+ /@types/js-cookie/3.0.2:
+ resolution: {integrity: sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==}
+ dev: true
+
+ /@types/node/18.13.0:
+ resolution: {integrity: sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==}
+
+ /@types/pino/5.20.0:
+ resolution: {integrity: sha512-gz3Ahvx1UDEveXViOQtYqnUkjSVQFdoJqpZTW/63spEHwOGRJRJIi3JMJSClp5Sk1x1ljn9tHWjGczmP6s/rvg==}
+ dependencies:
+ '@types/events': 3.0.0
+ '@types/node': 18.13.0
+ dev: false
+
+ /@types/pug/2.0.6:
+ resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
+ dev: true
+
+ /@types/resolve/1.17.1:
+ resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
+ dependencies:
+ '@types/node': 18.13.0
+ dev: true
+
+ /@types/resolve/1.20.2:
+ resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
+ dev: true
+
+ /@types/sass/1.43.1:
+ resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
+ dependencies:
+ '@types/node': 18.13.0
+ dev: true
+
+ /@types/trusted-types/2.0.2:
+ resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
+ dev: true
+
+ /@vite-pwa/sveltekit/0.1.3_kkbswr7m3b4a2hnxfqban7fxma:
+ resolution: {integrity: sha512-wOo8riFow/eT+5UWyOcfO3ol72+2bT2fHDwd+sryqqriCWbRxOSUBKivVLlqzyna0QA0sNkM1jDGjJShpwq7aQ==}
+ engines: {node: '>=16.14'}
+ peerDependencies:
+ '@sveltejs/kit': ^1.0.0
+ vite-plugin-pwa: ^0.14.0
+ dependencies:
+ '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1
+ vite-plugin-pwa: 0.14.3_vite@4.1.1
+ dev: true
+
+ /abort-controller/3.0.0:
+ resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+ engines: {node: '>=6.5'}
+ dependencies:
+ event-target-shim: 5.0.1
+
+ /acorn-node/1.8.2:
+ resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==}
+ dependencies:
+ acorn: 7.4.1
+ acorn-walk: 7.2.0
+ xtend: 4.0.2
+ dev: true
+
+ /acorn-walk/7.2.0:
+ resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
+ engines: {node: '>=0.4.0'}
+ dev: true
+
+ /acorn/7.4.1:
+ resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: true
+
+ /acorn/8.8.2:
+ resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: true
+
+ /ajv/8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: true
+
+ /ansi-styles/3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+ dependencies:
+ color-convert: 1.9.3
+ dev: true
+
+ /ansi-styles/4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: true
+
+ /anymatch/3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: true
+
+ /arg/5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+ dev: true
+
+ /async/3.2.4:
+ resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
+ dev: true
+
+ /at-least-node/1.0.0:
+ resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
+ engines: {node: '>= 4.0.0'}
+ dev: true
+
+ /atomic-sleep/1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+
+ /autoprefixer/10.4.13_postcss@8.4.21:
+ resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ browserslist: 4.21.5
+ caniuse-lite: 1.0.30001451
+ fraction.js: 4.2.0
+ normalize-range: 0.1.2
+ picocolors: 1.0.0
+ postcss: 8.4.21
+ postcss-value-parser: 4.2.0
+ dev: true
+
+ /available-typed-arrays/1.0.5:
+ resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.20.12:
+ resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.14
+ '@babel/core': 7.20.12
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.20.12:
+ resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12
+ core-js-compat: 3.27.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.20.12:
+ resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.12
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /balanced-match/1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: true
+
+ /base64-js/1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ /binary-extensions/2.2.0:
+ resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /brace-expansion/1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: true
+
+ /brace-expansion/2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: true
+
+ /braces/3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /browserslist/4.21.5:
+ resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+ dependencies:
+ caniuse-lite: 1.0.30001451
+ electron-to-chromium: 1.4.289
+ node-releases: 2.0.10
+ update-browserslist-db: 1.0.10_browserslist@4.21.5
+ dev: true
+
+ /buffer-crc32/0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ dev: true
+
+ /buffer-from/1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ dev: true
+
+ /buffer/6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ /builtin-modules/3.3.0:
+ resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /busboy/1.6.0:
+ resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
+ engines: {node: '>=10.16.0'}
+ dependencies:
+ streamsearch: 1.1.0
+ dev: true
+
+ /call-bind/1.0.2:
+ resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
+ dependencies:
+ function-bind: 1.1.1
+ get-intrinsic: 1.2.0
+ dev: true
+
+ /callsites/3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /camelcase-css/2.0.1:
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /caniuse-lite/1.0.30001451:
+ resolution: {integrity: sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==}
+ dev: true
+
+ /chalk/2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ dev: true
+
+ /chalk/4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+ dev: true
+
+ /chokidar/3.5.3:
+ resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.2
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /color-convert/1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+ dependencies:
+ color-name: 1.1.3
+ dev: true
+
+ /color-convert/2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
+ /color-name/1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+ dev: true
+
+ /color-name/1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: true
+
+ /colorette/2.0.19:
+ resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
+ dev: true
+
+ /commander/2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ /common-tags/1.8.2:
+ resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
+ engines: {node: '>=4.0.0'}
+ dev: true
+
+ /commondir/1.0.1:
+ resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
+ dev: true
+
+ /concat-map/0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: true
+
+ /convert-source-map/1.9.0:
+ resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+ dev: true
+
+ /cookie/0.5.0:
+ resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /core-js-compat/3.27.2:
+ resolution: {integrity: sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==}
+ dependencies:
+ browserslist: 4.21.5
+ dev: true
+
+ /cross-spawn/6.0.5:
+ resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
+ engines: {node: '>=4.8'}
+ dependencies:
+ nice-try: 1.0.5
+ path-key: 2.0.1
+ semver: 5.7.1
+ shebang-command: 1.2.0
+ which: 1.3.1
+ dev: true
+
+ /crypto-random-string/2.0.0:
+ resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /cssesc/3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /dateformat/4.6.3:
+ resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
+ dev: true
+
+ /debug/4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+ dev: true
+
+ /deepmerge/4.3.0:
+ resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /define-properties/1.1.4:
+ resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-property-descriptors: 1.0.0
+ object-keys: 1.1.1
+ dev: true
+
+ /defined/1.0.1:
+ resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==}
+ dev: true
+
+ /detect-indent/6.1.0:
+ resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /detective/5.2.1:
+ resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==}
+ engines: {node: '>=0.8.0'}
+ hasBin: true
+ dependencies:
+ acorn-node: 1.8.2
+ defined: 1.0.1
+ minimist: 1.2.7
+ dev: true
+
+ /devalue/4.2.3:
+ resolution: {integrity: sha512-JG6Q248aN0pgFL57e3zqTVeFraBe+5W2ugvv1mLXsJP6YYIYJhRZhAl7QP8haJrqob6X10F9NEkuCvNILZTPeQ==}
+ dev: true
+
+ /didyoumean/1.2.2:
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ dev: true
+
+ /dlv/1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+ dev: true
+
+ /ejs/3.1.8:
+ resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+ dependencies:
+ jake: 10.8.5
+ dev: true
+
+ /electron-to-chromium/1.4.289:
+ resolution: {integrity: sha512-relLdMfPBxqGCxy7Gyfm1HcbRPcFUJdlgnCPVgQ23sr1TvUrRJz0/QPoGP0+x41wOVSTN/Wi3w6YDgHiHJGOzg==}
+ dev: true
+
+ /end-of-stream/1.4.4:
+ resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ dependencies:
+ once: 1.4.0
+ dev: true
+
+ /error-ex/1.3.2:
+ resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+ dependencies:
+ is-arrayish: 0.2.1
+ dev: true
+
+ /es-abstract/1.21.1:
+ resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.2
+ es-set-tostringtag: 2.0.1
+ es-to-primitive: 1.2.1
+ function-bind: 1.1.1
+ function.prototype.name: 1.1.5
+ get-intrinsic: 1.2.0
+ get-symbol-description: 1.0.0
+ globalthis: 1.0.3
+ gopd: 1.0.1
+ has: 1.0.3
+ has-property-descriptors: 1.0.0
+ has-proto: 1.0.1
+ has-symbols: 1.0.3
+ internal-slot: 1.0.4
+ is-array-buffer: 3.0.1
+ is-callable: 1.2.7
+ is-negative-zero: 2.0.2
+ is-regex: 1.1.4
+ is-shared-array-buffer: 1.0.2
+ is-string: 1.0.7
+ is-typed-array: 1.1.10
+ is-weakref: 1.0.2
+ object-inspect: 1.12.3
+ object-keys: 1.1.1
+ object.assign: 4.1.4
+ regexp.prototype.flags: 1.4.3
+ safe-regex-test: 1.0.0
+ string.prototype.trimend: 1.0.6
+ string.prototype.trimstart: 1.0.6
+ typed-array-length: 1.0.4
+ unbox-primitive: 1.0.2
+ which-typed-array: 1.1.9
+ dev: true
+
+ /es-set-tostringtag/2.0.1:
+ resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.2.0
+ has: 1.0.3
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /es-to-primitive/1.2.1:
+ resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.0.5
+ is-symbol: 1.0.4
+ dev: true
+
+ /es6-promise/3.3.1:
+ resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
+ dev: true
+
+ /esbuild/0.16.17:
+ resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.16.17
+ '@esbuild/android-arm64': 0.16.17
+ '@esbuild/android-x64': 0.16.17
+ '@esbuild/darwin-arm64': 0.16.17
+ '@esbuild/darwin-x64': 0.16.17
+ '@esbuild/freebsd-arm64': 0.16.17
+ '@esbuild/freebsd-x64': 0.16.17
+ '@esbuild/linux-arm': 0.16.17
+ '@esbuild/linux-arm64': 0.16.17
+ '@esbuild/linux-ia32': 0.16.17
+ '@esbuild/linux-loong64': 0.16.17
+ '@esbuild/linux-mips64el': 0.16.17
+ '@esbuild/linux-ppc64': 0.16.17
+ '@esbuild/linux-riscv64': 0.16.17
+ '@esbuild/linux-s390x': 0.16.17
+ '@esbuild/linux-x64': 0.16.17
+ '@esbuild/netbsd-x64': 0.16.17
+ '@esbuild/openbsd-x64': 0.16.17
+ '@esbuild/sunos-x64': 0.16.17
+ '@esbuild/win32-arm64': 0.16.17
+ '@esbuild/win32-ia32': 0.16.17
+ '@esbuild/win32-x64': 0.16.17
+ dev: true
+
+ /escalade/3.1.1:
+ resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /escape-string-regexp/1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+ dev: true
+
+ /esm-env/1.0.0:
+ resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==}
+ dev: true
+
+ /estree-walker/1.0.1:
+ resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
+ dev: true
+
+ /estree-walker/2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ dev: true
+
+ /esutils/2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /event-target-shim/5.0.1:
+ resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+ engines: {node: '>=6'}
+
+ /events/3.3.0:
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+ engines: {node: '>=0.8.x'}
+
+ /fast-copy/3.0.0:
+ resolution: {integrity: sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==}
+ dev: true
+
+ /fast-deep-equal/3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: true
+
+ /fast-glob/3.2.12:
+ resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
+ /fast-json-stable-stringify/2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ dev: true
+
+ /fast-redact/3.1.2:
+ resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==}
+ engines: {node: '>=6'}
+ dev: false
+
+ /fast-safe-stringify/2.1.1:
+ resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+ dev: true
+
+ /fastq/1.15.0:
+ resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
+ /filelist/1.0.4:
+ resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
+ dependencies:
+ minimatch: 5.1.6
+ dev: true
+
+ /fill-range/7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /for-each/0.3.3:
+ resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
+ dependencies:
+ is-callable: 1.2.7
+ dev: true
+
+ /fraction.js/4.2.0:
+ resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
+ dev: true
+
+ /fs-extra/9.1.0:
+ resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ at-least-node: 1.0.0
+ graceful-fs: 4.2.10
+ jsonfile: 6.1.0
+ universalify: 2.0.0
+ dev: true
+
+ /fs.realpath/1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ dev: true
+
+ /fsevents/2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /function-bind/1.1.1:
+ resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+ dev: true
+
+ /function.prototype.name/1.1.5:
+ resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.21.1
+ functions-have-names: 1.2.3
+ dev: true
+
+ /functions-have-names/1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+ dev: true
+
+ /fuzzysort/2.0.4:
+ resolution: {integrity: sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==}
+ dev: false
+
+ /gensync/1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /get-intrinsic/1.2.0:
+ resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==}
+ dependencies:
+ function-bind: 1.1.1
+ has: 1.0.3
+ has-symbols: 1.0.3
+ dev: true
+
+ /get-own-enumerable-property-symbols/3.0.2:
+ resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
+ dev: true
+
+ /get-symbol-description/1.0.0:
+ resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.0
+ dev: true
+
+ /glob-parent/5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob-parent/6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob/7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+ dev: true
+
+ /glob/8.1.0:
+ resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 5.1.6
+ once: 1.4.0
+ dev: true
+
+ /globals/11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /globalthis/1.0.3:
+ resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ define-properties: 1.1.4
+ dev: true
+
+ /globalyzer/0.1.0:
+ resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
+ dev: true
+
+ /globrex/0.1.2:
+ resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
+ dev: true
+
+ /gopd/1.0.1:
+ resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
+ dependencies:
+ get-intrinsic: 1.2.0
+ dev: true
+
+ /graceful-fs/4.2.10:
+ resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
+ dev: true
+
+ /has-bigints/1.0.2:
+ resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
+ dev: true
+
+ /has-flag/3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /has-flag/4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /has-property-descriptors/1.0.0:
+ resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
+ dependencies:
+ get-intrinsic: 1.2.0
+ dev: true
+
+ /has-proto/1.0.1:
+ resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /has-symbols/1.0.3:
+ resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /has-tostringtag/1.0.0:
+ resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-symbols: 1.0.3
+ dev: true
+
+ /has/1.0.3:
+ resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ function-bind: 1.1.1
+ dev: true
+
+ /help-me/4.2.0:
+ resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==}
+ dependencies:
+ glob: 8.1.0
+ readable-stream: 3.6.0
+ dev: true
+
+ /hosted-git-info/2.8.9:
+ resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+ dev: true
+
+ /idb/7.1.1:
+ resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
+ dev: true
+
+ /ieee754/1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ /import-fresh/3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+
+ /inflight/1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+ dev: true
+
+ /inherits/2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ /internal-slot/1.0.4:
+ resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.2.0
+ has: 1.0.3
+ side-channel: 1.0.4
+ dev: true
+
+ /is-array-buffer/3.0.1:
+ resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.0
+ is-typed-array: 1.1.10
+ dev: true
+
+ /is-arrayish/0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+ dev: true
+
+ /is-bigint/1.0.4:
+ resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
+ dependencies:
+ has-bigints: 1.0.2
+ dev: true
+
+ /is-binary-path/2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.2.0
+ dev: true
+
+ /is-boolean-object/1.1.2:
+ resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-builtin-module/3.2.1:
+ resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
+ engines: {node: '>=6'}
+ dependencies:
+ builtin-modules: 3.3.0
+ dev: true
+
+ /is-callable/1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /is-core-module/2.11.0:
+ resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
+ dependencies:
+ has: 1.0.3
+ dev: true
+
+ /is-date-object/1.0.5:
+ resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-extglob/2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-glob/4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-module/1.0.0:
+ resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
+ dev: true
+
+ /is-negative-zero/2.0.2:
+ resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /is-number-object/1.0.7:
+ resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-number/7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-obj/1.0.1:
+ resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-reference/1.2.1:
+ resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
+ dependencies:
+ '@types/estree': 1.0.0
+ dev: true
+
+ /is-regex/1.1.4:
+ resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-regexp/1.0.0:
+ resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-shared-array-buffer/1.0.2:
+ resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
+ dependencies:
+ call-bind: 1.0.2
+ dev: true
+
+ /is-stream/2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-string/1.0.7:
+ resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-symbol/1.0.4:
+ resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-symbols: 1.0.3
+ dev: true
+
+ /is-typed-array/1.1.10:
+ resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.2
+ for-each: 0.3.3
+ gopd: 1.0.1
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-weakref/1.0.2:
+ resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
+ dependencies:
+ call-bind: 1.0.2
+ dev: true
+
+ /isexe/2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
+ /jake/10.8.5:
+ resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ async: 3.2.4
+ chalk: 4.1.2
+ filelist: 1.0.4
+ minimatch: 3.1.2
+ dev: true
+
+ /jest-worker/26.6.2:
+ resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
+ engines: {node: '>= 10.13.0'}
+ dependencies:
+ '@types/node': 18.13.0
+ merge-stream: 2.0.0
+ supports-color: 7.2.0
+ dev: true
+
+ /joycon/3.1.1:
+ resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /js-cookie/3.0.1:
+ resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /js-tokens/4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: true
+
+ /jsesc/0.5.0:
+ resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
+ hasBin: true
+ dev: true
+
+ /jsesc/2.5.2:
+ resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /json-parse-better-errors/1.0.2:
+ resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
+ dev: true
+
+ /json-schema-traverse/1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: true
+
+ /json-schema/0.4.0:
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+ dev: true
+
+ /json5/2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+ dev: true
+
+ /jsonfile/6.1.0:
+ resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+ dependencies:
+ universalify: 2.0.0
+ optionalDependencies:
+ graceful-fs: 4.2.10
+ dev: true
+
+ /jsonpointer/5.0.1:
+ resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /kleur/4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /leven/3.1.0:
+ resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /lilconfig/2.0.6:
+ resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /load-json-file/4.0.0:
+ resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
+ engines: {node: '>=4'}
+ dependencies:
+ graceful-fs: 4.2.10
+ parse-json: 4.0.0
+ pify: 3.0.0
+ strip-bom: 3.0.0
+ dev: true
+
+ /lodash.debounce/4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+ dev: true
+
+ /lodash.sortby/4.7.0:
+ resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
+ dev: true
+
+ /lodash/4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ dev: true
+
+ /lru-cache/5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ dependencies:
+ yallist: 3.1.1
+ dev: true
+
+ /magic-string/0.25.9:
+ resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
+ dependencies:
+ sourcemap-codec: 1.4.8
+ dev: true
+
+ /magic-string/0.27.0:
+ resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
+ engines: {node: '>=12'}
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.14
+ dev: true
+
+ /memorystream/0.3.1:
+ resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
+ engines: {node: '>= 0.10.0'}
+ dev: true
+
+ /merge-stream/2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: true
+
+ /merge2/1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /micromatch/4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /mime/3.0.0:
+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ dev: true
+
+ /min-indent/1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /mini-svg-data-uri/1.4.4:
+ resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+ hasBin: true
+ dev: true
+
+ /minimatch/3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+
+ /minimatch/5.1.6:
+ resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+ engines: {node: '>=10'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: true
+
+ /minimist/1.2.7:
+ resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
+ dev: true
+
+ /mkdirp/0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.7
+ dev: true
+
+ /mri/1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /mrmime/1.0.1:
+ resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /ms/2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ dev: true
+
+ /nanoid/3.3.4:
+ resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /nice-try/1.0.5:
+ resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
+ dev: true
+
+ /node-releases/2.0.10:
+ resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
+ dev: true
+
+ /normalize-package-data/2.5.0:
+ resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
+ dependencies:
+ hosted-git-info: 2.8.9
+ resolve: 1.22.1
+ semver: 5.7.1
+ validate-npm-package-license: 3.0.4
+ dev: true
+
+ /normalize-path/3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /normalize-range/0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /npm-run-all/4.1.5:
+ resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==}
+ engines: {node: '>= 4'}
+ hasBin: true
+ dependencies:
+ ansi-styles: 3.2.1
+ chalk: 2.4.2
+ cross-spawn: 6.0.5
+ memorystream: 0.3.1
+ minimatch: 3.1.2
+ pidtree: 0.3.1
+ read-pkg: 3.0.0
+ shell-quote: 1.8.0
+ string.prototype.padend: 3.1.4
+ dev: true
+
+ /object-hash/3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /object-inspect/1.12.3:
+ resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
+ dev: true
+
+ /object-keys/1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /object.assign/4.1.4:
+ resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ has-symbols: 1.0.3
+ object-keys: 1.1.1
+ dev: true
+
+ /on-exit-leak-free/2.1.0:
+ resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==}
+
+ /once/1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+ dev: true
+
+ /parent-module/1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: true
+
+ /parse-json/4.0.0:
+ resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
+ engines: {node: '>=4'}
+ dependencies:
+ error-ex: 1.3.2
+ json-parse-better-errors: 1.0.2
+ dev: true
+
+ /path-is-absolute/1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /path-key/2.0.1:
+ resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /path-parse/1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ dev: true
+
+ /path-type/3.0.0:
+ resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
+ engines: {node: '>=4'}
+ dependencies:
+ pify: 3.0.0
+ dev: true
+
+ /picocolors/1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+ dev: true
+
+ /picomatch/2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /pidtree/0.3.1:
+ resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+ dev: true
+
+ /pify/2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /pify/3.0.0:
+ resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /pino-abstract-transport/1.0.0:
+ resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==}
+ dependencies:
+ readable-stream: 4.3.0
+ split2: 4.1.0
+
+ /pino-pretty/9.1.1:
+ resolution: {integrity: sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==}
+ hasBin: true
+ dependencies:
+ colorette: 2.0.19
+ dateformat: 4.6.3
+ fast-copy: 3.0.0
+ fast-safe-stringify: 2.1.1
+ help-me: 4.2.0
+ joycon: 3.1.1
+ minimist: 1.2.7
+ on-exit-leak-free: 2.1.0
+ pino-abstract-transport: 1.0.0
+ pump: 3.0.0
+ readable-stream: 4.3.0
+ secure-json-parse: 2.7.0
+ sonic-boom: 3.2.1
+ strip-json-comments: 3.1.1
+ dev: true
+
+ /pino-seq/0.9.0:
+ resolution: {integrity: sha512-XbvixSHuoC6IXQ6rbdD2YFZDaPFTY505uEzWX4rNpiZQwecbwHzSyBxTdvgwLBR4WXOzfKWixKn/vXbhclIijw==}
+ hasBin: true
+ dependencies:
+ '@types/pino': 5.20.0
+ commander: 2.20.3
+ seq-logging: 1.1.2
+ split2: 3.2.2
+ dev: false
+
+ /pino-std-serializers/6.1.0:
+ resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==}
+ dev: false
+
+ /pino/8.9.0:
+ resolution: {integrity: sha512-/x9qSrFW4wh+7OL5bLIbfl06aK/8yCSIldhD3VmVGiVYWSdFFpXvJh/4xWKENs+DhG1VkJnnpWxMF6fZ2zGXeg==}
+ hasBin: true
+ dependencies:
+ atomic-sleep: 1.0.0
+ fast-redact: 3.1.2
+ on-exit-leak-free: 2.1.0
+ pino-abstract-transport: 1.0.0
+ pino-std-serializers: 6.1.0
+ process-warning: 2.1.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.4.2
+ sonic-boom: 3.2.1
+ thread-stream: 2.3.0
+ dev: false
+
+ /playwright-core/1.30.0:
+ resolution: {integrity: sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dev: true
+
+ /postcss-import/14.1.0_postcss@8.4.21:
+ resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ postcss: 8.4.21
+ postcss-value-parser: 4.2.0
+ read-cache: 1.0.0
+ resolve: 1.22.1
+ dev: true
+
+ /postcss-js/4.0.1_postcss@8.4.21:
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
+ engines: {node: ^12 || ^14 || >= 16}
+ peerDependencies:
+ postcss: ^8.4.21
+ dependencies:
+ camelcase-css: 2.0.1
+ postcss: 8.4.21
+ dev: true
+
+ /postcss-load-config/3.1.4_postcss@8.4.21:
+ resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
+ engines: {node: '>= 10'}
+ peerDependencies:
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ lilconfig: 2.0.6
+ postcss: 8.4.21
+ yaml: 1.10.2
+ dev: true
+
+ /postcss-load-config/4.0.1_postcss@8.4.21:
+ resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ lilconfig: 2.0.6
+ postcss: 8.4.21
+ yaml: 2.2.1
+ dev: true
+
+ /postcss-nested/6.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+ dependencies:
+ postcss: 8.4.21
+ postcss-selector-parser: 6.0.11
+ dev: true
+
+ /postcss-selector-parser/6.0.11:
+ resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+ dev: true
+
+ /postcss-value-parser/4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+ dev: true
+
+ /postcss/8.4.21:
+ resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.4
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /pretty-bytes/5.6.0:
+ resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /pretty-bytes/6.1.0:
+ resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==}
+ engines: {node: ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /process-warning/2.1.0:
+ resolution: {integrity: sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==}
+ dev: false
+
+ /process/0.11.10:
+ resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+ engines: {node: '>= 0.6.0'}
+
+ /pump/3.0.0:
+ resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: true
+
+ /punycode/2.3.0:
+ resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /queue-microtask/1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
+ /quick-format-unescaped/4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+ dev: false
+
+ /quick-lru/5.1.1:
+ resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /randombytes/2.1.0:
+ resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: true
+
+ /read-cache/1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+ dependencies:
+ pify: 2.3.0
+ dev: true
+
+ /read-pkg/3.0.0:
+ resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
+ engines: {node: '>=4'}
+ dependencies:
+ load-json-file: 4.0.0
+ normalize-package-data: 2.5.0
+ path-type: 3.0.0
+ dev: true
+
+ /readable-stream/3.6.0:
+ resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
+ engines: {node: '>= 6'}
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ /readable-stream/4.3.0:
+ resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ abort-controller: 3.0.0
+ buffer: 6.0.3
+ events: 3.3.0
+ process: 0.11.10
+
+ /readdirp/3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: true
+
+ /real-require/0.2.0:
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+ engines: {node: '>= 12.13.0'}
+ dev: false
+
+ /regenerate-unicode-properties/10.1.0:
+ resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ regenerate: 1.4.2
+ dev: true
+
+ /regenerate/1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+ dev: true
+
+ /regenerator-runtime/0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+ dev: true
+
+ /regenerator-transform/0.15.1:
+ resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
+ dependencies:
+ '@babel/runtime': 7.20.13
+ dev: true
+
+ /regexp.prototype.flags/1.4.3:
+ resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ functions-have-names: 1.2.3
+ dev: true
+
+ /regexpu-core/5.3.0:
+ resolution: {integrity: sha512-ZdhUQlng0RoscyW7jADnUZ25F5eVtHdMyXSb2PiwafvteRAOJUjFoUPEYZSIfP99fBIs3maLIRfpEddT78wAAQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ '@babel/regjsgen': 0.8.0
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.1.0
+ regjsparser: 0.9.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.1.0
+ dev: true
+
+ /regjsparser/0.9.1:
+ resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
+ hasBin: true
+ dependencies:
+ jsesc: 0.5.0
+ dev: true
+
+ /require-from-string/2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /resolve-from/4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /resolve/1.22.1:
+ resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.11.0
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+ dev: true
+
+ /reusify/1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /rimraf/2.7.1:
+ resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /rollup-plugin-terser/7.0.2_rollup@2.79.1:
+ resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
+ deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
+ peerDependencies:
+ rollup: ^2.0.0
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ jest-worker: 26.6.2
+ rollup: 2.79.1
+ serialize-javascript: 4.0.0
+ terser: 5.16.3
+ dev: true
+
+ /rollup/2.79.1:
+ resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /rollup/3.14.0:
+ resolution: {integrity: sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==}
+ engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /run-parallel/1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
+ /sade/1.8.1:
+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+ engines: {node: '>=6'}
+ dependencies:
+ mri: 1.2.0
+ dev: true
+
+ /safe-buffer/5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ /safe-regex-test/1.0.0:
+ resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.0
+ is-regex: 1.1.4
+ dev: true
+
+ /safe-stable-stringify/2.4.2:
+ resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==}
+ engines: {node: '>=10'}
+ dev: false
+
+ /sander/0.5.1:
+ resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
+ dependencies:
+ es6-promise: 3.3.1
+ graceful-fs: 4.2.10
+ mkdirp: 0.5.6
+ rimraf: 2.7.1
+ dev: true
+
+ /secure-json-parse/2.7.0:
+ resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+ dev: true
+
+ /semver/5.7.1:
+ resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ hasBin: true
+ dev: true
+
+ /semver/6.3.0:
+ resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+ hasBin: true
+ dev: true
+
+ /seq-logging/1.1.2:
+ resolution: {integrity: sha512-9n7bCIHiMdBene104oSEa2917OcNBw+uee2v+we4AQxmjqt/aeQkWy1296IvGsogbj5fK6wuDNhVhm/DYmauVA==}
+ dev: false
+
+ /serialize-javascript/4.0.0:
+ resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
+ dependencies:
+ randombytes: 2.1.0
+ dev: true
+
+ /set-cookie-parser/2.5.1:
+ resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==}
+ dev: true
+
+ /shebang-command/1.2.0:
+ resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ shebang-regex: 1.0.0
+ dev: true
+
+ /shebang-regex/1.0.0:
+ resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /shell-quote/1.8.0:
+ resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==}
+ dev: true
+
+ /side-channel/1.0.4:
+ resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.0
+ object-inspect: 1.12.3
+ dev: true
+
+ /sirv/2.0.2:
+ resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
+ engines: {node: '>= 10'}
+ dependencies:
+ '@polka/url': 1.0.0-next.21
+ mrmime: 1.0.1
+ totalist: 3.0.0
+ dev: true
+
+ /sonic-boom/3.2.1:
+ resolution: {integrity: sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==}
+ dependencies:
+ atomic-sleep: 1.0.0
+
+ /sorcery/0.11.0:
+ resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==}
+ hasBin: true
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.4.14
+ buffer-crc32: 0.2.13
+ minimist: 1.2.7
+ sander: 0.5.1
+ dev: true
+
+ /source-map-js/1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /source-map-support/0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+ dev: true
+
+ /source-map/0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /source-map/0.8.0-beta.0:
+ resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
+ engines: {node: '>= 8'}
+ dependencies:
+ whatwg-url: 7.1.0
+ dev: true
+
+ /sourcemap-codec/1.4.8:
+ resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
+ deprecated: Please use @jridgewell/sourcemap-codec instead
+ dev: true
+
+ /spdx-correct/3.1.1:
+ resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
+ dependencies:
+ spdx-expression-parse: 3.0.1
+ spdx-license-ids: 3.0.12
+ dev: true
+
+ /spdx-exceptions/2.3.0:
+ resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
+ dev: true
+
+ /spdx-expression-parse/3.0.1:
+ resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
+ dependencies:
+ spdx-exceptions: 2.3.0
+ spdx-license-ids: 3.0.12
+ dev: true
+
+ /spdx-license-ids/3.0.12:
+ resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==}
+ dev: true
+
+ /split2/3.2.2:
+ resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
+ dependencies:
+ readable-stream: 3.6.0
+ dev: false
+
+ /split2/4.1.0:
+ resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==}
+ engines: {node: '>= 10.x'}
+
+ /streamsearch/1.1.0:
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
+ engines: {node: '>=10.0.0'}
+ dev: true
+
+ /string.prototype.matchall/4.0.8:
+ resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.21.1
+ get-intrinsic: 1.2.0
+ has-symbols: 1.0.3
+ internal-slot: 1.0.4
+ regexp.prototype.flags: 1.4.3
+ side-channel: 1.0.4
+ dev: true
+
+ /string.prototype.padend/3.1.4:
+ resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.21.1
+ dev: true
+
+ /string.prototype.trimend/1.0.6:
+ resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.21.1
+ dev: true
+
+ /string.prototype.trimstart/1.0.6:
+ resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.21.1
+ dev: true
+
+ /string_decoder/1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ dependencies:
+ safe-buffer: 5.2.1
+
+ /stringify-object/3.3.0:
+ resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
+ engines: {node: '>=4'}
+ dependencies:
+ get-own-enumerable-property-symbols: 3.0.2
+ is-obj: 1.0.1
+ is-regexp: 1.0.0
+ dev: true
+
+ /strip-bom/3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /strip-comments/2.0.1:
+ resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /strip-indent/3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ min-indent: 1.0.1
+ dev: true
+
+ /strip-json-comments/3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /supports-color/5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+ dependencies:
+ has-flag: 3.0.0
+ dev: true
+
+ /supports-color/7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+ dev: true
+
+ /supports-preserve-symlinks-flag/1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /svelte-check/3.0.3_gqx7lw3sljhsd4bstor5m2aa2u:
+ resolution: {integrity: sha512-ByBFXo3bfHRGIsYEasHkdMhLkNleVfszX/Ns1oip58tPJlKdo5Ssr8kgVIuo5oq00hss8AIcdesuy0Xt0BcTvg==}
+ hasBin: true
+ peerDependencies:
+ svelte: ^3.55.0
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.17
+ chokidar: 3.5.3
+ fast-glob: 3.2.12
+ import-fresh: 3.3.0
+ picocolors: 1.0.0
+ sade: 1.8.1
+ svelte: 3.55.1
+ svelte-preprocess: 5.0.1_b25a55hc532q56kmuqlrolam2i
+ typescript: 4.9.5
+ transitivePeerDependencies:
+ - '@babel/core'
+ - coffeescript
+ - less
+ - postcss
+ - postcss-load-config
+ - pug
+ - sass
+ - stylus
+ - sugarss
+ dev: true
+
+ /svelte-headless-table/0.17.2_svelte@3.55.1:
+ resolution: {integrity: sha512-XYG3PC6HzotIhzeqb+M+HGwtkfIhuRJr2+BgRYYD4xTANEUtlWg6M8tIgQp7fqJf7VKOdhmXQbyvjWKE/VGG3w==}
+ dependencies:
+ svelte-keyed: 1.1.6_svelte@3.55.1
+ svelte-render: 1.6.0
+ svelte-subscribe: 1.0.5
+ transitivePeerDependencies:
+ - svelte
+ dev: false
+
+ /svelte-hmr/0.15.1_svelte@3.55.1:
+ resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
+ engines: {node: ^12.20 || ^14.13.1 || >= 16}
+ peerDependencies:
+ svelte: '>=3.19.0'
+ dependencies:
+ svelte: 3.55.1
+ dev: true
+
+ /svelte-keyed/1.1.6_svelte@3.55.1:
+ resolution: {integrity: sha512-sd/7j3waSpOkeiQqATstlvNySbEpSs49aoaZ/nc09x/iEndnXQ/9Zy5PTD06+C7hlZk8KjZ4bhrktAQ791jWMQ==}
+ peerDependencies:
+ svelte: ^3.49.0
+ dependencies:
+ svelte: 3.55.1
+ dev: false
+
+ /svelte-preprocess/5.0.1_b25a55hc532q56kmuqlrolam2i:
+ resolution: {integrity: sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==}
+ engines: {node: '>= 14.10.0'}
+ requiresBuild: true
+ peerDependencies:
+ '@babel/core': ^7.10.2
+ coffeescript: ^2.5.1
+ less: ^3.11.3 || ^4.0.0
+ postcss: ^7 || ^8
+ postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0
+ pug: ^3.0.0
+ sass: ^1.26.8
+ stylus: ^0.55.0
+ sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
+ svelte: ^3.23.0
+ typescript: ^3.9.5 || ^4.0.0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ coffeescript:
+ optional: true
+ less:
+ optional: true
+ postcss:
+ optional: true
+ postcss-load-config:
+ optional: true
+ pug:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ typescript:
+ optional: true
+ dependencies:
+ '@types/pug': 2.0.6
+ '@types/sass': 1.43.1
+ detect-indent: 6.1.0
+ magic-string: 0.27.0
+ postcss: 8.4.21
+ postcss-load-config: 4.0.1_postcss@8.4.21
+ sorcery: 0.11.0
+ strip-indent: 3.0.0
+ svelte: 3.55.1
+ typescript: 4.9.5
+ dev: true
+
+ /svelte-render/1.6.0:
+ resolution: {integrity: sha512-Y6Ea6U768MuhH0xR72pAPnXdSnJ+4yi24YyD/b9Q46yGalQFVxUyan496EELWtQEvgQE9Z2PsFAG8JjvrWcDAg==}
+ dependencies:
+ svelte-subscribe: 1.0.5
+ dev: false
+
+ /svelte-subscribe/1.0.5:
+ resolution: {integrity: sha512-p+vRSBVzR9BQC72mjd2eqCv8zx5euLZQJF7QqAw5d41aKzQVOq90y71/NXch+nDNMjWbRo0CX+brcWYgPryJlw==}
+ dev: false
+
+ /svelte/3.55.1:
+ resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==}
+ engines: {node: '>= 8'}
+
+ /tailwindcss/3.2.6_postcss@8.4.21:
+ resolution: {integrity: sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==}
+ engines: {node: '>=12.13.0'}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.0.9
+ dependencies:
+ arg: 5.0.2
+ chokidar: 3.5.3
+ color-name: 1.1.4
+ detective: 5.2.1
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.2.12
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ lilconfig: 2.0.6
+ micromatch: 4.0.5
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.0.0
+ postcss: 8.4.21
+ postcss-import: 14.1.0_postcss@8.4.21
+ postcss-js: 4.0.1_postcss@8.4.21
+ postcss-load-config: 3.1.4_postcss@8.4.21
+ postcss-nested: 6.0.0_postcss@8.4.21
+ postcss-selector-parser: 6.0.11
+ postcss-value-parser: 4.2.0
+ quick-lru: 5.1.1
+ resolve: 1.22.1
+ transitivePeerDependencies:
+ - ts-node
+ dev: true
+
+ /temp-dir/2.0.0:
+ resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /temporal-polyfill/0.0.8:
+ resolution: {integrity: sha512-IuA8GhS1PRC04H/zVNAIxJvCZQum6V5HjqFj7gz1a3SMUf/Kf1xIXILNYtxrWYnGqIU/RrDRxlCKCm/vmqnBvw==}
+ dependencies:
+ temporal-spec: 0.0.3
+ dev: false
+
+ /temporal-spec/0.0.3:
+ resolution: {integrity: sha512-gJu7QRqn5c2vTSkYWGC4qz1i+FZ9C+Cz16UIBMRcjgXOsHfXeSIgaWUKeq/2rz1iNfFxvmF/ywqbfC6ggTpjkA==}
+ dev: false
+
+ /tempy/0.6.0:
+ resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==}
+ engines: {node: '>=10'}
+ dependencies:
+ is-stream: 2.0.1
+ temp-dir: 2.0.0
+ type-fest: 0.16.0
+ unique-string: 2.0.0
+ dev: true
+
+ /terser/5.16.3:
+ resolution: {integrity: sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ '@jridgewell/source-map': 0.3.2
+ acorn: 8.8.2
+ commander: 2.20.3
+ source-map-support: 0.5.21
+ dev: true
+
+ /thread-stream/2.3.0:
+ resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==}
+ dependencies:
+ real-require: 0.2.0
+ dev: false
+
+ /tiny-glob/0.2.9:
+ resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
+ dependencies:
+ globalyzer: 0.1.0
+ globrex: 0.1.2
+ dev: true
+
+ /to-fast-properties/2.0.0:
+ resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /to-regex-range/5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /totalist/3.0.0:
+ resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /tr46/1.0.1:
+ resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
+ dependencies:
+ punycode: 2.3.0
+ dev: true
+
+ /tslib/2.5.0:
+ resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
+ dev: true
+
+ /turbo-query/1.9.0:
+ resolution: {integrity: sha512-14E/AJkYEexNBP6PiT4lJCPZloCLVg5HIPE1OrmcIAQmrwgq0QdJL3eIjYebCQ5uvTgIyg+jp/J7/o6l+3GTrQ==}
+ dev: false
+
+ /type-fest/0.16.0:
+ resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /typed-array-length/1.0.4:
+ resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
+ dependencies:
+ call-bind: 1.0.2
+ for-each: 0.3.3
+ is-typed-array: 1.1.10
+ dev: true
+
+ /typesafe-i18n/5.24.0_typescript@4.9.5:
+ resolution: {integrity: sha512-GGIV+x+Azs+uVe1940ZX3MtIKSN0eXrO/x1Z7d0B/FO610evlmTEBIIYIHFWvjhZJslty11INedwRkZKSDVwTQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=3.5.1'
+ dependencies:
+ typescript: 4.9.5
+ dev: true
+
+ /typescript/4.9.5:
+ resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
+ engines: {node: '>=4.2.0'}
+ hasBin: true
+ dev: true
+
+ /unbox-primitive/1.0.2:
+ resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
+ dependencies:
+ call-bind: 1.0.2
+ has-bigints: 1.0.2
+ has-symbols: 1.0.3
+ which-boxed-primitive: 1.0.2
+ dev: true
+
+ /undici/5.18.0:
+ resolution: {integrity: sha512-1iVwbhonhFytNdg0P4PqyIAXbdlVZVebtPDvuM36m66mRw4OGrCm2MYynJv/UENFLdP13J1nPVQzVE2zTs1OeA==}
+ engines: {node: '>=12.18'}
+ dependencies:
+ busboy: 1.6.0
+ dev: true
+
+ /unicode-canonical-property-names-ecmascript/2.0.0:
+ resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /unicode-match-property-ecmascript/2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.0
+ unicode-property-aliases-ecmascript: 2.1.0
+ dev: true
+
+ /unicode-match-property-value-ecmascript/2.1.0:
+ resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /unicode-property-aliases-ecmascript/2.1.0:
+ resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /unique-string/2.0.0:
+ resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
+ engines: {node: '>=8'}
+ dependencies:
+ crypto-random-string: 2.0.0
+ dev: true
+
+ /universalify/2.0.0:
+ resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
+ engines: {node: '>= 10.0.0'}
+ dev: true
+
+ /upath/1.2.0:
+ resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /update-browserslist-db/1.0.10_browserslist@4.21.5:
+ resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
+ browserslist: 4.21.5
+ escalade: 3.1.1
+ picocolors: 1.0.0
+ dev: true
+
+ /uri-js/4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.3.0
+ dev: true
+
+ /util-deprecate/1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ /validate-npm-package-license/3.0.4:
+ resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+ dependencies:
+ spdx-correct: 3.1.1
+ spdx-expression-parse: 3.0.1
+ dev: true
+
+ /vite-plugin-pwa/0.14.3_vite@4.1.1:
+ resolution: {integrity: sha512-o/CEzdHXamdSV4WJ6hp1EQNe+yVvoFf9b5q1nMhOSqKxaW7BaDqAHAwnq8jt21wakDmcaipnuF3/j78AzZJ6wg==}
+ peerDependencies:
+ vite: ^3.1.0 || ^4.0.0
+ dependencies:
+ '@rollup/plugin-replace': 5.0.2_rollup@3.14.0
+ debug: 4.3.4
+ fast-glob: 3.2.12
+ pretty-bytes: 6.1.0
+ rollup: 3.14.0
+ vite: 4.1.1
+ workbox-build: 6.5.4
+ workbox-window: 6.5.4
+ transitivePeerDependencies:
+ - '@types/babel__core'
+ - supports-color
+ dev: true
+
+ /vite/4.1.1:
+ resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': '>= 14'
+ less: '*'
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.16.17
+ postcss: 8.4.21
+ resolve: 1.22.1
+ rollup: 3.14.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /vitefu/0.2.4_vite@4.1.1:
+ resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+ dependencies:
+ vite: 4.1.1
+ dev: true
+
+ /webidl-conversions/4.0.2:
+ resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
+ dev: true
+
+ /whatwg-url/7.1.0:
+ resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
+ dependencies:
+ lodash.sortby: 4.7.0
+ tr46: 1.0.1
+ webidl-conversions: 4.0.2
+ dev: true
+
+ /which-boxed-primitive/1.0.2:
+ resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
+ dependencies:
+ is-bigint: 1.0.4
+ is-boolean-object: 1.1.2
+ is-number-object: 1.0.7
+ is-string: 1.0.7
+ is-symbol: 1.0.4
+ dev: true
+
+ /which-typed-array/1.1.9:
+ resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.2
+ for-each: 0.3.3
+ gopd: 1.0.1
+ has-tostringtag: 1.0.0
+ is-typed-array: 1.1.10
+ dev: true
+
+ /which/1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /workbox-background-sync/6.5.4:
+ resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==}
+ dependencies:
+ idb: 7.1.1
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-broadcast-update/6.5.4:
+ resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-build/6.5.4:
+ resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ '@apideck/better-ajv-errors': 0.3.6_ajv@8.12.0
+ '@babel/core': 7.20.12
+ '@babel/preset-env': 7.20.2_@babel+core@7.20.12
+ '@babel/runtime': 7.20.13
+ '@rollup/plugin-babel': 5.3.1_3dsfpkpoyvuuxyfgdbpn4j4uzm
+ '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.1
+ '@rollup/plugin-replace': 2.4.2_rollup@2.79.1
+ '@surma/rollup-plugin-off-main-thread': 2.2.3
+ ajv: 8.12.0
+ common-tags: 1.8.2
+ fast-json-stable-stringify: 2.1.0
+ fs-extra: 9.1.0
+ glob: 7.2.3
+ lodash: 4.17.21
+ pretty-bytes: 5.6.0
+ rollup: 2.79.1
+ rollup-plugin-terser: 7.0.2_rollup@2.79.1
+ source-map: 0.8.0-beta.0
+ stringify-object: 3.3.0
+ strip-comments: 2.0.1
+ tempy: 0.6.0
+ upath: 1.2.0
+ workbox-background-sync: 6.5.4
+ workbox-broadcast-update: 6.5.4
+ workbox-cacheable-response: 6.5.4
+ workbox-core: 6.5.4
+ workbox-expiration: 6.5.4
+ workbox-google-analytics: 6.5.4
+ workbox-navigation-preload: 6.5.4
+ workbox-precaching: 6.5.4
+ workbox-range-requests: 6.5.4
+ workbox-recipes: 6.5.4
+ workbox-routing: 6.5.4
+ workbox-strategies: 6.5.4
+ workbox-streams: 6.5.4
+ workbox-sw: 6.5.4
+ workbox-window: 6.5.4
+ transitivePeerDependencies:
+ - '@types/babel__core'
+ - supports-color
+ dev: true
+
+ /workbox-cacheable-response/6.5.4:
+ resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-core/6.5.4:
+ resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==}
+ dev: true
+
+ /workbox-expiration/6.5.4:
+ resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==}
+ dependencies:
+ idb: 7.1.1
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-google-analytics/6.5.4:
+ resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==}
+ dependencies:
+ workbox-background-sync: 6.5.4
+ workbox-core: 6.5.4
+ workbox-routing: 6.5.4
+ workbox-strategies: 6.5.4
+ dev: true
+
+ /workbox-navigation-preload/6.5.4:
+ resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-precaching/6.5.4:
+ resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==}
+ dependencies:
+ workbox-core: 6.5.4
+ workbox-routing: 6.5.4
+ workbox-strategies: 6.5.4
+ dev: true
+
+ /workbox-range-requests/6.5.4:
+ resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-recipes/6.5.4:
+ resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==}
+ dependencies:
+ workbox-cacheable-response: 6.5.4
+ workbox-core: 6.5.4
+ workbox-expiration: 6.5.4
+ workbox-precaching: 6.5.4
+ workbox-routing: 6.5.4
+ workbox-strategies: 6.5.4
+ dev: true
+
+ /workbox-routing/6.5.4:
+ resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-strategies/6.5.4:
+ resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==}
+ dependencies:
+ workbox-core: 6.5.4
+ dev: true
+
+ /workbox-streams/6.5.4:
+ resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==}
+ dependencies:
+ workbox-core: 6.5.4
+ workbox-routing: 6.5.4
+ dev: true
+
+ /workbox-sw/6.5.4:
+ resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==}
+ dev: true
+
+ /workbox-window/6.5.4:
+ resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==}
+ dependencies:
+ '@types/trusted-types': 2.0.2
+ workbox-core: 6.5.4
+ dev: true
+
+ /wrappy/1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ dev: true
+
+ /xtend/4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+ dev: true
+
+ /yallist/3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ dev: true
+
+ /yaml/1.10.2:
+ resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /yaml/2.2.1:
+ resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==}
+ engines: {node: '>= 14'}
+ dev: true
diff --git a/code/app/postcss.config.cjs b/code/app/postcss.config.cjs
new file mode 100644
index 0000000..a53e3b3
--- /dev/null
+++ b/code/app/postcss.config.cjs
@@ -0,0 +1,13 @@
+const tailwindcss = require("tailwindcss");
+const autoprefixer = require("autoprefixer");
+const nesting = require("tailwindcss/nesting");
+
+const config = {
+ plugins: [
+ nesting,
+ tailwindcss,
+ autoprefixer
+ ],
+};
+
+module.exports = config;
diff --git a/code/app/src/actions/pwKey.ts b/code/app/src/actions/pwKey.ts
new file mode 100644
index 0000000..cf85685
--- /dev/null
+++ b/code/app/src/actions/pwKey.ts
@@ -0,0 +1,7 @@
+import { is_testing } from "$configuration";
+
+export default function pwKey(node: HTMLElement, value: string | undefined) {
+ if (!value) return;
+ if (!is_testing()) return;
+ node.setAttribute("pw-key", value);
+} \ No newline at end of file
diff --git a/code/app/src/app.d.ts b/code/app/src/app.d.ts
new file mode 100644
index 0000000..31b276e
--- /dev/null
+++ b/code/app/src/app.d.ts
@@ -0,0 +1,9 @@
+// See https://kit.svelte.dev/docs/types#app
+// for information about these interfaces
+// and what to do when importing types
+declare namespace App {
+ interface Locals { }
+ interface Platform { }
+ interface PrivateEnv { }
+ interface PublicEnv { }
+} \ No newline at end of file
diff --git a/code/app/src/app.html b/code/app/src/app.html
new file mode 100644
index 0000000..308b223
--- /dev/null
+++ b/code/app/src/app.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="h-full bg-white" lang="en">
+
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ %sveltekit.head%
+</head>
+
+<body class="h-full">
+ <div>%sveltekit.body%</div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/code/app/src/app.pcss b/code/app/src/app.pcss
new file mode 100644
index 0000000..5450db4
--- /dev/null
+++ b/code/app/src/app.pcss
@@ -0,0 +1,34 @@
+/* Write your global styles here, in PostCSS syntax */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+pre {
+ font-family: monospace !important;
+}
+
+*:focus-visible {
+ outline: 1px auto;
+}
+
+.c-disabled {
+ cursor: not-allowed !important;
+ filter: opacity(.45);
+ pointer-events: none !important;
+}
+
+.c-disabled.loading {
+ cursor: wait !important;
+}
+
+.link {
+ @apply text-blue-600 hover:text-blue-700 transition duration-300 ease-in-out mb-4 cursor-pointer;
+
+ &.danger {
+ @apply text-red-600 hover:text-red-700;
+ }
+
+ &.active {
+ @apply underline
+ }
+}
diff --git a/code/app/src/components/alert.svelte b/code/app/src/components/alert.svelte
new file mode 100644
index 0000000..16d8340
--- /dev/null
+++ b/code/app/src/components/alert.svelte
@@ -0,0 +1,268 @@
+<script lang="ts">
+ import {random_string} from "$utilities/misc-helpers";
+ import {createEventDispatcher} from "svelte";
+ import {onMount} from "svelte";
+ import pwKey from "$actions/pwKey";
+ import {Temporal} from "temporal-polyfill";
+ import {ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon, XCircleIcon, XMarkIcon} from "./icons";
+
+ const dispatch = createEventDispatcher();
+ const noCooldownSetting = "no-cooldown";
+
+ let iconComponent: any;
+ let colorClassPart = "";
+
+ /**
+ * An optional id for this alert, a default is set if not specified.
+ * This value is necessary for closeable cooldown to work.
+ */
+ // if no unique id is supplied, cooldown will not work between page loads.
+ // Therefore we are disabling it with noCooldownSetting in the fallback id.
+ export let id = "alert--" + noCooldownSetting + "--" + random_string(4);
+ /**
+ * The title to communicate, value is optional
+ */
+ export let title = "";
+ /**
+ * The message to communicate, value is optional
+ */
+ export let message = "";
+ /**
+ * Changes the alerts color and icon.
+ */
+ export let type: "info" | "success" | "warning" | "error" = "info";
+ /**
+ * If true the alert can be removed from the DOM by clicking on a X icon on the upper right hand courner
+ */
+ export let closeable = false;
+ /**
+ * The amount of seconds that should go by before this alert is shown again, only works when a unique id is set.
+ * Set to ~ if it should only be shown once per client (State stored in localestorage).
+ **/
+ export let closeableCooldown = "-1";
+ /**
+ * The text that is displayed on the right link
+ */
+ export let rightLinkText = "";
+ /**
+ * An array of list items displayed under the message or title
+ */
+ export let listItems: Array<string> = [];
+ /**
+ * An array of {id:string;text:string;color?:string}, where id is dispatched back as an svelte event with this syntax act$id (ex: on:actcancel).
+ * Text is the button text
+ * Color is the optional tailwind color to used, the value is used in classes like bg-$color-50.
+ */
+ export let actions: Array<{ id: string; text: string; color?: string }> = [];
+ /**
+ * This value is set on a plain anchor tag without any svelte routing,
+ * listen to the on:rightLinkClick if you want to intercept the click without navigating
+ */
+ export let rightLinkHref = "javascript:void(0)";
+ $: cooldownEnabled =
+ id.indexOf(noCooldownSetting) === -1 && closeable && (closeableCooldown === "~" || parseInt(closeableCooldown) > 0);
+ /**
+ * Sets this alerts visibility state, when this is false it is removed from the dom using an {#if} block.
+ */
+ export let visible = closeableCooldown === "~" || parseInt(closeableCooldown) > 0 ? false : true;
+
+ export let _pwKey: string | undefined = undefined;
+
+ const cooldownStorageKey = "lastseen--" + id;
+
+ $: switch (type) {
+ case "info": {
+ colorClassPart = "blue";
+ iconComponent = InformationCircleIcon;
+ break;
+ }
+ case "warning": {
+ colorClassPart = "yellow";
+ iconComponent = ExclamationTriangleIcon;
+ break;
+ }
+ case "error": {
+ colorClassPart = "red";
+ iconComponent = XCircleIcon;
+ break;
+ }
+ case "success": {
+ colorClassPart = "green";
+ iconComponent = CheckCircleIcon;
+ break;
+ }
+ }
+
+ function close() {
+ visible = false;
+ if (cooldownEnabled) {
+ console.log("Cooldown enabled for " + id + ", " + closeableCooldown === "~" ? "with an endless cooldown" : "");
+ localStorage.setItem(cooldownStorageKey, String(Temporal.Now.instant().epochSeconds));
+ }
+ }
+
+ function rightLinkClicked() {
+ dispatch("rightLinkCliked");
+ }
+
+ function actionClicked(name: string) {
+ dispatch("act" + name);
+ }
+
+ // Manages the state of the alert if cooldown is enabled
+ function run_cooldown() {
+ if (!cooldownEnabled) {
+ console.log("Alert cooldown is not enabled for " + id);
+ return;
+ }
+ if (!localStorage.getItem(cooldownStorageKey)) {
+ console.log("Alert " + id + " has not been seen yet, displaying");
+ visible = true;
+ return;
+ }
+ // if (!visible) {
+ // console.log(
+ // "Alert " + id + " is not visible, stopping cooldown change"
+ // );
+ // return;
+ // }
+ if (closeableCooldown === "~") {
+ console.log("Alert " + id + " has an infinite cooldown, hiding");
+ visible = false;
+ return;
+ }
+
+ const lastSeen = Temporal.Instant.fromEpochSeconds(parseInt(localStorage.getItem(cooldownStorageKey) ?? "-1"));
+ if (Temporal.Instant.compare(Temporal.Now.instant(), lastSeen.add({seconds: parseInt(closeableCooldown)})) === 1) {
+ console.log(
+ "Alert " +
+ id +
+ " has a cooldown of " +
+ closeableCooldown +
+ " and was last seen " +
+ lastSeen.toLocaleString() +
+ " making it due for a showing",
+ );
+ visible = true;
+ } else {
+ visible = false;
+ }
+ }
+
+ onMount(() => {
+ if (cooldownEnabled) {
+ run_cooldown();
+ }
+
+ if (closeable && closeableCooldown && id.indexOf(noCooldownSetting) !== -1) {
+ // TODO: This prints twice before shutting up as it should, in this example look at the only alert with closeableCooldown in alertsbook.
+ // Looks like svelte mounts three times and that my id is only set on the third. Not sure it does at all after logging the id onMount.
+ console.error("Alert cooldown does not work without specifying a unique id, related id: " + id);
+ }
+ });
+</script>
+
+{#if visible}
+ <div class="rounded-md bg-{colorClassPart}-50 p-4 {$$restProps.class ?? ''}" use:pwKey={_pwKey}>
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <svelte:component this={iconComponent} class="text-{colorClassPart}-400"/>
+ </div>
+ <div class="ml-3 text-sm w-full">
+ {#if !rightLinkText}
+ {#if title}
+ <h3 class="font-bold text-{colorClassPart}-800">
+ {title}
+ </h3>
+ {/if}
+ {#if message}
+ <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start">
+ <p>
+ {@html message}
+ </p>
+ </div>
+ {/if}
+ {#if listItems?.length ?? 0}
+ <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700">
+ {#each listItems as listItem}
+ <li>{listItem}</li>
+ {/each}
+ </ul>
+ {/if}
+ {:else}
+ <div class="flex-1 md:flex md:justify-between">
+ <div>
+ {#if title}
+ <h3 class="font-medium text-{colorClassPart}-800">
+ {title}
+ </h3>
+ {/if}
+ {#if message}
+ <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start">
+ <p>
+ {@html message}
+ </p>
+ </div>
+ {/if}
+ {#if listItems?.length ?? 0}
+ <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700">
+ {#each listItems as listItem}
+ <li>{listItem}</li>
+ {/each}
+ </ul>
+ {/if}
+ </div>
+ <p class="mt-3 text-sm md:mt-0 md:ml-6 flex items-end">
+ <a
+ href={rightLinkHref}
+ on:click={() => rightLinkClicked()}
+ class="whitespace-nowrap font-medium text-{colorClassPart}-700 hover:text-{colorClassPart}-600"
+ >
+ {rightLinkText}
+ <span aria-hidden="true"> &rarr;</span>
+ </a>
+ </p>
+ </div>
+ {/if}
+ {#if actions?.length ?? 0}
+ <div class="ml-2 mt-4">
+ <div class="-mx-2 -my-1.5 flex gap-1">
+ {#each actions as action}
+ {@const color = action?.color ?? colorClassPart}
+ <button
+ type="button"
+ on:click={() => actionClicked(action.id)}
+ class="rounded-md
+ bg-{color}-50
+ px-2 py-1.5 text-sm font-medium
+ text-{color}-800
+ hover:bg-{color}-100
+ focus:outline-none focus:ring-2
+ focus:ring-{color}-600
+ focus:ring-offset-2
+ focus:ring-offset-{color}-50"
+ >
+ {action.text}
+ </button>
+ {/each}
+ </div>
+ </div>
+ {/if}
+ </div>
+ {#if closeable}
+ <div class="ml-auto pl-3">
+ <div class="-mx-1.5 -my-1.5">
+ <button
+ type="button"
+ on:click={() => close()}
+ class="inline-flex rounded-md bg-{colorClassPart}-50 p-1.5 text-{colorClassPart}-500 hover:bg-{colorClassPart}-100 focus:outline-none focus:ring-2 focus:ring-{colorClassPart}-600 focus:ring-offset-2 focus:ring-offset-{colorClassPart}-50"
+ >
+ <span class="sr-only">Dismiss</span>
+ <XMarkIcon/>
+ </button>
+ </div>
+ </div>
+ {/if}
+ </div>
+ </div>
+{/if}
diff --git a/code/app/src/components/badge.svelte b/code/app/src/components/badge.svelte
new file mode 100644
index 0000000..f967c2d
--- /dev/null
+++ b/code/app/src/components/badge.svelte
@@ -0,0 +1,76 @@
+<script lang="ts">
+ import { createEventDispatcher } from "svelte";
+
+ export let id: string | undefined = undefined;
+ export let type: "default" | "red" | "yellow" | "green" | "blue" | "tame" = "default";
+ export let text: string;
+ export let size: "large" | "default" = "default";
+ export let withDot: boolean = false;
+ export let removable: boolean = false;
+ export let uppercase: boolean = false;
+ export let tabindex: string | undefined = undefined;
+
+ let colorName = "gray";
+ let sizeClass = "rounded px-2 py-0.5 text-xs";
+ let dotSizeClass = "mr-1.5 h-2 w-2";
+
+ const dispatch = createEventDispatcher();
+
+ function handle_remove(event) {
+ dispatch("remove", { event, id, text });
+ }
+
+ $: switch (type) {
+ case "red":
+ colorName = "red";
+ break;
+ case "yellow":
+ colorName = "yellow";
+ break;
+ case "blue":
+ colorName = "blue";
+ break;
+ case "green":
+ colorName = "teal";
+ break;
+ case "default":
+ case "tame":
+ default:
+ colorName = "gray";
+ break;
+ }
+
+ $: switch (size) {
+ case "large":
+ sizeClass = "rounded-md px-2.5 py-0.5 text-sm";
+ dotSizeClass = "-ml-0.5 mr-1.5 h-2 w-2";
+ break;
+ case "default":
+ default:
+ sizeClass = "rounded px-2 py-0.5 text-xs";
+ dotSizeClass = "mr-1.5 h-2 w-2";
+ break;
+ }
+</script>
+
+<span class="inline-flex items-center font-medium {uppercase ? 'uppercase' : ''} bg-{colorName}-100 text-{colorName}-800 {sizeClass}" {id}>
+ {#if withDot}
+ <svg class="{dotSizeClass} text-{colorName}-400" fill="currentColor" viewBox="0 0 8 8">
+ <circle cx="4" cy="4" r="3" />
+ </svg>
+ {/if}
+ {text}
+ {#if removable}
+ <button
+ on:click={handle_remove}
+ tabindex={parseInt(tabindex)}
+ type="button"
+ class="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full text-{colorName}-400 hover:bg-{colorName}-200 hover:text-{colorName}-500 focus:bg-{colorName}-500 focus:outline-none"
+ >
+ <span class="sr-only">Remove badge</span>
+ <svg class="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
+ <path stroke-linecap="round" stroke-width="1.5" d="M1 1l6 6m0-6L1 7" />
+ </svg>
+ </button>
+ {/if}
+</span>
diff --git a/code/app/src/components/button.svelte b/code/app/src/components/button.svelte
new file mode 100644
index 0000000..1d6ac4b
--- /dev/null
+++ b/code/app/src/components/button.svelte
@@ -0,0 +1,116 @@
+<script context="module" lang="ts">
+ export type ButtonKind = "primary" | "secondary" | "white" | "reset";
+ export type ButtonSize = "sm" | "lg" | "md" | "xl";
+</script>
+
+<script lang="ts">
+ import pwKey from "$actions/pwKey";
+ import { SpinnerIcon } from "./icons";
+
+ export let kind = "primary" as ButtonKind;
+ export let size = "md" as ButtonSize;
+ export let type: "button" | "submit" | "reset" = "button";
+ export let id: string | undefined = undefined;
+ export let tabindex: string | undefined = undefined;
+ export let style: string | undefined = undefined;
+ export let title: string | undefined = undefined;
+ export let disabled: boolean | null = false;
+ export let href: string | undefined = undefined;
+ export let text: string;
+ export let loading = false;
+ export let fullWidth = false;
+ export let _pwKey: string | undefined = undefined;
+
+ let sizeClasses = "";
+ let kindClasses = "";
+ let spinnerTextClasses = "";
+ let spinnerMarginClasses = "";
+
+ $: shared_props = {
+ type: type,
+ id: id || null,
+ title: title || null,
+ disabled: disabled || loading || null,
+ tabindex: tabindex || null,
+ style: style || null,
+ } as any;
+
+ $: switch (size) {
+ case "sm":
+ sizeClasses = "px-2.5 py-1.5 text-xs";
+ spinnerMarginClasses = "mr-2";
+ break;
+ case "md":
+ sizeClasses = "px-3 py-2 text-sm";
+ spinnerMarginClasses = "mr-2";
+ break;
+ case "lg":
+ sizeClasses = "px-3 py-2 text-lg";
+ spinnerMarginClasses = "mr-2";
+ break;
+ case "xl":
+ sizeClasses = "px-6 py-3 text-xl";
+ spinnerMarginClasses = "mr-2";
+ break;
+ }
+
+ $: switch (kind) {
+ case "secondary":
+ kindClasses = "border-transparent text-teal-800 bg-teal-100 hover:bg-teal-200 focus:ring-teal-500";
+ spinnerTextClasses = "teal-800";
+ break;
+ case "primary":
+ kindClasses = "border-transparent text-teal-900 bg-teal-300 hover:bg-teal-400 focus:ring-teal-500";
+ spinnerTextClasses = "text-teal-900";
+ break;
+ case "white":
+ kindClasses = "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-gray-400";
+ spinnerTextClasses = "text-gray-700";
+ break;
+ case "reset":
+ kindClasses = "reset outline-none ring-0 focus:ring-0 focus-visible:ring-0";
+ break;
+ }
+</script>
+
+{#if href}
+ <a
+ use:pwKey={_pwKey}
+ {...shared_props}
+ {href}
+ class="{sizeClasses} {kindClasses} {loading ? 'disabled:' : ''} {$$restProps.class ?? ''} {fullWidth
+ ? 'w-full justify-center'
+ : ''} disabled:cursor-not-allowed inline-flex items-center border font-bold rounded shadow-sm focus:outline-none focus:ring-2"
+ >
+ {#if loading}
+ <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} />
+ {/if}
+ {text}
+ </a>
+{:else}
+ <button
+ use:pwKey={_pwKey}
+ {...shared_props}
+ on:click
+ class="btn {sizeClasses} {kindClasses} {$$restProps.class ?? ''}
+ {fullWidth
+ ? 'w-full justify-center'
+ : ''} inline-flex items-center border font-bold rounded shadow-sm focus:outline-none focus:ring-2"
+ >
+ {#if loading}
+ <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} />
+ {/if}
+ {text}
+ </button>
+{/if}
+
+<style>
+ .reset {
+ border: 0px;
+ outline: none;
+ }
+
+ .reset:focus {
+ outline: none;
+ }
+</style>
diff --git a/code/app/src/components/checkbox.svelte b/code/app/src/components/checkbox.svelte
new file mode 100644
index 0000000..db72bee
--- /dev/null
+++ b/code/app/src/components/checkbox.svelte
@@ -0,0 +1,29 @@
+<script lang="ts">
+ import pwKey from "$actions/pwKey";
+ import {random_string} from "$utilities/misc-helpers";
+
+ export let label: string;
+ export let id: string | undefined = "input__" + random_string(4);
+ export let name: string | undefined = undefined;
+ export let disabled: boolean | null = null;
+ export let checked: boolean;
+ export let required: boolean | null = null;
+ export let _pwKey: string | undefined = undefined;
+</script>
+
+<div class="flex items-center">
+ <input
+ {name}
+ use:pwKey={_pwKey}
+ {disabled}
+ {id}
+ {required}
+ type="checkbox"
+ bind:checked
+ class="h-4 w-4 text-teal-600 focus:ring-teal-500 border-gray-300 rounded"
+ />
+ <label for={id} class="ml-2 block text-sm text-gray-900">
+ {@html required ? "<span class='text-red-500'>*</span>" : ""}
+ {label}
+ </label>
+</div>
diff --git a/code/app/src/components/combobox.svelte b/code/app/src/components/combobox.svelte
new file mode 100644
index 0000000..396c18a
--- /dev/null
+++ b/code/app/src/components/combobox.svelte
@@ -0,0 +1,451 @@
+<script lang="ts" context="module">
+ export type ComboboxOption = {
+ id: string;
+ name: string;
+ selected?: boolean;
+ };
+</script>
+
+<script lang="ts">
+ import { CheckCircleIcon, ChevronUpDownIcon, XIcon } from "./icons";
+ import { random_string } from "$utilities/misc-helpers";
+ import { go, highlight } from "fuzzysort";
+ import Badge from "./badge.svelte";
+ import Button from "./button.svelte";
+ import LL from "$i18n/i18n-svelte";
+ import { element_has_focus } from "$utilities/dom-helpers";
+
+ export let id = "combobox-" + random_string(3);
+ export let label: string | undefined = undefined;
+ export let errorText: string | undefined = undefined;
+ export let disabled: boolean | undefined = undefined;
+ export let required: boolean | undefined = undefined;
+ export let maxlength: number | undefined = undefined;
+ export let placeholder: string = $LL.combobox.search();
+ export let options: Array<ComboboxOption> | undefined = [];
+ export let createable = false;
+ export let loading = false;
+ export let multiple = false;
+ export let noResultsText: string = $LL.combobox.noRecordsFound();
+ export let on_create_async = async ({ name: string }) => {};
+
+ export const reset = () => methods.reset();
+ export const select = (id: string) => methods.select_entry(id);
+ export const deselect = (id: string) => methods.deselect_entry(id);
+
+ const INTERNAL_ID = "INTERNAL__" + id;
+
+ let optionsListId = id + "--options";
+ let searchInputNode;
+ let searchResults: Array<any> = [];
+ let searchValue = "";
+ let showCreationHint = false;
+ let showDropdown = false;
+ let inputHasFocus = false;
+ let lastKeydownCode = "";
+ let mouseIsOverDropdown = false;
+ let mouseIsOverComponent = false;
+
+ $: ariaErrorDescribedBy = id + "__" + "error";
+ $: colorName = errorText ? "red" : "teal";
+ $: attributes = {
+ "aria-describedby": errorText ? ariaErrorDescribedBy : null,
+ "aria-invalid": errorText ? "true" : null,
+ disabled: disabled || null,
+ required: required || null,
+ maxlength: maxlength || null,
+ id: id || null,
+ placeholder: placeholder || null,
+ } as any;
+ $: hasSelection = options.some((c) => c.selected === true);
+ $: if (searchValue.trim()) {
+ showCreationHint = createable && options.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue));
+ } else {
+ showCreationHint = false;
+ options = methods.get_sorted_array(options);
+ }
+
+ function on_select(event) {
+ const node = event.target.closest("[data-id]");
+ if (!node) return;
+ methods.select_entry(node.dataset.id);
+ }
+
+ const search = {
+ normalise_value(value: string): string {
+ if (!value) {
+ return "";
+ }
+ return value.trim().toLowerCase();
+ },
+ do() {
+ const query = search.normalise_value(searchValue);
+
+ if (!query.trim()) {
+ searchResults = [];
+ return;
+ }
+
+ // @ts-ignore
+ searchResults = go(query, options, {
+ limit: 15,
+ allowTypo: true,
+ threshold: -10000,
+ key: "name",
+ });
+ showDropdown = true;
+ },
+ on_input_focus() {
+ showDropdown = true;
+ inputHasFocus = true;
+ },
+ on_input_click() {
+ showDropdown = true;
+ inputHasFocus = true;
+ },
+ on_input_focusout() {
+ inputHasFocus = false;
+ if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) {
+ return;
+ }
+ const selected = options.find((c) => c.selected === true);
+ if (selected && !multiple) {
+ searchValue = selected.name;
+ }
+ document.querySelector("#" + INTERNAL_ID + " ul li.focus")?.classList.remove("focus");
+ showDropdown = false;
+ },
+ on_input_wrapper_focus(event) {
+ if (event.code && event.code !== "Space" && event.code !== "Enter") return;
+ if (!element_has_focus(searchInputNode)) searchInputNode.focus();
+ showDropdown = true;
+ },
+ };
+
+ const methods = {
+ reset(focus_input = false) {
+ searchValue = "";
+ const copy = options;
+ for (const entry of copy) {
+ entry.selected = false;
+ }
+ options = methods.get_sorted_array(copy);
+ if (focus_input) {
+ searchInputNode?.focus();
+ showDropdown = true;
+ } else {
+ showDropdown = false;
+ }
+ },
+ async create_entry(name: string) {
+ if (!name || !createable || loading) {
+ console.log("Not sending creation event due to failed preconditions", { name, createable, loading });
+ return;
+ }
+ try {
+ await on_create_async({ name });
+ searchValue = "";
+ loading = false;
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ select_entry(entryId: string) {
+ if (!entryId || loading) {
+ console.log("Not selecting entry due to failed preconditions", {
+ entryId,
+ loading,
+ });
+ return;
+ }
+
+ const copy = options;
+ for (const entry of options) {
+ if (entry.id === entryId) {
+ entry.selected = true;
+ if (multiple) {
+ searchValue = "";
+ } else {
+ searchValue = entry.name;
+ }
+ } else if (!multiple) {
+ entry.selected = false;
+ }
+ }
+ options = methods.get_sorted_array(copy);
+ searchInputNode?.focus();
+ searchResults = [];
+ },
+ deselect_entry(entryId: string) {
+ if (!entryId || loading) {
+ console.log("Not deselecting entry due to failed preconditions", {
+ entryId,
+ loading,
+ });
+ return;
+ }
+ console.log("Deselecting entry", entryId);
+
+ const copy = options;
+
+ for (const entry of copy) {
+ if (entry.id === entryId) {
+ entry.selected = false;
+ }
+ }
+
+ options = methods.get_sorted_array(copy);
+ searchInputNode?.focus();
+ },
+ get_sorted_array(options: Array<ComboboxOption>): Array<ComboboxOption> {
+ if (!options) {
+ return;
+ }
+ if (options.length < 1) {
+ return [];
+ }
+ if (searchValue) {
+ return options;
+ }
+
+ return options.sort((a, b) => search.normalise_value(a.name).localeCompare(search.normalise_value(b.name)));
+ },
+ };
+
+ const windowEvents = {
+ on_mousemove(event: any) {
+ if (!event.target) return;
+ mouseIsOverDropdown = event.target?.closest("#" + INTERNAL_ID + " .tongue") != null ?? false;
+ mouseIsOverComponent = event.target?.closest("#" + INTERNAL_ID) != null ?? false;
+ },
+ on_click() {
+ if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent) {
+ showDropdown = false;
+ }
+ },
+ on_keydown(event: any) {
+ lastKeydownCode = event.code;
+ const enterPressed = event.code === "Enter";
+ const backspacePressed = event.code === "Backspace";
+ const arrowUpPressed = event.code === "ArrowUp";
+ const spacePressed = event.code === "Space";
+ const arrowDownPressed = event.code === "ArrowDown";
+ const searchInputHasFocus = element_has_focus(searchInputNode);
+ const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul li.focus") as HTMLLIElement;
+
+ if (showDropdown && (enterPressed || arrowDownPressed || arrowUpPressed)) {
+ event.preventDefault();
+ }
+
+ if (searchInputHasFocus && backspacePressed && !searchValue && options.length > 0) {
+ if (options.filter((c) => c.selected === true).at(-1)?.id ?? false) {
+ methods.deselect_entry(options.filter((c) => c.selected === true).at(-1)?.id ?? "");
+ }
+ return;
+ }
+
+ if (searchInputHasFocus && enterPressed && showCreationHint) {
+ methods.create_entry(searchValue.trim());
+ return;
+ }
+
+ if (searchInputHasFocus && !focusedEntry && arrowDownPressed) {
+ const firstEntry = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type");
+ if (firstEntry) {
+ firstEntry.classList.add("focus");
+ return;
+ }
+ }
+
+ if (focusedEntry && (arrowUpPressed || arrowDownPressed)) {
+ if (arrowDownPressed) {
+ if (focusedEntry.nextElementSibling) {
+ focusedEntry.nextElementSibling.classList.add("focus");
+ focusedEntry.nextElementSibling.scrollIntoView(false);
+ } else {
+ const firstLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type");
+ firstLIEl.classList.add("focus");
+ firstLIEl.scrollIntoView(false);
+ }
+ } else if (arrowUpPressed) {
+ if (focusedEntry.previousElementSibling) {
+ focusedEntry.previousElementSibling.classList.add("focus");
+ focusedEntry.previousElementSibling.scrollIntoView(false);
+ } else {
+ const lastLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type");
+ lastLIEl.classList.add("focus");
+ lastLIEl.scrollIntoView(false);
+ }
+ }
+ focusedEntry.classList.remove("focus");
+ return;
+ }
+
+ if (focusedEntry && (spacePressed || enterPressed)) {
+ methods.select_entry(focusedEntry.dataset.id);
+ return;
+ }
+
+ if (lastKeydownCode === "Tab" && !searchInputHasFocus) {
+ showDropdown = false;
+ }
+ },
+ on_touchend(event) {
+ windowEvents.on_mousemove(event);
+ },
+ };
+</script>
+
+<svelte:window
+ on:keydown={windowEvents.on_keydown}
+ on:mousemove={windowEvents.on_mousemove}
+ on:touchend={windowEvents.on_touchend}
+ on:click={windowEvents.on_click}
+/>
+
+<div id={INTERNAL_ID} class:cursor-wait={loading}>
+ {#if label}
+ <label for={id} class="block text-sm font-medium text-gray-700">
+ {label}
+ {@html required ? "<span class='text-red-500'>*</span>" : ""}
+ </label>
+ {/if}
+ <div class="relative {label ? 'mt-1' : ''}">
+ <div
+ on:click={search.on_input_wrapper_focus}
+ on:keypress={search.on_input_wrapper_focus}
+ class="cursor-text w-full flex rounded-md border bg-white py-2 pl-3 pr-12 sm:text-sm
+ {inputHasFocus ? `border-${colorName}-500 outline-none ring-1 ring-${colorName}-500` : 'shadow-sm border-gray-300'}"
+ >
+ {#if multiple === true && hasSelection}
+ <div class="flex gap-1 flex-wrap">
+ {#each options.filter((c) => c.selected === true) as option}
+ <Badge
+ id={option.id}
+ removable
+ tabindex="-1"
+ on:remove={(e) => methods.deselect_entry(e.detail.id)}
+ text={option.name}
+ />
+ {/each}
+ </div>
+ {/if}
+ <div>
+ <input
+ {...attributes}
+ type="text"
+ style="all: unset;"
+ role="combobox"
+ aria-controls={optionsListId}
+ aria-expanded={showDropdown}
+ bind:value={searchValue}
+ bind:this={searchInputNode}
+ on:input={() => search.do()}
+ on:click={search.on_input_click}
+ on:focus={search.on_input_focus}
+ on:blur={search.on_input_focusout}
+ autocomplete="off"
+ />
+ {#if hasSelection}
+ <button
+ type="button"
+ on:click={() => reset()}
+ title={$LL.reset()}
+ tabindex="-1"
+ class="text-gray-400 absolute cursor-pointer inset-y-0 right-0 flex items-center rounded-r-md px-2"
+ >
+ <XIcon />
+ </button>
+ {:else}
+ <span tabindex="-1" class="text-gray-400 absolute inset-y-0 right-0 flex items-center rounded-r-md px-2">
+ <ChevronUpDownIcon />
+ </span>
+ {/if}
+ </div>
+ </div>
+ {#if errorText}
+ <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}>
+ {errorText}
+ </p>
+ {/if}
+ <div
+ class="tongue {showDropdown ? 'absolute' : 'hidden'}
+ z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white
+ text-base shadow-lg ring-1 ring-teal ring-opacity-5 focus:outline-none sm:text-sm"
+ >
+ <ul id={optionsListId} role="listbox" tabindex="-1">
+ {#if searchResults.length > 0}
+ {#each searchResults.filter((c) => !c.selected) as result}
+ <li
+ class="item"
+ data-id={result.obj.id}
+ aria-selected={result.obj.selected}
+ role="option"
+ on:click={on_select}
+ on:keypress={on_select}
+ tabindex="-1"
+ >
+ {@html highlight(result, '<span class="font-bold">', "</span>")}
+ </li>
+ {/each}
+ {:else if options.length > 0}
+ {#each options as option}
+ <!--
+ Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
+ Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
+ -->
+ <li
+ class="item"
+ aria-selected={option.selected}
+ role="option"
+ data-id={option.id}
+ on:click={on_select}
+ on:keypress={on_select}
+ tabindex="-1"
+ >
+ <span class="block truncate {option.selected ? 'text-semibold' : ''}">{option.name}</span>
+ {#if option.selected}
+ <span class="absolute inset-y-0 right-0 flex items-center pr-4 text-{colorName}-600">
+ <CheckCircleIcon />
+ </span>
+ {/if}
+ </li>
+ {/each}
+ {:else}
+ <slot name="no-records">
+ <p class="px-2">{noResultsText}</p>
+ {#if createable && !searchValue}
+ <p class="px-2 text-gray-500">{$LL.combobox.createRecordHelpText()}</p>
+ {/if}
+ </slot>
+ {/if}
+ </ul>
+ {#if showCreationHint}
+ <div class="sticky bottom-0 w-full bg-white">
+ <Button
+ text={$LL.combobox.createRecordButtonText(searchValue.trim())}
+ title={$LL.combobox.createRecordButtonText(searchValue.trim())}
+ {loading}
+ kind="reset"
+ type="button"
+ on:click={() => methods.create_entry(searchValue.trim())}
+ />
+ </div>
+ {/if}
+ </div>
+ </div>
+</div>
+
+<style lang="postcss">
+ .focus {
+ @apply text-white bg-teal-300;
+ }
+
+ .item {
+ @apply relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900;
+ }
+
+ .item[aria-selected="true"] {
+ @apply bg-teal-200;
+ }
+</style>
diff --git a/code/app/src/components/icons/adjustments.svelte b/code/app/src/components/icons/adjustments.svelte
new file mode 100644
index 0000000..83bda27
--- /dev/null
+++ b/code/app/src/components/icons/adjustments.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
+ />
+</svg>
diff --git a/code/app/src/components/icons/bars-3-center-left.svelte b/code/app/src/components/icons/bars-3-center-left.svelte
new file mode 100644
index 0000000..785ece3
--- /dev/null
+++ b/code/app/src/components/icons/bars-3-center-left.svelte
@@ -0,0 +1,15 @@
+<svg
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5"
+ />
+</svg>
diff --git a/code/app/src/components/icons/calendar.svelte b/code/app/src/components/icons/calendar.svelte
new file mode 100644
index 0000000..e0053ee
--- /dev/null
+++ b/code/app/src/components/icons/calendar.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6 {$$restProps.class ?? ''}"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"
+ />
+</svg>
diff --git a/code/app/src/components/icons/check-circle.svelte b/code/app/src/components/icons/check-circle.svelte
new file mode 100644
index 0000000..e30778e
--- /dev/null
+++ b/code/app/src/components/icons/check-circle.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/chevron-down.svelte b/code/app/src/components/icons/chevron-down.svelte
new file mode 100644
index 0000000..5b29ece
--- /dev/null
+++ b/code/app/src/components/icons/chevron-down.svelte
@@ -0,0 +1,7 @@
+<svg class="h-5 w-5 {$$restProps.class ?? ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
+ <path
+ fill-rule="evenodd"
+ d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/chevron-up-down.svelte b/code/app/src/components/icons/chevron-up-down.svelte
new file mode 100644
index 0000000..c07aed5
--- /dev/null
+++ b/code/app/src/components/icons/chevron-up-down.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/chevron-up.svelte b/code/app/src/components/icons/chevron-up.svelte
new file mode 100644
index 0000000..289e71d
--- /dev/null
+++ b/code/app/src/components/icons/chevron-up.svelte
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
+ <path
+ fill-rule="evenodd"
+ d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/database.svelte b/code/app/src/components/icons/database.svelte
new file mode 100644
index 0000000..6ffdadb
--- /dev/null
+++ b/code/app/src/components/icons/database.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
+ />
+</svg>
diff --git a/code/app/src/components/icons/exclamation-circle.svelte b/code/app/src/components/icons/exclamation-circle.svelte
new file mode 100644
index 0000000..2ce79b1
--- /dev/null
+++ b/code/app/src/components/icons/exclamation-circle.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/exclamation-triangle.svelte b/code/app/src/components/icons/exclamation-triangle.svelte
new file mode 100644
index 0000000..8d807db
--- /dev/null
+++ b/code/app/src/components/icons/exclamation-triangle.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M8.485 3.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 3.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/folder-open.svelte b/code/app/src/components/icons/folder-open.svelte
new file mode 100644
index 0000000..409c8e2
--- /dev/null
+++ b/code/app/src/components/icons/folder-open.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6 {$$restProps.class ?? ''}"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"
+ />
+</svg>
diff --git a/code/app/src/components/icons/funnel.svelte b/code/app/src/components/icons/funnel.svelte
new file mode 100644
index 0000000..7e9daeb
--- /dev/null
+++ b/code/app/src/components/icons/funnel.svelte
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
+ <path
+ fill-rule="evenodd"
+ d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 01.628.74v2.288a2.25 2.25 0 01-.659 1.59l-4.682 4.683a2.25 2.25 0 00-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 018 18.25v-5.757a2.25 2.25 0 00-.659-1.591L2.659 6.22A2.25 2.25 0 012 4.629V2.34a.75.75 0 01.628-.74z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/home.svelte b/code/app/src/components/icons/home.svelte
new file mode 100644
index 0000000..ee8305d
--- /dev/null
+++ b/code/app/src/components/icons/home.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
+ />
+</svg>
diff --git a/code/app/src/components/icons/index.ts b/code/app/src/components/icons/index.ts
new file mode 100644
index 0000000..eb5b439
--- /dev/null
+++ b/code/app/src/components/icons/index.ts
@@ -0,0 +1,47 @@
+import XIcon from "./x.svelte";
+import MenuIcon from "./menu.svelte";
+import AdjustmentsIcon from "./adjustments.svelte";
+import DatabaseIcon from "./database.svelte";
+import HomeIcon from "./home.svelte";
+import InformationCircleIcon from "./information-circle.svelte";
+import ExclamationTriangleIcon from "./exclamation-triangle.svelte";
+import XCircleIcon from "./x-circle.svelte";
+import CheckCircleIcon from "./check-circle.svelte";
+import XMarkIcon from "./x-mark.svelte";
+import SpinnerIcon from "./spinner.svelte";
+import ExclamationCircleIcon from "./exclamation-circle.svelte";
+import ChevronUpDownIcon from "./chevron-up-down.svelte";
+import MagnifyingGlassIcon from "./magnifying-glass.svelte";
+import Bars3CenterLeftIcon from "./bars-3-center-left.svelte";
+import CalendarIcon from "./calendar.svelte";
+import FolderOpenIcon from "./folder-open.svelte";
+import MegaphoneIcon from "./megaphone.svelte";
+import QueueListIcon from "./queue-list.svelte";
+import ChevronDownIcon from "./chevron-down.svelte";
+import ChevronUpIcon from "./chevron-up.svelte";
+import FunnelIcon from "./funnel.svelte";
+
+export {
+ FunnelIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+ QueueListIcon,
+ FolderOpenIcon,
+ MegaphoneIcon,
+ CalendarIcon,
+ Bars3CenterLeftIcon,
+ MagnifyingGlassIcon,
+ ChevronUpDownIcon,
+ XIcon,
+ MenuIcon,
+ HomeIcon,
+ DatabaseIcon,
+ AdjustmentsIcon,
+ InformationCircleIcon,
+ ExclamationTriangleIcon,
+ ExclamationCircleIcon,
+ XCircleIcon,
+ CheckCircleIcon,
+ XMarkIcon,
+ SpinnerIcon
+} \ No newline at end of file
diff --git a/code/app/src/components/icons/information-circle.svelte b/code/app/src/components/icons/information-circle.svelte
new file mode 100644
index 0000000..68dbc60
--- /dev/null
+++ b/code/app/src/components/icons/information-circle.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M19 10.5a8.5 8.5 0 11-17 0 8.5 8.5 0 0117 0zM8.25 9.75A.75.75 0 019 9h.253a1.75 1.75 0 011.709 2.13l-.46 2.066a.25.25 0 00.245.304H11a.75.75 0 010 1.5h-.253a1.75 1.75 0 01-1.709-2.13l.46-2.066a.25.25 0 00-.245-.304H9a.75.75 0 01-.75-.75zM10 7a1 1 0 100-2 1 1 0 000 2z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/magnifying-glass.svelte b/code/app/src/components/icons/magnifying-glass.svelte
new file mode 100644
index 0000000..f8fdb6e
--- /dev/null
+++ b/code/app/src/components/icons/magnifying-glass.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/megaphone.svelte b/code/app/src/components/icons/megaphone.svelte
new file mode 100644
index 0000000..7ada5f3
--- /dev/null
+++ b/code/app/src/components/icons/megaphone.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6 {$$restProps.class ?? ''}"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
+ />
+</svg>
diff --git a/code/app/src/components/icons/menu.svelte b/code/app/src/components/icons/menu.svelte
new file mode 100644
index 0000000..471d85f
--- /dev/null
+++ b/code/app/src/components/icons/menu.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4 6h16M4 12h16M4 18h16"
+ />
+</svg>
diff --git a/code/app/src/components/icons/queue-list.svelte b/code/app/src/components/icons/queue-list.svelte
new file mode 100644
index 0000000..6148394
--- /dev/null
+++ b/code/app/src/components/icons/queue-list.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6 {$$restProps.class ?? ''}"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z"
+ />
+</svg>
diff --git a/code/app/src/components/icons/spinner.svelte b/code/app/src/components/icons/spinner.svelte
new file mode 100644
index 0000000..80cc57c
--- /dev/null
+++ b/code/app/src/components/icons/spinner.svelte
@@ -0,0 +1,20 @@
+<svg
+ class="-ml-1 mr-3 h-5 w-5 animate-spin {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+>
+ <circle
+ class="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ stroke-width="4"
+ />
+ <path
+ class="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ />
+</svg>
diff --git a/code/app/src/components/icons/x-circle.svelte b/code/app/src/components/icons/x-circle.svelte
new file mode 100644
index 0000000..3793b5a
--- /dev/null
+++ b/code/app/src/components/icons/x-circle.svelte
@@ -0,0 +1,13 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
+ clip-rule="evenodd"
+ />
+</svg>
diff --git a/code/app/src/components/icons/x-mark.svelte b/code/app/src/components/icons/x-mark.svelte
new file mode 100644
index 0000000..fd1c6a1
--- /dev/null
+++ b/code/app/src/components/icons/x-mark.svelte
@@ -0,0 +1,11 @@
+<svg
+ class="h-5 w-5 {$$restProps.class ?? ''}"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+>
+ <path
+ d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+ />
+</svg>
diff --git a/code/app/src/components/icons/x.svelte b/code/app/src/components/icons/x.svelte
new file mode 100644
index 0000000..6125ab8
--- /dev/null
+++ b/code/app/src/components/icons/x.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="h-6 w-6 {$$restProps.class ?? ''}"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ stroke-width="2"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M6 18L18 6M6 6l12 12"
+ />
+</svg>
diff --git a/code/app/src/components/index.ts b/code/app/src/components/index.ts
new file mode 100644
index 0000000..494bc0c
--- /dev/null
+++ b/code/app/src/components/index.ts
@@ -0,0 +1,25 @@
+import Alert from "./alert.svelte";
+import Button from "./button.svelte";
+import Checkbox from "./checkbox.svelte";
+import Input from "./input.svelte";
+import LocaleSwitcher from "./locale-switcher.svelte";
+import Switch from "./switch.svelte";
+import Badge from "./badge.svelte";
+import ProjectStatusBadge from "./project-status-badge.svelte";
+import TextArea from "./textarea.svelte";
+import Combobox from "./combobox.svelte";
+import Notification from "./notification.svelte";
+
+export {
+ Badge,
+ Combobox,
+ TextArea,
+ ProjectStatusBadge,
+ Alert,
+ Button,
+ Checkbox,
+ Input,
+ LocaleSwitcher,
+ Switch,
+ Notification,
+}; \ No newline at end of file
diff --git a/code/app/src/components/input.svelte b/code/app/src/components/input.svelte
new file mode 100644
index 0000000..f97c1f1
--- /dev/null
+++ b/code/app/src/components/input.svelte
@@ -0,0 +1,112 @@
+<script lang="ts">
+ import pwKey from "$actions/pwKey";
+ import {random_string} from "$utilities/misc-helpers";
+ import {ExclamationCircleIcon} from "./icons";
+
+ export let label: string | undefined = undefined;
+ export let type: string = "text";
+ export let autocomplete: string | undefined = undefined;
+ export let required: boolean | undefined = undefined;
+ export let id: string | undefined = "input__" + random_string(4);
+ export let name: string | undefined = undefined;
+ export let placeholder: string | undefined = undefined;
+ export let helpText: string | undefined = undefined;
+ export let errorText: string | undefined = undefined;
+ export let errors: Array<string> | undefined = undefined;
+ export let disabled = false;
+ export let hideLabel = false;
+ export let cornerHint: string | undefined = undefined;
+ export let icon: any = undefined;
+ export let addon: string | undefined = undefined;
+ export let value: string | undefined;
+ export let wrapperClass: string | undefined = undefined;
+ export let _pwKey: string | undefined = undefined;
+
+ $: ariaErrorDescribedBy = id + "__" + "error";
+ $: attributes = {
+ "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null,
+ "aria-invalid": errorText || errors?.length ? "true" : null,
+ disabled: disabled || null,
+ autocomplete: autocomplete || null,
+ required: required || null,
+ } as any;
+ $: hasBling = icon || addon || errorText;
+ const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500";
+ let colorClass = defaultColorClass;
+ $: if (errorText) {
+ colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300";
+ } else {
+ colorClass = defaultColorClass;
+ }
+
+ function typeAction(node: HTMLInputElement) {
+ node.type = type;
+ }
+</script>
+
+<div class={wrapperClass}>
+ {#if label && !cornerHint && !hideLabel}
+ <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}>
+ {label}
+ {@html required ? "<span class='text-red-500'>*</span>" : ""}
+ </label>
+ {:else if cornerHint && !hideLabel}
+ <div class="flex justify-between">
+ {#if label}
+ <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}>
+ {label}
+ {@html required ? "<span class='text-red-500'>*</span>" : ""}
+ </label>
+ {/if}
+ <span class="text-sm text-gray-500">
+ {cornerHint}
+ </span>
+ </div>
+ {/if}
+ <div class="{label ? 'mt-1' : ''} {hasBling ? 'relative rounded-md' : ''} {addon ? 'flex' : ''}">
+ {#if icon}
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+ <svelte:component this={icon} class={errorText ? "text-red-500" : "text-gray-400"}/>
+ </div>
+ {:else if addon}
+ <div class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm">
+ <span class="text-gray-500 sm:text-sm">{addon}</span>
+ </div>
+ {/if}
+ <input
+ use:typeAction
+ use:pwKey={_pwKey}
+ {name}
+ {id}
+ {...attributes}
+ bind:value
+ class="block w-full rounded-md shadow-sm sm:text-sm
+ {colorClass}
+ {disabled ? 'disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500' : ''}
+ {addon ? 'min-w-0 flex-1 rounded-none rounded-r-md' : ''}
+ {icon ? 'pl-10' : ''}"
+ {placeholder}
+ />
+ {#if errorText}
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
+ <ExclamationCircleIcon class="text-red-500"/>
+ </div>
+ {/if}
+ </div>
+ {#if helpText && !errorText}
+ <p class="mt-2 text-sm text-gray-500">
+ {helpText}
+ </p>
+ {/if}
+ {#if errorText || errors?.length === 1}
+ <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}>
+ {errorText ?? errors[0]}
+ </p>
+ {:else if errors && errors.length}
+ <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}>
+ {#each errors as error}
+ <li class="text-sm text-red-600">{error}</li>
+ {/each}
+ </ul>
+ {/if}
+</div>
diff --git a/code/app/src/components/locale-switcher.svelte b/code/app/src/components/locale-switcher.svelte
new file mode 100644
index 0000000..fc03f39
--- /dev/null
+++ b/code/app/src/components/locale-switcher.svelte
@@ -0,0 +1,56 @@
+<script lang="ts">
+ import pwKey from "$actions/pwKey";
+ import {browser} from "$app/environment";
+ import {page} from "$app/stores";
+ import {CookieNames} from "$configuration";
+ import {setLocale, locale} from "$i18n/i18n-svelte";
+ import type {Locales} from "$i18n/i18n-types";
+ import {locales} from "$i18n/i18n-util";
+ import {loadLocaleAsync} from "$i18n/i18n-util.async";
+ import Cookies from "js-cookie";
+
+ export let _pwKey: string | undefined = undefined;
+ export let tabindex: number | undefined = undefined;
+ let currentLocale = Cookies.get(CookieNames.locale);
+
+ async function switch_locale(newLocale: Locales) {
+ if (!newLocale || $locale === newLocale) return;
+ await loadLocaleAsync(newLocale);
+ setLocale(newLocale);
+ document.querySelector("html")?.setAttribute("lang", newLocale);
+ Cookies.set(CookieNames.locale, newLocale);
+ currentLocale = newLocale;
+ console.log("Switched to: " + newLocale);
+ }
+
+ function on_change(event: Event) {
+ const target = event.target as HTMLSelectElement;
+ switch_locale(target.options[target.selectedIndex].value as Locales);
+ }
+
+ $: if (browser) {
+ switch_locale($page.params.lang as Locales);
+ }
+
+ function get_locale_name(iso: string) {
+ switch (iso) {
+ case "nb": {
+ return "Norsk Bokmål";
+ }
+ case "en": {
+ return "English";
+ }
+ }
+ }
+</script>
+
+<select
+ {tabindex}
+ use:pwKey={_pwKey}
+ on:change={on_change}
+ class="mt-1 mr-1 block border-none py-2 pl-3 pr-10 text-base rounded-md right-0 absolute focus:outline-none focus:ring-teal-500 sm:text-sm"
+>
+ {#each locales as aLocale}
+ <option value={aLocale} selected={aLocale === currentLocale}>{get_locale_name(aLocale)}</option>
+ {/each}
+</select>
diff --git a/code/app/src/components/notification.svelte b/code/app/src/components/notification.svelte
new file mode 100644
index 0000000..d78b3d3
--- /dev/null
+++ b/code/app/src/components/notification.svelte
@@ -0,0 +1,119 @@
+<script context="module" lang="ts">
+ export type NotificationType = "info" | "error" | "success" | "warning" | "subtle";
+</script>
+
+<script lang="ts">
+ import { Transition } from "@rgossiaux/svelte-headlessui";
+ import { onDestroy } from "svelte";
+ import { XMarkIcon, ExclamationCircleIcon, InformationCircleIcon, XCircleIcon, CheckCircleIcon } from "./icons";
+
+ export let title: string;
+ export let subtitle = "";
+ export let show = false;
+ export let type: NotificationType = "info";
+ export let hideAfterSeconds = -1;
+ export let nonClosable = false;
+
+ $: _show = show && title.length > 0;
+ let timeout;
+ let iconClass = "";
+ let icon = undefined;
+ let bgClass = "";
+ let ringClass = "";
+
+ onDestroy(() => {
+ clearTimeout(timeout);
+ });
+
+ $: if (hideAfterSeconds > 0) {
+ timeout = setTimeout(() => close(), hideAfterSeconds * 1000);
+ } else {
+ timeout = -1;
+ show = true;
+ }
+
+ $: switch (type) {
+ case "error":
+ iconClass = "text-red-400";
+ bgClass = "bg-red-50";
+ ringClass = "ring-1 ring-red-100";
+ icon = XCircleIcon;
+ break;
+ case "info":
+ iconClass = "text-blue-400";
+ bgClass = "bg-blue-50";
+ ringClass = "ring-1 ring-blue-100";
+ icon = InformationCircleIcon;
+ break;
+ case "success":
+ iconClass = "text-green-400";
+ bgClass = "bg-green-50";
+ ringClass = "ring-1 ring-green-100";
+ icon = CheckCircleIcon;
+ break;
+ case "warning":
+ iconClass = "text-yellow-400";
+ bgClass = "bg-yellow-50";
+ ringClass = "ring-1 ring-yellow-100";
+ icon = ExclamationCircleIcon;
+ break;
+ case "subtle":
+ icon = undefined;
+ bgClass = "bg-white";
+ ringClass = "ring-1 ring-gray-100";
+ break;
+ default:
+ icon = undefined;
+ bgClass = "bg-white";
+ ringClass = "";
+ break;
+ }
+
+ function close() {
+ show = false;
+ }
+</script>
+
+<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
+ <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
+ <Transition
+ class="w-full flex justify-end"
+ show={_show}
+ enter="transform ease-out duration-300 transition"
+ enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
+ enterTo="translate-y-0 opacity-100 sm:translate-x-0"
+ leave="transition ease-in duration-100"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg shadow-md {bgClass} {ringClass}">
+ <div class="p-4">
+ <div class="flex items-start">
+ {#if icon}
+ <div class="flex-shrink-0">
+ <svelte:component this={icon} class={iconClass} />
+ </div>
+ {/if}
+ <div class="ml-3 w-0 flex-1 pt-0.5">
+ <p class="text-sm font-medium text-gray-900">{title}</p>
+ {#if subtitle}
+ <p class="mt-1 text-sm text-gray-500">{subtitle}</p>
+ {/if}
+ </div>
+ {#if !nonClosable}
+ <div class="ml-4 flex flex-shrink-0">
+ <button
+ on:click={close}
+ type="button"
+ class="inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ >
+ <XMarkIcon />
+ </button>
+ </div>
+ {/if}
+ </div>
+ </div>
+ </div>
+ </Transition>
+ </div>
+</div>
diff --git a/code/app/src/components/project-status-badge.svelte b/code/app/src/components/project-status-badge.svelte
new file mode 100644
index 0000000..3e93935
--- /dev/null
+++ b/code/app/src/components/project-status-badge.svelte
@@ -0,0 +1,25 @@
+<script lang="ts">
+ import type {ProjectStatus} from "$models/projects/ProjectStatus";
+ import Badge from "./badge.svelte";
+
+ export let status: string | ProjectStatus;
+
+ let text = "";
+ let type = "default" as any;
+ $: switch (status) {
+ case "idl":
+ type = "tame";
+ text = "IDLE";
+ break;
+ case "exp":
+ type = "yellow";
+ text = "EXPIRED";
+ break;
+ case "act":
+ type = "green";
+ text = "ACTIVE";
+ break;
+ }
+</script>
+
+<Badge {text} {type} uppercase/>
diff --git a/code/app/src/components/switch.svelte b/code/app/src/components/switch.svelte
new file mode 100644
index 0000000..1b67f80
--- /dev/null
+++ b/code/app/src/components/switch.svelte
@@ -0,0 +1,125 @@
+<script context="module" lang="ts">
+ export type SwitchType = "short" | "icon" | "default";
+</script>
+
+<script lang="ts">
+ import pwKey from "$actions/pwKey";
+
+ export let enabled = false;
+ export let type: SwitchType = "default";
+ export let srText = "Use setting";
+ export let label: string | undefined = undefined;
+ export let description: string | undefined = undefined;
+ export let rightAlignedLabelDescription = false;
+ export let _pwKey: string | undefined = undefined;
+
+ $: colorClass = enabled ? "bg-teal-600 focus:ring-teal-500" : "bg-gray-200 focus:ring-teal-500";
+ $: translateClass = enabled ? "translate-x-5" : "translate-x-0";
+ $: hasLabelOrDescription = label || description;
+
+ function toggle() {
+ enabled = !enabled;
+ }
+</script>
+
+<div class="{hasLabelOrDescription ? 'flex items-center' : ''} {rightAlignedLabelDescription ? '' : 'justify-between'}">
+ {#if hasLabelOrDescription && !rightAlignedLabelDescription}
+ <span class="flex flex-grow flex-col">
+ {#if label}
+ <span class="text-sm font-medium text-gray-900">{label}</span>
+ {/if}
+ {#if description}
+ <span class="text-sm text-gray-500">{description}</span>
+ {/if}
+ </span>
+ {/if}
+ {#if type === "short"}
+ <button
+ type="button"
+ class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2"
+ role="switch"
+ aria-checked={enabled}
+ use:pwKey={_pwKey}
+ on:click={toggle}
+ >
+ <span class="sr-only">{srText}</span>
+ <span aria-hidden="true" class="pointer-events-none absolute h-full w-full rounded-md"/>
+ <span
+ aria-hidden="true"
+ class="{colorClass} pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out"
+ />
+ <span
+ aria-hidden="true"
+ class="{translateClass} pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out"
+ />
+ </button>
+ {:else if type === "icon"}
+ <button
+ type="button"
+ class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2"
+ role="switch"
+ aria-checked={enabled}
+ use:pwKey={_pwKey}
+ on:click={toggle}
+ >
+ <span class="sr-only">{srText}</span>
+ <span
+ class="{translateClass} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ >
+ <span
+ class="{enabled
+ ? 'opacity-0 ease-out duration-100'
+ : 'opacity-100 ease-in duration-200'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
+ aria-hidden="true"
+ >
+ <svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
+ <path
+ d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ </span>
+ <span
+ class="{enabled
+ ? 'opacity-100 ease-in duration-200'
+ : 'opacity-0 ease-out duration-100'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
+ aria-hidden="true"
+ >
+ <svg class="h-3 w-3 text-indigo-600" fill="currentColor" viewBox="0 0 12 12">
+ <path
+ d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
+ />
+ </svg>
+ </span>
+ </span>
+ </button>
+ {:else if type === "default"}
+ <button
+ type="button"
+ class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2"
+ role="switch"
+ aria-checked={enabled}
+ use:pwKey={_pwKey}
+ on:click={toggle}
+ >
+ <span class="sr-only">{srText}</span>
+ <span
+ aria-hidden="true"
+ class="{translateClass} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ />
+ </button>
+ {/if}
+ {#if hasLabelOrDescription && rightAlignedLabelDescription}
+ <span class="ml-3">
+ {#if label}
+ <span class="text-sm font-medium text-gray-900">{label}</span>
+ {/if}
+ {#if description}
+ <span class="text-sm text-gray-500">{description}</span>
+ {/if}
+ </span>
+ {/if}
+</div>
diff --git a/code/app/src/components/textarea.svelte b/code/app/src/components/textarea.svelte
new file mode 100644
index 0000000..223a265
--- /dev/null
+++ b/code/app/src/components/textarea.svelte
@@ -0,0 +1,81 @@
+<script lang="ts">
+ import {random_string} from "$utilities/misc-helpers";
+
+ export let id = "textarea-" + random_string(4);
+ export let disabled = false;
+ export let rows = 2;
+ export let cols = 0;
+ export let name = "";
+ export let placeholder = "";
+ export let value;
+ export let label = "";
+ export let required = false;
+ export let errorText = "";
+ export let errors: Array<string> | undefined = undefined;
+
+ $: ariaErrorDescribedBy = id + "__" + "error";
+ $: attributes = {
+ "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null,
+ "aria-invalid": errorText || errors?.length ? "true" : null,
+ rows: rows || null,
+ cols: cols || null,
+ name: name || null,
+ id: id || null,
+ disabled: disabled || null,
+ required: required || null,
+ } as any;
+
+ let textareaElement;
+ let scrollHeight = 0;
+ const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500";
+ let colorClass = defaultColorClass;
+
+ $: if (errorText) {
+ colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300";
+ } else {
+ colorClass = defaultColorClass;
+ }
+
+ $: if (textareaElement) {
+ scrollHeight = textareaElement.scrollHeight;
+ }
+
+ function on_input(event) {
+ event.target.style.height = "auto";
+ event.target.style.height = this.scrollHeight + "px";
+ }
+</script>
+
+<div>
+ {#if label}
+ <label for={id} class="block text-sm font-medium text-gray-700">
+ {label}
+ {@html required ? "<span class='text-red-500'>*</span>" : ""}
+ </label>
+ {/if}
+ <div class="mt-1">
+ <textarea
+ {rows}
+ {name}
+ {id}
+ {...attributes}
+ style="overflow-y:hidden;min-height:calc(1.5em + .75rem + 2px);{scrollHeight ? 'height:{scrollHeight}px' : ''};"
+ bind:value
+ bind:this={textareaElement}
+ on:input={on_input}
+ {placeholder}
+ class="block w-full rounded-md {colorClass} shadow-sm sm:text-sm"
+ />
+ {#if errorText || errors?.length === 1}
+ <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}>
+ {errorText ?? errors[0]}
+ </p>
+ {:else if errors && errors.length}
+ <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}>
+ {#each errors as error}
+ <li class="text-sm text-red-600">{error}</li>
+ {/each}
+ </ul>
+ {/if}
+ </div>
+</div>
diff --git a/code/app/src/configuration/index.ts b/code/app/src/configuration/index.ts
new file mode 100644
index 0000000..abf6ac5
--- /dev/null
+++ b/code/app/src/configuration/index.ts
@@ -0,0 +1,64 @@
+import { env } from "$env/dynamic/private";
+
+export const APP_ADDRESS = "https://stage.greatoffice.app";
+export const API_ADDRESS = "https://stage-api.greatoffice.app";
+export const DEV_APP_ADDRESS = "http://localhost";
+export const DEV_API_ADDRESS = "http://localhost:5000";
+
+export function api_base(path: string = "", explicitVersion = 1): string {
+ if (path && !path.startsWith("_")) path = "v" + explicitVersion + path;
+ return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path !== "" ? "/" + path : "");
+}
+
+export function is_development(): boolean {
+ return import.meta.env.DEV;
+}
+
+export function is_testing(): boolean {
+ return env.TESTING == "true";
+}
+
+export function is_debug(): boolean {
+ return localStorage.getItem(StorageKeys.debug) !== "true";
+}
+
+export const CookieNames = {
+ theme: "go_theme",
+ locale: "go_locale",
+ session: "go_session",
+};
+
+export function get_test_context(): TestContext {
+ return {
+ user: {
+ username: env.TEST_USERNAME,
+ password: env.TEST_PASSWORD,
+ },
+ };
+}
+
+export interface TestContext {
+ user: {
+ username: string,
+ password: string
+ };
+}
+
+export const QueryKeys = {
+ labels: "labels",
+ categories: "categories",
+ entries: "entries",
+};
+
+export const StorageKeys = {
+ session: "sessionData",
+ theme: "theme",
+ debug: "debug",
+ categories: "categories",
+ labels: "labels",
+ entries: "entries",
+ stopwatch: "stopwatchState",
+ logLevel: "logLevel",
+};
+
+export type PortalMessage = "emailValidated"; \ No newline at end of file
diff --git a/code/app/src/global.d.ts b/code/app/src/global.d.ts
new file mode 100644
index 0000000..13f5e16
--- /dev/null
+++ b/code/app/src/global.d.ts
@@ -0,0 +1,11 @@
+/// <reference types="@sveltejs/kit" />
+
+type Locales = import('$lib/i18n/i18n-types').Locales
+type TranslationFunctions = import('$lib/i18n/i18n-types').TranslationFunctions
+
+declare namespace App {
+ interface Locals {
+ locale: Locales
+ LL: TranslationFunctions
+ }
+} \ No newline at end of file
diff --git a/code/app/src/hooks.server.ts b/code/app/src/hooks.server.ts
new file mode 100644
index 0000000..2720480
--- /dev/null
+++ b/code/app/src/hooks.server.ts
@@ -0,0 +1,49 @@
+import { CookieNames } from "$configuration";
+import { detectLocale, i18n, isLocale, locales } from "$i18n/i18n-util";
+import { log_debug } from "$utilities/logger";
+import type { Handle, RequestEvent } from "@sveltejs/kit";
+import { initAcceptLanguageHeaderDetector } from "typesafe-i18n/detectors";
+import type { Locales } from "$i18n/i18n-types";
+import { loadAllLocales } from "$i18n/i18n-util.sync";
+
+loadAllLocales();
+const L = i18n();
+
+export const handle: Handle = async ({ event, resolve }) => {
+ const localeCookie = event.cookies.get(CookieNames.locale);
+ const preferredLocale = getPreferredLocale(event);
+ let finalLocale = localeCookie ?? preferredLocale;
+ let forceCookieSet = false;
+
+ log_debug("Handling locale", {
+ locales,
+ localeCookie,
+ preferredLocale,
+ finalLocale,
+ });
+
+ if (!isLocale(finalLocale)) {
+ log_debug(finalLocale + " is not a valid locale or it does not exist, switching to default: en");
+ finalLocale = "en";
+ forceCookieSet = true;
+ }
+
+ if (!localeCookie || forceCookieSet) {
+ // Set a locale cookie
+ event.cookies.set(CookieNames.locale, finalLocale, {
+ sameSite: "strict",
+ path: "/",
+ httpOnly: false,
+ });
+ }
+
+ event.locals.locale = finalLocale as Locales;
+ event.locals.LL = L[finalLocale as Locales];
+
+ return resolve(event, { transformPageChunk: ({ html }) => html.replace("%lang%", finalLocale) });
+};
+
+function getPreferredLocale(event: RequestEvent) {
+ const acceptLanguageDetector = initAcceptLanguageHeaderDetector(event.request);
+ return detectLocale(acceptLanguageDetector);
+}
diff --git a/code/app/src/i18n/en/app/index.ts b/code/app/src/i18n/en/app/index.ts
new file mode 100644
index 0000000..7ccfc97
--- /dev/null
+++ b/code/app/src/i18n/en/app/index.ts
@@ -0,0 +1,7 @@
+import type { BaseTranslation } from '../../i18n-types'
+
+const en_app: BaseTranslation = {
+ members: "Members",
+}
+
+export default en_app \ No newline at end of file
diff --git a/code/app/src/i18n/en/index.ts b/code/app/src/i18n/en/index.ts
new file mode 100644
index 0000000..b38eb48
--- /dev/null
+++ b/code/app/src/i18n/en/index.ts
@@ -0,0 +1,63 @@
+import type { BaseTranslation } from "../i18n-types";
+
+const en: BaseTranslation = {
+ or: "Or",
+ name: "Name",
+ emailAddress: "Email address",
+ password: "Password",
+ pageNotFound: "Page not found",
+ noInternet: "It seems like your device does not have a internet connection, please check your connection.",
+ reset: "Reset",
+ of: "{0} of {1}",
+ isRequired: "{0} is required",
+ submit: "Submit",
+ success: "Success",
+ tryAgainSoon: "Try again soon",
+ createANewAccount: "Create a new account",
+ unexpectedError: "An unexpected error occured",
+ notFound: "Not found",
+ documentation: "Documentation",
+ tos: "Terms of service",
+ privacyPolicy: "Privacy policy",
+ signIntoYourAccount: "Sign into your account",
+ combobox: {
+ search: "Search",
+ noRecordsFound: "No records found",
+ createRecordHelpText: "Create a record by typing the name in the search bar and pressing enter",
+ createRecordButtonText: "Press enter or click here to create {0}"
+ },
+ signInPage: {
+ title: "Sign in",
+ notMyComputer: "This is not my computer",
+ resetPassword: "Reset password",
+ yourPasswordIsUpdated: "Your password is updated",
+ signIn: "Sign In",
+ yourNewPasswordIsApplied: "Your new password is applied",
+ signInBelow: "Sign in below",
+ yourAccountIsDisabled: "Your account is disabled",
+ contactYourAdminIfDisabled: "Contact your administrator if this feels wrong",
+ youHaveReachedInactivityLimit: "You've reached the hidden inactivity limit",
+ feelFreeToSignInAgain: "Feel free to sign in again"
+ },
+ signUpPage: {
+ title: "Sign up",
+ createYourNewAccount: "Create your new account",
+ },
+ resetPasswordPage: {
+ title: "Reset password",
+ fulfillTitle: "Set new password",
+ setANewPassword: "Set a new password",
+ expired: "Expired",
+ requestHasExpired: "Your request has expired",
+ requestANewReset: "Request a new reset",
+ invalidRequestTitle: "Your request is invalid",
+ invalidRequestMessage: "This could be due to it being expired, nonexsistent or something else",
+ newPassword: "New password",
+ requestSentMessage: "If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account.",
+ requestAPasswordReset: "Request a password reset",
+ requestNotFound: "Your request was not found",
+ submitANewRequestBelow: "Submit a new reset request below"
+ }
+};
+
+export default en;
diff --git a/code/app/src/i18n/formatters.ts b/code/app/src/i18n/formatters.ts
new file mode 100644
index 0000000..ade2f89
--- /dev/null
+++ b/code/app/src/i18n/formatters.ts
@@ -0,0 +1,13 @@
+import { capitalise } from "$utilities/misc-helpers";
+import type { FormattersInitializer } from "typesafe-i18n";
+import type { Locales, Formatters } from "./i18n-types";
+
+export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
+
+ const formatters: Formatters = {
+ // add your formatter functions here
+ capitalise: (value: string) => capitalise(value),
+ };
+
+ return formatters;
+};
diff --git a/code/app/src/i18n/i18n-svelte.ts b/code/app/src/i18n/i18n-svelte.ts
new file mode 100644
index 0000000..6cdffb3
--- /dev/null
+++ b/code/app/src/i18n/i18n-svelte.ts
@@ -0,0 +1,12 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { initI18nSvelte } from 'typesafe-i18n/svelte'
+import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales } from './i18n-util'
+
+const { locale, LL, setLocale } = initI18nSvelte<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
+
+export { locale, LL, setLocale }
+
+export default LL
diff --git a/code/app/src/i18n/i18n-types.ts b/code/app/src/i18n/i18n-types.ts
new file mode 100644
index 0000000..ef1d664
--- /dev/null
+++ b/code/app/src/i18n/i18n-types.ts
@@ -0,0 +1,461 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'
+
+export type BaseTranslation = BaseTranslationType & DisallowNamespaces
+export type BaseLocale = 'en'
+
+export type Locales =
+ | 'en'
+ | 'nb'
+
+export type Translation = RootTranslation & DisallowNamespaces
+
+export type Translations = RootTranslation &
+{
+ app: NamespaceAppTranslation
+}
+
+type RootTranslation = {
+ /**
+ * O​r
+ */
+ or: string
+ /**
+ * N​a​m​e
+ */
+ name: string
+ /**
+ * E​m​a​i​l​ ​a​d​d​r​e​s​s
+ */
+ emailAddress: string
+ /**
+ * P​a​s​s​w​o​r​d
+ */
+ password: string
+ /**
+ * P​a​g​e​ ​n​o​t​ ​f​o​u​n​d
+ */
+ pageNotFound: string
+ /**
+ * I​t​ ​s​e​e​m​s​ ​l​i​k​e​ ​y​o​u​r​ ​d​e​v​i​c​e​ ​d​o​e​s​ ​n​o​t​ ​h​a​v​e​ ​a​ ​i​n​t​e​r​n​e​t​ ​c​o​n​n​e​c​t​i​o​n​,​ ​p​l​e​a​s​e​ ​c​h​e​c​k​ ​y​o​u​r​ ​c​o​n​n​e​c​t​i​o​n​.
+ */
+ noInternet: string
+ /**
+ * R​e​s​e​t
+ */
+ reset: string
+ /**
+ * {​0​}​ ​o​f​ ​{​1​}
+ * @param {unknown} 0
+ * @param {unknown} 1
+ */
+ of: RequiredParams<'0' | '1'>
+ /**
+ * {​0​}​ ​i​s​ ​r​e​q​u​i​r​e​d
+ * @param {unknown} 0
+ */
+ isRequired: RequiredParams<'0'>
+ /**
+ * S​u​b​m​i​t
+ */
+ submit: string
+ /**
+ * S​u​c​c​e​s​s
+ */
+ success: string
+ /**
+ * T​r​y​ ​a​g​a​i​n​ ​s​o​o​n
+ */
+ tryAgainSoon: string
+ /**
+ * C​r​e​a​t​e​ ​a​ ​n​e​w​ ​a​c​c​o​u​n​t
+ */
+ createANewAccount: string
+ /**
+ * A​n​ ​u​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d
+ */
+ unexpectedError: string
+ /**
+ * N​o​t​ ​f​o​u​n​d
+ */
+ notFound: string
+ /**
+ * D​o​c​u​m​e​n​t​a​t​i​o​n
+ */
+ documentation: string
+ /**
+ * T​e​r​m​s​ ​o​f​ ​s​e​r​v​i​c​e
+ */
+ tos: string
+ /**
+ * P​r​i​v​a​c​y​ ​p​o​l​i​c​y
+ */
+ privacyPolicy: string
+ /**
+ * S​i​g​n​ ​i​n​t​o​ ​y​o​u​r​ ​a​c​c​o​u​n​t
+ */
+ signIntoYourAccount: string
+ combobox: {
+ /**
+ * S​e​a​r​c​h
+ */
+ search: string
+ /**
+ * N​o​ ​r​e​c​o​r​d​s​ ​f​o​u​n​d
+ */
+ noRecordsFound: string
+ /**
+ * C​r​e​a​t​e​ ​a​ ​r​e​c​o​r​d​ ​b​y​ ​t​y​p​i​n​g​ ​t​h​e​ ​n​a​m​e​ ​i​n​ ​t​h​e​ ​s​e​a​r​c​h​ ​b​a​r​ ​a​n​d​ ​p​r​e​s​s​i​n​g​ ​e​n​t​e​r
+ */
+ createRecordHelpText: string
+ /**
+ * P​r​e​s​s​ ​e​n​t​e​r​ ​o​r​ ​c​l​i​c​k​ ​h​e​r​e​ ​t​o​ ​c​r​e​a​t​e​ ​{​0​}
+ * @param {unknown} 0
+ */
+ createRecordButtonText: RequiredParams<'0'>
+ }
+ signInPage: {
+ /**
+ * S​i​g​n​ ​i​n
+ */
+ title: string
+ /**
+ * T​h​i​s​ ​i​s​ ​n​o​t​ ​m​y​ ​c​o​m​p​u​t​e​r
+ */
+ notMyComputer: string
+ /**
+ * R​e​s​e​t​ ​p​a​s​s​w​o​r​d
+ */
+ resetPassword: string
+ /**
+ * Y​o​u​r​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​u​p​d​a​t​e​d
+ */
+ yourPasswordIsUpdated: string
+ /**
+ * S​i​g​n​ ​I​n
+ */
+ signIn: string
+ /**
+ * Y​o​u​r​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​a​p​p​l​i​e​d
+ */
+ yourNewPasswordIsApplied: string
+ /**
+ * S​i​g​n​ ​i​n​ ​b​e​l​o​w
+ */
+ signInBelow: string
+ /**
+ * Y​o​u​r​ ​a​c​c​o​u​n​t​ ​i​s​ ​d​i​s​a​b​l​e​d
+ */
+ yourAccountIsDisabled: string
+ /**
+ * C​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​ ​i​f​ ​t​h​i​s​ ​f​e​e​l​s​ ​w​r​o​n​g
+ */
+ contactYourAdminIfDisabled: string
+ /**
+ * Y​o​u​'​v​e​ ​r​e​a​c​h​e​d​ ​t​h​e​ ​h​i​d​d​e​n​ ​i​n​a​c​t​i​v​i​t​y​ ​l​i​m​i​t
+ */
+ youHaveReachedInactivityLimit: string
+ /**
+ * F​e​e​l​ ​f​r​e​e​ ​t​o​ ​s​i​g​n​ ​i​n​ ​a​g​a​i​n
+ */
+ feelFreeToSignInAgain: string
+ }
+ signUpPage: {
+ /**
+ * S​i​g​n​ ​u​p
+ */
+ title: string
+ /**
+ * C​r​e​a​t​e​ ​y​o​u​r​ ​n​e​w​ ​a​c​c​o​u​n​t
+ */
+ createYourNewAccount: string
+ }
+ resetPasswordPage: {
+ /**
+ * R​e​s​e​t​ ​p​a​s​s​w​o​r​d
+ */
+ title: string
+ /**
+ * S​e​t​ ​n​e​w​ ​p​a​s​s​w​o​r​d
+ */
+ fulfillTitle: string
+ /**
+ * S​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d
+ */
+ setANewPassword: string
+ /**
+ * E​x​p​i​r​e​d
+ */
+ expired: string
+ /**
+ * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​h​a​s​ ​e​x​p​i​r​e​d
+ */
+ requestHasExpired: string
+ /**
+ * R​e​q​u​e​s​t​ ​a​ ​n​e​w​ ​r​e​s​e​t
+ */
+ requestANewReset: string
+ /**
+ * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​i​s​ ​i​n​v​a​l​i​d
+ */
+ invalidRequestTitle: string
+ /**
+ * T​h​i​s​ ​c​o​u​l​d​ ​b​e​ ​d​u​e​ ​t​o​ ​i​t​ ​b​e​i​n​g​ ​e​x​p​i​r​e​d​,​ ​n​o​n​e​x​s​i​s​t​e​n​t​ ​o​r​ ​s​o​m​e​t​h​i​n​g​ ​e​l​s​e
+ */
+ invalidRequestMessage: string
+ /**
+ * N​e​w​ ​p​a​s​s​w​o​r​d
+ */
+ newPassword: string
+ /**
+ * I​f​ ​w​e​ ​f​i​n​d​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s​ ​i​n​ ​o​u​r​ ​s​y​s​t​e​m​s​,​ ​y​o​u​ ​w​i​l​l​ ​r​e​c​e​i​v​e​ ​a​n​ ​e​m​a​i​l​ ​w​i​t​h​ ​i​n​s​t​r​u​c​t​i​o​n​s​ ​o​n​ ​h​o​w​ ​t​o​ ​s​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​f​o​r​ ​y​o​u​r​ ​a​c​c​o​u​n​t​.
+ */
+ requestSentMessage: string
+ /**
+ * R​e​q​u​e​s​t​ ​a​ ​p​a​s​s​w​o​r​d​ ​r​e​s​e​t
+ */
+ requestAPasswordReset: string
+ /**
+ * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​w​a​s​ ​n​o​t​ ​f​o​u​n​d
+ */
+ requestNotFound: string
+ /**
+ * S​u​b​m​i​t​ ​a​ ​n​e​w​ ​r​e​s​e​t​ ​r​e​q​u​e​s​t​ ​b​e​l​o​w
+ */
+ submitANewRequestBelow: string
+ }
+}
+
+export type NamespaceAppTranslation = {
+ /**
+ * M​e​m​b​e​r​s
+ */
+ members: string
+}
+
+export type Namespaces =
+ | 'app'
+
+type DisallowNamespaces = {
+ /**
+ * reserved for 'app'-namespace\
+ * you need to use the `./app/index.ts` file instead
+ */
+ app?: "[typesafe-i18n] reserved for 'app'-namespace. You need to use the `./app/index.ts` file instead."
+}
+
+export type TranslationFunctions = {
+ /**
+ * Or
+ */
+ or: () => LocalizedString
+ /**
+ * Name
+ */
+ name: () => LocalizedString
+ /**
+ * Email address
+ */
+ emailAddress: () => LocalizedString
+ /**
+ * Password
+ */
+ password: () => LocalizedString
+ /**
+ * Page not found
+ */
+ pageNotFound: () => LocalizedString
+ /**
+ * It seems like your device does not have a internet connection, please check your connection.
+ */
+ noInternet: () => LocalizedString
+ /**
+ * Reset
+ */
+ reset: () => LocalizedString
+ /**
+ * {0} of {1}
+ */
+ of: (arg0: unknown, arg1: unknown) => LocalizedString
+ /**
+ * {0} is required
+ */
+ isRequired: (arg0: unknown) => LocalizedString
+ /**
+ * Submit
+ */
+ submit: () => LocalizedString
+ /**
+ * Success
+ */
+ success: () => LocalizedString
+ /**
+ * Try again soon
+ */
+ tryAgainSoon: () => LocalizedString
+ /**
+ * Create a new account
+ */
+ createANewAccount: () => LocalizedString
+ /**
+ * An unexpected error occured
+ */
+ unexpectedError: () => LocalizedString
+ /**
+ * Not found
+ */
+ notFound: () => LocalizedString
+ /**
+ * Documentation
+ */
+ documentation: () => LocalizedString
+ /**
+ * Terms of service
+ */
+ tos: () => LocalizedString
+ /**
+ * Privacy policy
+ */
+ privacyPolicy: () => LocalizedString
+ /**
+ * Sign into your account
+ */
+ signIntoYourAccount: () => LocalizedString
+ combobox: {
+ /**
+ * Search
+ */
+ search: () => LocalizedString
+ /**
+ * No records found
+ */
+ noRecordsFound: () => LocalizedString
+ /**
+ * Create a record by typing the name in the search bar and pressing enter
+ */
+ createRecordHelpText: () => LocalizedString
+ /**
+ * Press enter or click here to create {0}
+ */
+ createRecordButtonText: (arg0: unknown) => LocalizedString
+ }
+ signInPage: {
+ /**
+ * Sign in
+ */
+ title: () => LocalizedString
+ /**
+ * This is not my computer
+ */
+ notMyComputer: () => LocalizedString
+ /**
+ * Reset password
+ */
+ resetPassword: () => LocalizedString
+ /**
+ * Your password is updated
+ */
+ yourPasswordIsUpdated: () => LocalizedString
+ /**
+ * Sign In
+ */
+ signIn: () => LocalizedString
+ /**
+ * Your new password is applied
+ */
+ yourNewPasswordIsApplied: () => LocalizedString
+ /**
+ * Sign in below
+ */
+ signInBelow: () => LocalizedString
+ /**
+ * Your account is disabled
+ */
+ yourAccountIsDisabled: () => LocalizedString
+ /**
+ * Contact your administrator if this feels wrong
+ */
+ contactYourAdminIfDisabled: () => LocalizedString
+ /**
+ * You've reached the hidden inactivity limit
+ */
+ youHaveReachedInactivityLimit: () => LocalizedString
+ /**
+ * Feel free to sign in again
+ */
+ feelFreeToSignInAgain: () => LocalizedString
+ }
+ signUpPage: {
+ /**
+ * Sign up
+ */
+ title: () => LocalizedString
+ /**
+ * Create your new account
+ */
+ createYourNewAccount: () => LocalizedString
+ }
+ resetPasswordPage: {
+ /**
+ * Reset password
+ */
+ title: () => LocalizedString
+ /**
+ * Set new password
+ */
+ fulfillTitle: () => LocalizedString
+ /**
+ * Set a new password
+ */
+ setANewPassword: () => LocalizedString
+ /**
+ * Expired
+ */
+ expired: () => LocalizedString
+ /**
+ * Your request has expired
+ */
+ requestHasExpired: () => LocalizedString
+ /**
+ * Request a new reset
+ */
+ requestANewReset: () => LocalizedString
+ /**
+ * Your request is invalid
+ */
+ invalidRequestTitle: () => LocalizedString
+ /**
+ * This could be due to it being expired, nonexsistent or something else
+ */
+ invalidRequestMessage: () => LocalizedString
+ /**
+ * New password
+ */
+ newPassword: () => LocalizedString
+ /**
+ * If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account.
+ */
+ requestSentMessage: () => LocalizedString
+ /**
+ * Request a password reset
+ */
+ requestAPasswordReset: () => LocalizedString
+ /**
+ * Your request was not found
+ */
+ requestNotFound: () => LocalizedString
+ /**
+ * Submit a new reset request below
+ */
+ submitANewRequestBelow: () => LocalizedString
+ }
+ app: {
+ /**
+ * Members
+ */
+ members: () => LocalizedString
+ }
+}
+
+export type Formatters = {}
diff --git a/code/app/src/i18n/i18n-util.async.ts b/code/app/src/i18n/i18n-util.async.ts
new file mode 100644
index 0000000..2e6717e
--- /dev/null
+++ b/code/app/src/i18n/i18n-util.async.ts
@@ -0,0 +1,42 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { initFormatters } from './formatters'
+import type { Locales, Namespaces, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+
+const localeTranslationLoaders = {
+ en: () => import('./en'),
+ nb: () => import('./nb'),
+}
+
+const localeNamespaceLoaders = {
+ en: {
+ app: () => import('./en/app')
+ },
+ nb: {
+ app: () => import('./nb/app')
+ }
+}
+
+const updateDictionary = (locale: Locales, dictionary: Partial<Translations>): Translations =>
+ loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
+
+export const importLocaleAsync = async (locale: Locales): Promise<Translations> =>
+ (await localeTranslationLoaders[locale]()).default as unknown as Translations
+
+export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
+ updateDictionary(locale, await importLocaleAsync(locale))
+ loadFormatters(locale)
+}
+
+export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
+
+export const loadFormatters = (locale: Locales): void =>
+ void (loadedFormatters[locale] = initFormatters(locale))
+
+export const importNamespaceAsync = async<Namespace extends Namespaces>(locale: Locales, namespace: Namespace) =>
+ (await localeNamespaceLoaders[locale][namespace]()).default as unknown as Translations[Namespace]
+
+export const loadNamespaceAsync = async <Namespace extends Namespaces>(locale: Locales, namespace: Namespace): Promise<void> =>
+ void updateDictionary(locale, { [namespace]: await importNamespaceAsync(locale, namespace )})
diff --git a/code/app/src/i18n/i18n-util.sync.ts b/code/app/src/i18n/i18n-util.sync.ts
new file mode 100644
index 0000000..8144fdc
--- /dev/null
+++ b/code/app/src/i18n/i18n-util.sync.ts
@@ -0,0 +1,35 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { initFormatters } from './formatters'
+import type { Locales, Translations } from './i18n-types'
+import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+
+import en from './en'
+import nb from './nb'
+
+import en_app from './en/app'
+import nb_app from './nb/app'
+
+const localeTranslations = {
+ en: {
+ ...en,
+ app: en_app
+ },
+ nb: {
+ ...nb,
+ app: nb_app
+ },
+}
+
+export const loadLocale = (locale: Locales): void => {
+ if (loadedLocales[locale]) return
+
+ loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
+ loadFormatters(locale)
+}
+
+export const loadAllLocales = (): void => locales.forEach(loadLocale)
+
+export const loadFormatters = (locale: Locales): void =>
+ void (loadedFormatters[locale] = initFormatters(locale))
diff --git a/code/app/src/i18n/i18n-util.ts b/code/app/src/i18n/i18n-util.ts
new file mode 100644
index 0000000..5b7b6ed
--- /dev/null
+++ b/code/app/src/i18n/i18n-util.ts
@@ -0,0 +1,45 @@
+// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
+/* eslint-disable */
+
+import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
+import type { LocaleDetector } from 'typesafe-i18n/detectors'
+import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
+import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
+import { initExtendDictionary } from 'typesafe-i18n/utils'
+import type { Formatters, Locales, Namespaces, Translations, TranslationFunctions } from './i18n-types'
+
+export const baseLocale: Locales = 'en'
+
+export const locales: Locales[] = [
+ 'en',
+ 'nb'
+]
+
+export const namespaces: Namespaces[] = [
+ 'app'
+]
+
+export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
+
+
+ export const isNamespace = (namespace: string): namespace is Namespaces => namespaces.includes(namespace as Namespaces)
+
+export const loadedLocales: Record<Locales, Translations> = {} as Record<Locales, Translations>
+
+export const loadedFormatters: Record<Locales, Formatters> = {} as Record<Locales, Formatters>
+
+export const extendDictionary = initExtendDictionary<Translations>()
+
+export const i18nString = (locale: Locales): TranslateByString => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
+
+export const i18nObject = (locale: Locales): TranslationFunctions =>
+ initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
+ locale,
+ loadedLocales[locale],
+ loadedFormatters[locale]
+ )
+
+export const i18n = (): LocaleTranslationFunctions<Locales, Translations, TranslationFunctions> =>
+ initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
+
+export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn<Locales>(baseLocale, locales, ...detectors)
diff --git a/code/app/src/i18n/nb/app/index.ts b/code/app/src/i18n/nb/app/index.ts
new file mode 100644
index 0000000..6bf9ba6
--- /dev/null
+++ b/code/app/src/i18n/nb/app/index.ts
@@ -0,0 +1,7 @@
+import type { NamespaceAppTranslation } from '../../i18n-types'
+
+const nb_app: NamespaceAppTranslation = {
+ members: "Medlemmer"
+}
+
+export default nb_app
diff --git a/code/app/src/i18n/nb/index.ts b/code/app/src/i18n/nb/index.ts
new file mode 100644
index 0000000..ef67504
--- /dev/null
+++ b/code/app/src/i18n/nb/index.ts
@@ -0,0 +1,51 @@
+import type { Translation } from "../i18n-types";
+
+const nb: Translation = {
+ or: "Eller",
+ name: "Navn",
+ emailAddress: "E-postadresse",
+ password: "Passord",
+ pageNotFound: "Fant ikke siden",
+ noInternet: "Det ser ut som at du ikke tilkoblet internettet, sjekk tilkoblingen din for å fortsette",
+ reset: "Tilbakestill",
+ of: "{0} av {1}",
+ isRequired: "{0} er påkrevd",
+ submit: "Send",
+ success: "Suksess",
+ tryAgainSoon: "Prøv igjen snart",
+ createANewAccount: "Lag en ny konto",
+ unexpectedError: "En uventet feil oppstod",
+ notFound: "Ikke funnet",
+ documentation: "Dokumentasjon",
+ tos: "Vilkår",
+ privacyPolicy: "Personvernerklæring",
+ signIntoYourAccount: "Logg inn med din konto",
+ signInPage: {
+ notMyComputer: "Dette er ikke min datamaskin",
+ resetPassword: "Tilbakestill passord",
+ yourPasswordIsUpdated: "Ditt passord er oppdater",
+ signIn: "Logg inn",
+ yourNewPasswordIsApplied: "Ditt nye passord er satt",
+ signInBelow: "Logg inn nedenfor",
+ yourAccountIsDisabled: "Din konto er deaktivert",
+ contactYourAdminIfDisabled: "Ta kontakt med din administrator hvis dette føles feil",
+ youHaveReachedInactivityLimit: "Du har nådd den hemmelige inaktivitetsgrensen",
+ feelFreeToSignInAgain: "Logg gjerne inn igjen"
+ },
+ signUpPage: {
+ createYourNewAccount: "Opprett din nye konto",
+ },
+ resetPasswordPage: {
+ setANewPassword: "Skriv et nytt passord",
+ expired: "Utgått",
+ requestHasExpired: "Din forespørsel er utgått",
+ requestANewReset: "Spør om en ny tilbakestillingslenke",
+ newPassword: "Nytt passord",
+ requestSentMessage: "Hvis vi finner e-postadressen din i våre systemer, vil du få en e-post med instrukser for å sette ditt nye passord.",
+ requestAPasswordReset: "Forespør tilbakestilling av ditt passord",
+ requestNotFound: "Din forespørsel ble ikke funnet",
+ submitANewRequestBelow: "Spør om en ny tilbakestillingslenke nedenfor"
+ }
+}
+
+export default nb; \ No newline at end of file
diff --git a/code/app/src/models/base/Customer.ts b/code/app/src/models/base/Customer.ts
new file mode 100644
index 0000000..ff52fbd
--- /dev/null
+++ b/code/app/src/models/base/Customer.ts
@@ -0,0 +1,21 @@
+import type {CustomerContact} from "./CustomerContact";
+import type {User} from "./User";
+
+export type Customer = {
+ /**
+ * Guid id for customer
+ */
+ id: string,
+ /**
+ * The name of the company
+ */
+ name: string,
+ /**
+ * Responsible contact in the current tenant
+ */
+ tenantContact: User,
+ /**
+ * The customers main contact
+ */
+ mainContact: CustomerContact,
+} \ No newline at end of file
diff --git a/code/app/src/models/base/CustomerContact.ts b/code/app/src/models/base/CustomerContact.ts
new file mode 100644
index 0000000..e8abea5
--- /dev/null
+++ b/code/app/src/models/base/CustomerContact.ts
@@ -0,0 +1,8 @@
+export type CustomerContact = {
+ firstName: string,
+ lastname: string,
+ email: string,
+ phone: string,
+ workTitle: string,
+ note: string
+} \ No newline at end of file
diff --git a/code/app/src/models/base/CustomerEvent.ts b/code/app/src/models/base/CustomerEvent.ts
new file mode 100644
index 0000000..af86511
--- /dev/null
+++ b/code/app/src/models/base/CustomerEvent.ts
@@ -0,0 +1,6 @@
+export type CustomerEvent = {
+ /**
+ * A descriptive name for the occured event
+ */
+ name: string,
+} \ No newline at end of file
diff --git a/code/app/src/models/base/SessionData.ts b/code/app/src/models/base/SessionData.ts
new file mode 100644
index 0000000..015cbf3
--- /dev/null
+++ b/code/app/src/models/base/SessionData.ts
@@ -0,0 +1,5 @@
+export type SessionData = {
+ id: string,
+ username: string,
+ displayName: string,
+} \ No newline at end of file
diff --git a/code/app/src/models/base/Tenant.ts b/code/app/src/models/base/Tenant.ts
new file mode 100644
index 0000000..6307efc
--- /dev/null
+++ b/code/app/src/models/base/Tenant.ts
@@ -0,0 +1,8 @@
+import type {User} from "./User";
+
+export type Tenant = {
+ id: string,
+ name: string,
+ description: string,
+ masterUser: User,
+} \ No newline at end of file
diff --git a/code/app/src/models/base/User.ts b/code/app/src/models/base/User.ts
new file mode 100644
index 0000000..2b74d0e
--- /dev/null
+++ b/code/app/src/models/base/User.ts
@@ -0,0 +1,13 @@
+import type {UserRole} from "./UserRole";
+
+export type User = {
+ /**
+ * Guid id for user
+ */
+ id: string,
+ firstName: string,
+ lastName: string,
+ role: UserRole,
+ username: string,
+ email: string
+} \ No newline at end of file
diff --git a/code/app/src/models/base/UserRole.ts b/code/app/src/models/base/UserRole.ts
new file mode 100644
index 0000000..ec32852
--- /dev/null
+++ b/code/app/src/models/base/UserRole.ts
@@ -0,0 +1,5 @@
+export enum UserRole {
+ REGULAR = "reg",
+ ADMINISTRATOR = "adm",
+ OWNER = "own"
+} \ No newline at end of file
diff --git a/code/app/src/models/internal/FormError.ts b/code/app/src/models/internal/FormError.ts
new file mode 100644
index 0000000..f6d8978
--- /dev/null
+++ b/code/app/src/models/internal/FormError.ts
@@ -0,0 +1,24 @@
+import type { KnownProblem } from "./KnownProblem";
+
+export class FormError {
+ title: string;
+ subtitle: string;
+ constructor(title: string = "", subtitle: string = "") {
+ this.title = title;
+ this.title = subtitle;
+ }
+
+ set(title: string = "", subtitle: string = "") {
+ this.title = title;
+ this.subtitle = subtitle;
+ }
+
+ set_from_known_problem(knownProblem: KnownProblem) {
+ this.title = knownProblem.title ?? "";
+ this.subtitle = knownProblem.subtitle ?? "";
+ }
+
+ has_error() {
+ return this.title?.length > 0 || this.subtitle?.length > 0;
+ }
+} \ No newline at end of file
diff --git a/code/app/src/models/internal/IForm.ts b/code/app/src/models/internal/IForm.ts
new file mode 100644
index 0000000..c14b770
--- /dev/null
+++ b/code/app/src/models/internal/IForm.ts
@@ -0,0 +1,15 @@
+import type { FormError } from "./FormError";
+
+export interface IForm {
+ fields: Record<string, IFormField>;
+ error: FormError;
+ get_payload: Function;
+ submit_async: Function;
+ isLoading: boolean;
+ showError: boolean;
+}
+
+export interface IFormField {
+ value: any;
+ errors: Array<string>;
+}
diff --git a/code/app/src/models/internal/KnownProblem.ts b/code/app/src/models/internal/KnownProblem.ts
new file mode 100644
index 0000000..b6923d9
--- /dev/null
+++ b/code/app/src/models/internal/KnownProblem.ts
@@ -0,0 +1,10 @@
+export type KnownProblem = {
+ title: string,
+ subtitle: string,
+ errors: Record<string, string[]>,
+ traceId: string,
+}
+
+export function is_known_problem(response: Response): boolean {
+ return response.headers.has("X-IsKnownProblem");
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/Project.ts b/code/app/src/models/projects/Project.ts
new file mode 100644
index 0000000..f265e67
--- /dev/null
+++ b/code/app/src/models/projects/Project.ts
@@ -0,0 +1,13 @@
+import type { Temporal } from "temporal-polyfill"
+import type { ProjectMember } from "./ProjectMember"
+import type { ProjectStatus } from "./ProjectStatus"
+
+export type Project = {
+ id: string,
+ name: string,
+ description?: string,
+ start: Temporal.PlainDate,
+ stop?: Temporal.PlainDate,
+ members: Array<ProjectMember>,
+ status: ProjectStatus
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/ProjectLabel.ts b/code/app/src/models/projects/ProjectLabel.ts
new file mode 100644
index 0000000..59aa9d5
--- /dev/null
+++ b/code/app/src/models/projects/ProjectLabel.ts
@@ -0,0 +1,5 @@
+export type ProjectLabel = {
+ id: string,
+ name: string,
+ color: string
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/ProjectMember.ts b/code/app/src/models/projects/ProjectMember.ts
new file mode 100644
index 0000000..de348ef
--- /dev/null
+++ b/code/app/src/models/projects/ProjectMember.ts
@@ -0,0 +1,10 @@
+import type { ProjectRole } from "./ProjectRole"
+
+export type ProjectMember = {
+ id: string,
+ name: string,
+ role: ProjectRole,
+ email: string,
+ userId?: string,
+ customerId?: string
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/ProjectMeta.ts b/code/app/src/models/projects/ProjectMeta.ts
new file mode 100644
index 0000000..c583b47
--- /dev/null
+++ b/code/app/src/models/projects/ProjectMeta.ts
@@ -0,0 +1,7 @@
+import type { Temporal } from "temporal-polyfill"
+import type { User } from "../base/User"
+
+export type ProjectMeta = {
+ created: Temporal.PlainDateTime,
+ createdBy: User,
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/ProjectRole.ts b/code/app/src/models/projects/ProjectRole.ts
new file mode 100644
index 0000000..0fa2347
--- /dev/null
+++ b/code/app/src/models/projects/ProjectRole.ts
@@ -0,0 +1,7 @@
+export enum ProjectRole {
+ EXTERNAL = "ext",
+ INTERNAL = "int",
+ RESOURCE = "res",
+ MANAGER = "man",
+ OWNER = "own"
+} \ No newline at end of file
diff --git a/code/app/src/models/projects/ProjectStatus.ts b/code/app/src/models/projects/ProjectStatus.ts
new file mode 100644
index 0000000..2df4b88
--- /dev/null
+++ b/code/app/src/models/projects/ProjectStatus.ts
@@ -0,0 +1,5 @@
+export enum ProjectStatus {
+ ACTIVE = "act",
+ EXPIRED = "exp",
+ IDLE = "idl"
+} \ No newline at end of file
diff --git a/code/app/src/models/work/WorkCategory.ts b/code/app/src/models/work/WorkCategory.ts
new file mode 100644
index 0000000..7dd85d5
--- /dev/null
+++ b/code/app/src/models/work/WorkCategory.ts
@@ -0,0 +1,5 @@
+export type WorkCategory = {
+ id: string,
+ name: string,
+ color: string
+}
diff --git a/code/app/src/models/work/WorkEntry.ts b/code/app/src/models/work/WorkEntry.ts
new file mode 100644
index 0000000..2108b88
--- /dev/null
+++ b/code/app/src/models/work/WorkEntry.ts
@@ -0,0 +1,13 @@
+import type { WorkLabel } from "./WorkLabel";
+import type { WorkCategory } from "./WorkCategory";
+import type { Project } from "../projects/Project";
+
+export type WorkEntry = {
+ id: string,
+ start: string,
+ stop: string,
+ description: string,
+ labels?: Array<WorkLabel>,
+ category?: WorkCategory,
+ project?: Project
+}
diff --git a/code/app/src/models/work/WorkEntryQueryResponse.ts b/code/app/src/models/work/WorkEntryQueryResponse.ts
new file mode 100644
index 0000000..a6974f1
--- /dev/null
+++ b/code/app/src/models/work/WorkEntryQueryResponse.ts
@@ -0,0 +1,27 @@
+import type { WorkCategory } from "./WorkCategory";
+import type { WorkLabel } from "./WorkLabel";
+import type { Temporal } from "temporal-polyfill";
+
+export interface WorkEntryQueryResponse {
+ duration: WorkEntryQueryDuration,
+ categories?: Array<WorkCategory>,
+ labels?: Array<WorkLabel>,
+ dateRange?: WorkEntryQueryDateRange,
+ specificDate?: Temporal.PlainDateTime
+ page: number,
+ pageSize: number
+}
+
+export interface WorkEntryQueryDateRange {
+ from: Temporal.PlainDateTime,
+ to: Temporal.PlainDateTime
+}
+
+export enum WorkEntryQueryDuration {
+ TODAY = 0,
+ THIS_WEEK = 1,
+ THIS_MONTH = 2,
+ THIS_YEAR = 3,
+ SPECIFIC_DATE = 4,
+ DATE_RANGE = 5,
+}
diff --git a/code/app/src/models/work/WorkLabel.ts b/code/app/src/models/work/WorkLabel.ts
new file mode 100644
index 0000000..f7e2795
--- /dev/null
+++ b/code/app/src/models/work/WorkLabel.ts
@@ -0,0 +1,5 @@
+export interface WorkLabel {
+ id?: string,
+ name?: string,
+ color?: string
+}
diff --git a/code/app/src/models/work/WorkQuery.ts b/code/app/src/models/work/WorkQuery.ts
new file mode 100644
index 0000000..93b0aa4
--- /dev/null
+++ b/code/app/src/models/work/WorkQuery.ts
@@ -0,0 +1,17 @@
+import type {WorkEntry} from "./WorkEntry";
+
+export interface IWorkQuery {
+ results: Array<WorkEntry>,
+ page: number,
+ pageSize: number,
+ totalRecords: number,
+ totalPageCount: number,
+}
+
+export class WorkQuery implements IWorkQuery {
+ results: WorkEntry[];
+ page: number;
+ pageSize: number;
+ totalRecords: number;
+ totalPageCount: number;
+}
diff --git a/code/app/src/routes/(api)/delete-cookie/+server.ts b/code/app/src/routes/(api)/delete-cookie/+server.ts
new file mode 100644
index 0000000..ee5e1dc
--- /dev/null
+++ b/code/app/src/routes/(api)/delete-cookie/+server.ts
@@ -0,0 +1,8 @@
+import type { RequestHandler } from './$types';
+
+export const GET: RequestHandler = async ({ cookies, url }) => {
+ const cookieToDelete = url.searchParams.get("key");
+ if (!cookieToDelete || cookies.get(cookieToDelete) === undefined) return;
+ cookies.delete(cookieToDelete)
+ return new Response();
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(app)/+layout.svelte b/code/app/src/routes/(main)/(app)/+layout.svelte
new file mode 100644
index 0000000..09dbb47
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/+layout.svelte
@@ -0,0 +1,379 @@
+<script lang="ts">
+ import {
+ ChevronUpDownIcon,
+ MagnifyingGlassIcon,
+ Bars3CenterLeftIcon,
+ XMarkIcon,
+ HomeIcon,
+ MegaphoneIcon,
+ FolderOpenIcon,
+ QueueListIcon,
+ CalendarIcon,
+ } from "$components/icons";
+ import { AccountService } from "$services/account-service";
+ import {
+ Dialog,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuItems,
+ Transition,
+ TransitionChild,
+ TransitionRoot,
+ } from "@rgossiaux/svelte-headlessui";
+ import { DialogPanel } from "@developermuch/dev-svelte-headlessui";
+ import { Input, Notification } from "$components";
+ import { goto } from "$app/navigation";
+ import { page } from "$app/stores";
+ import { onMount } from "svelte";
+ import { fgs, sgs } from "$utilities/global-state";
+
+ const accountService = AccountService.resolve();
+ const session = {
+ profile: {
+ username: "Brukernavn",
+ displayName: "epost@adresse.no",
+ },
+ };
+
+ let sidebarOpen = false;
+ let sidebarSearchValue: string | undefined;
+ let showEmailValidatedNotif = false;
+
+ onMount(() => {
+ showEmailValidatedNotif =
+ fgs("showEmailValidatedAlertWhenLoggedIn") === "true";
+ if (showEmailValidatedNotif)
+ sgs("showEmailValidatedAlertWhenLoggedIn", false);
+ });
+
+ function sign_out() {
+ accountService.end_session(() => goto("/sign-in"));
+ }
+ const navigationItems = [
+ {
+ href: "/home",
+ name: "Home",
+ icon: HomeIcon,
+ },
+ {
+ href: "/projects",
+ name: "Projects",
+ icon: CalendarIcon,
+ },
+ {
+ href: "/tickets",
+ name: "Tickets",
+ icon: MegaphoneIcon,
+ },
+ {
+ href: "/todo",
+ name: "Todo",
+ icon: QueueListIcon,
+ },
+ {
+ href: "/wiki",
+ name: "Wiki",
+ icon: FolderOpenIcon,
+ },
+ ];
+</script>
+
+{#if showEmailValidatedNotif}
+ <Notification
+ title="Email successfully validated"
+ subtitle="Because of this, you now have gained access to more functionality"
+ show={true}
+ />
+{/if}
+
+<div class="min-h-full">
+ <!-- Mobile sidebar -->
+ <TransitionRoot show={sidebarOpen}>
+ <Dialog
+ as="div"
+ class="relative z-40 lg:hidden"
+ on:close={() => (sidebarOpen = false)}
+ >
+ <TransitionChild
+ as="div"
+ enter="transition-opacity ease-linear duration-300"
+ enterFrom="opacity-0"
+ enterTo="opacity-100"
+ leave="transition-opacity ease-linear duration-300"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div class="fixed inset-0 bg-gray-600 bg-opacity-75" />
+ </TransitionChild>
+
+ <div class="fixed inset-0 z-40 flex">
+ <TransitionChild
+ as="div"
+ enter="transition ease-in-out duration-300 transform"
+ enterFrom="-translate-x-full"
+ enterTo="translate-x-0"
+ leave="transition ease-in-out duration-300 transform"
+ leaveFrom="translate-x-0"
+ leaveTo="-translate-x-full"
+ >
+ <DialogPanel
+ class="relative flex w-full max-w-xs flex-1 flex-col bg-white pt-5 pb-4"
+ >
+ <TransitionChild
+ as="div"
+ enter="ease-in-out duration-300"
+ enterFrom="opacity-0"
+ enterTo="opacity-100"
+ leave="ease-in-out duration-300"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div class="absolute top-0 right-0 -mr-12 pt-2">
+ <button
+ type="button"
+ class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
+ on:click={() => (sidebarOpen = false)}
+ >
+ <span class="sr-only">Close sidebar</span>
+ <XMarkIcon class="text-white" aria-hidden="true" />
+ </button>
+ </div>
+ </TransitionChild>
+ <div class="mt-5 h-0 flex-1 overflow-y-auto">
+ <nav class="px-2">
+ <div class="space-y-1">
+ {#each navigationItems as item}
+ {@const current = $page.url.pathname.startsWith(item.href)}
+ <a
+ href={item.href}
+ aria-current={current ? "page" : undefined}
+ class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md
+ {current
+ ? 'bg-gray-100 text-gray-900'
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}"
+ >
+ <svelte:component
+ this={item.icon}
+ class="mr-3 flex-shrink-0 h-6 w-6 {current
+ ? 'text-gray-500'
+ : 'text-gray-400 group-hover:text-gray-500'}"
+ aria-hidden="true"
+ />
+ {item.name}
+ </a>
+ {/each}
+ </div>
+ </nav>
+ </div>
+ </DialogPanel>
+ </TransitionChild>
+ <div class="w-14 flex-shrink-0" aria-hidden="true">
+ <!-- Dummy element to force sidebar to shrink to fit close icon -->
+ </div>
+ </div>
+ </Dialog>
+ </TransitionRoot>
+
+ <!-- Static sidebar for desktop -->
+ <div
+ class="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col lg:border-r lg:border-gray-200 lg:bg-gray-100 lg:pb-4"
+ >
+ <div class="flex h-0 flex-1 p-3 flex-col overflow-y-auto">
+ <!-- User account dropdown -->
+ <Menu class="relative inline-block text-left">
+ <MenuButton
+ class="group w-full rounded-md bg-gray-100 px-3.5 py-2 text-left text-sm font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-gray-100"
+ >
+ <span class="flex w-full items-center justify-between">
+ <span class="flex min-w-0 items-center justify-between space-x-3">
+ <span class="flex min-w-0 flex-1 flex-col">
+ <span class="truncate text-sm font-medium text-gray-900">
+ {session.profile.username}
+ </span>
+ <span class="truncate text-sm text-gray-500"
+ >{session.profile.displayName}</span
+ >
+ </span>
+ </span>
+ <ChevronUpDownIcon
+ class="flex-shrink-0 text-gray-400 group-hover:text-gray-500"
+ aria-hidden="true"
+ />
+ </span>
+ </MenuButton>
+ <Transition
+ leave="transition ease-in duration-75"
+ enter="transition ease-out duration-100"
+ enterFrom="transform opacity-0 scale-95"
+ enterTo="transform opacity-100 scale-100"
+ leaveFrom="transform opacity-100 scale-100"
+ leaveTo="transform opacity-0 scale-95"
+ as="div"
+ >
+ <MenuItems
+ class="absolute right-0 left-0 z-10 mt-1 origin-top divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
+ >
+ <div class="py-1">
+ <MenuItem>
+ <a
+ href="/profile"
+ class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"
+ >
+ View profile
+ </a>
+ </MenuItem>
+ <MenuItem>
+ <a
+ href="/settings"
+ class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"
+ >
+ Settings
+ </a>
+ </MenuItem>
+ </div>
+ <div class="py-1">
+ <MenuItem>
+ <span
+ on:click={() => sign_out()}
+ class="text-gray-700 block px-4 py-2 text-sm hover:bg-red-200 hover:text-red-900 cursor-pointer"
+ >
+ Sign out
+ </span>
+ </MenuItem>
+ </div>
+ </MenuItems>
+ </Transition>
+ </Menu>
+ <!-- Sidebar Search -->
+ <div class="mt-3 hidden">
+ <label for="search" class="sr-only">Search</label>
+ <div class="relative mt-1 rounded-md shadow-sm">
+ <Input
+ type="search"
+ name="search"
+ icon={MagnifyingGlassIcon}
+ placeholder="Search"
+ bind:value={sidebarSearchValue}
+ />
+ </div>
+ </div>
+ <!-- Navigation -->
+ <nav class="mt-5">
+ <div class="space-y-1">
+ {#each navigationItems as item}
+ {@const current = $page.url.pathname.startsWith(item.href)}
+ <a
+ href={item.href}
+ aria-current={current ? "page" : undefined}
+ class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md
+ {current
+ ? 'bg-gray-200 text-gray-900'
+ : 'text-gray-700 hover:text-gray-900 hover:bg-gray-50'}"
+ >
+ <svelte:component
+ this={item.icon}
+ class="mr-3 flex-shrink-0 h-6 w-6 {current
+ ? 'text-gray-500'
+ : 'text-gray-400 group-hover:text-gray-500'}"
+ aria-hidden="true"
+ />
+ {item.name}
+ </a>
+ {/each}
+ </div>
+ </nav>
+ </div>
+ </div>
+
+ <!-- Main column -->
+ <div class="flex flex-col lg:pl-64">
+ <!-- Search header -->
+ <div
+ class="sticky top-0 z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:hidden"
+ >
+ <button
+ type="button"
+ class="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden"
+ on:click={() => (sidebarOpen = true)}
+ >
+ <span class="sr-only">Open sidebar</span>
+ <Bars3CenterLeftIcon aria-hidden="true" />
+ </button>
+ <div class="flex flex-1 justify-between px-4 sm:px-6 lg:px-8">
+ <div class="flex flex-1">
+ <form class="flex w-full md:ml-0" action="#" method="GET">
+ <label for="search-field" class="sr-only">Search</label>
+ <div
+ class="relative w-full text-gray-400 focus-within:text-gray-600"
+ >
+ <Input
+ bind:value={sidebarSearchValue}
+ icon={MagnifyingGlassIcon}
+ id="search-field"
+ name="search-field"
+ placeholder="Search"
+ type="search"
+ />
+ </div>
+ </form>
+ </div>
+ <div class="flex items-center">
+ <!-- Profile dropdown -->
+ <Menu as="div" class="relative ml-3">
+ <div>
+ <MenuButton
+ class="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2"
+ >
+ <span class="sr-only">Open user menu</span>
+ </MenuButton>
+ </div>
+ <Transition
+ enterFrom="transform opacity-0 scale-95"
+ enterTo="transform opacity-100 scale-100"
+ leaveFrom="transform opacity-100 scale-100"
+ leaveTo="transform opacity-0 scale-95"
+ as="div"
+ >
+ <MenuItems
+ class="absolute right-0 z-10 mt-2 w-48 origin-top-right divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
+ >
+ <div class="py-1">
+ <MenuItem>
+ <a
+ href="/profile"
+ class="text-gray-700 block px-4 py-2 text-sm"
+ >
+ View profile
+ </a>
+ </MenuItem>
+ <MenuItem>
+ <a
+ href="/settings"
+ class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100"
+ >
+ Settings
+ </a>
+ </MenuItem>
+ <div class="py-1">
+ <MenuItem>
+ <span
+ on:click={() => sign_out()}
+ class="text-gray-700 block px-4 py-2 text-sm"
+ >
+ Sign out
+ </span>
+ </MenuItem>
+ </div>
+ </div>
+ </MenuItems>
+ </Transition>
+ </Menu>
+ </div>
+ </div>
+ </div>
+ <main class="flex-1 p-3">
+ <slot />
+ </main>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(app)/home/+page.svelte b/code/app/src/routes/(main)/(app)/home/+page.svelte
new file mode 100644
index 0000000..247ee47
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/home/+page.svelte
@@ -0,0 +1 @@
+<h1>Welcome Home</h1> \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(app)/org/+page.svelte b/code/app/src/routes/(main)/(app)/org/+page.svelte
new file mode 100644
index 0000000..429ec25
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/org/+page.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<h1>$ORGNAME</h1>
diff --git a/code/app/src/routes/(main)/(app)/profile/+page.svelte b/code/app/src/routes/(main)/(app)/profile/+page.svelte
new file mode 100644
index 0000000..7c6eb3e
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/profile/+page.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<h1>Hi, Ivar</h1>
diff --git a/code/app/src/routes/(main)/(app)/projects/+page.svelte b/code/app/src/routes/(main)/(app)/projects/+page.svelte
new file mode 100644
index 0000000..2585331
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/projects/+page.svelte
@@ -0,0 +1,118 @@
+<script lang="ts">
+ import { Button, ProjectStatusBadge, Input } from "$components";
+ import type { Project } from "$models/projects/Project";
+ import { createTable, Subscribe, Render } from "svelte-headless-table";
+ import { addSortBy, addTableFilter } from "svelte-headless-table/plugins";
+ import { writable, type Writable } from "svelte/store";
+ import { ChevronDownIcon, ChevronUpIcon, ChevronUpDownIcon, MagnifyingGlassIcon, FunnelIcon } from "$components/icons";
+ import LL from "$i18n/i18n-svelte";
+ import { goto } from "$app/navigation";
+
+ const projects: Writable<Array<Project>> = writable([]);
+
+ function on_open_project(event) {
+ if (event.code && (event.code !== "Enter" || event.code !== "Space")) return;
+ const name = event.target.innerText;
+ const projectId = $projects.find((p) => p.name === name).id;
+ goto("/projects/" + projectId);
+ }
+
+ const table = createTable(projects, {
+ sort: addSortBy(),
+ filter: addTableFilter(),
+ });
+
+ const columns = table.createColumns([
+ table.column({ header: $LL.name(), accessor: "name" }),
+ table.column({ header: "Status", accessor: "status" }),
+ table.column({ header: "Start", accessor: "start" }),
+ table.column({ header: "Description", accessor: "description", plugins: { sort: { disable: true } } }),
+ ]);
+
+ const { headerRows, rows, tableAttrs, tableBodyAttrs, pluginStates } = table.createViewModel(columns);
+ const { filterValue } = pluginStates.filter;
+</script>
+
+<div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-xl font-semibold text-gray-900">Projects</h1>
+ <p class="mt-2 text-sm text-gray-700">A list of all the projects in your organsation.</p>
+ </div>
+ <div class="mt-4 sm:mt-0 sm:ml-16 inline-flex gap-1 sm:flex-none">
+ <Input icon={MagnifyingGlassIcon} placeholder="Search" bind:value={$filterValue} />
+ <Button text="Create project" href="/projects/create" />
+ </div>
+</div>
+<div class="-mx-2 mt-6 rounded-md shadow overflow-auto max-h-[80vh] sm:-mx-6 md:mx-0">
+ <table {...$tableAttrs} class="min-w-full divide-y divide-gray-300">
+ <thead class="bg-gray-50">
+ {#each $headerRows as headerRow (headerRow.id)}
+ <Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
+ <tr {...rowAttrs} class="shadow-sm">
+ {#each headerRow.cells as cell (cell.id)}
+ <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
+ <th
+ {...attrs}
+ scope="col"
+ class="sticky top-0 bg-gray-50 bg-opacity-100 whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >
+ <div class="group inline-flex">
+ <Render of={cell.render()} />
+ <span
+ on:click={props.sort.toggle}
+ on:keypress={props.sort.toggle}
+ class="{props.sort.disabled
+ ? 'bg-gray-200 text-gray-900 group-hover:bg-gray-300'
+ : 'invisible text-gray-400 group-hover:visible group-focus:visible'}
+ {props.sort.disabled ? '' : 'cursor-pointer'}
+ ml-2 flex-none rounded"
+ >
+ {#if props.sort.order === "asc"}
+ <ChevronUpIcon />
+ {:else if props.sort.order === "desc"}
+ <ChevronDownIcon />
+ {:else if !props.sort.disabled}
+ <ChevronUpDownIcon />
+ {/if}
+ </span>
+ {#if cell.id === "status"}
+ <span
+ class="invisible text-gray-400 cursor-pointer group-hover:visible group-focus:visible ml-2 flex-none rounded"
+ >
+ <FunnelIcon />
+ </span>
+ {/if}
+ </div>
+ </th>
+ </Subscribe>
+ {/each}
+ </tr>
+ </Subscribe>
+ {/each}
+ </thead>
+ <tbody {...$tableBodyAttrs} class="divide-y divide-gray-200 bg-white">
+ {#each $rows as row (row.id)}
+ <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
+ <tr {...rowAttrs}>
+ {#each row.cells as cell (cell.id)}
+ {@const materialisedCell = cell.render()}
+ <Subscribe attrs={cell.attrs()} let:attrs>
+ <td {...attrs} class="whitespace-nowrap px-2 py-2 text-sm">
+ {#if cell.id === "name"}
+ <span class="link" title="Open project" on:click={on_open_project} on:keypress={on_open_project}>
+ <Render of={materialisedCell} />
+ </span>
+ {:else if cell.id === "status"}
+ <ProjectStatusBadge status={materialisedCell.toString()} />
+ {:else}
+ <Render of={materialisedCell} />
+ {/if}
+ </td>
+ </Subscribe>
+ {/each}
+ </tr>
+ </Subscribe>
+ {/each}
+ </tbody>
+ </table>
+</div>
diff --git a/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte b/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte
new file mode 100644
index 0000000..ca474e2
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+ import { page } from "$app/stores";
+</script>
+
+<h1>{$page.params.id}</h1>
diff --git a/code/app/src/routes/(main)/(app)/projects/create/+page.svelte b/code/app/src/routes/(main)/(app)/projects/create/+page.svelte
new file mode 100644
index 0000000..d710edc
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/projects/create/+page.svelte
@@ -0,0 +1,59 @@
+<script lang="ts">
+ import { Input, TextArea, Combobox, Button } from "$components";
+ import type { ProjectMember } from "$models/projects/ProjectMember";
+ import LL from "$i18n/i18n-svelte";
+
+ let members = [];
+ const formData = {
+ name: {
+ value: "",
+ errors: [],
+ },
+ description: {
+ value: "",
+ errors: [],
+ },
+ start: {
+ value: "",
+ errors: [],
+ },
+ stop: {
+ value: "",
+ errors: [],
+ },
+ members: {
+ value: [] as Array<ProjectMember>,
+ errors: [],
+ },
+ };
+
+ const formError = {
+ title: "",
+ subtitle: "",
+ };
+
+ async function submit_form_async() {
+ alert("Submitted");
+ }
+</script>
+
+<h1>Create a new project</h1>
+<form on:submit|preventDefault={submit_form_async} class="max-w-md flex flex-col gap-2">
+ <Input label="Name" bind:value={formData.name.value} errors={formData.name.errors} required />
+ <TextArea label="Description" bind:value={formData.description.value} errors={formData.description.errors} />
+ <section class="grid grid-flow-row sm:grid-flow-col gap-2">
+ <Input type="date" label="Start" bind:value={formData.start.value} errors={formData.start.errors} />
+ <Input type="date" label="Stop" bind:value={formData.stop.value} errors={formData.stop.errors} />
+ </section>
+ <Combobox options={members} label={$LL.app.members()}>
+ <svelte:fragment slot="no-records">
+ <h1>No members found</h1>
+ {#if !members?.length}
+ <p>
+ <a href="/users/create" class="link">Click here</a> to create your first user
+ </p>
+ {/if}
+ </svelte:fragment>
+ </Combobox>
+ <Button text={$LL.submit()} />
+</form>
diff --git a/code/app/src/routes/(main)/(app)/settings/+page.svelte b/code/app/src/routes/(main)/(app)/settings/+page.svelte
new file mode 100644
index 0000000..8e99661
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/settings/+page.svelte
@@ -0,0 +1,205 @@
+<script lang="ts">
+ import {Input, Button, Switch} from "$components";
+</script>
+
+<div class="relative mx-auto max-w-4xl md:px-8 xl:px-0">
+ <div class="pt-10 pb-16">
+ <div class="px-4 sm:px-6 md:px-0">
+ <h1 class="text-3xl font-bold tracking-tight text-gray-900">Settings</h1>
+ </div>
+ <div class="px-4 sm:px-6 md:px-0">
+ <div class="py-6">
+ <!-- Tabs -->
+ <div class="lg:hidden">
+ <label for="selected-tab" class="sr-only">Select a tab</label>
+ <select
+ id="selected-tab"
+ name="selected-tab"
+ class="mt-1 block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-purple-500 focus:outline-none focus:ring-purple-500 sm:text-sm"
+ >
+ <option selected>General</option>
+
+ <option>Password</option>
+
+ <option>Notifications</option>
+
+>
+
+ <option>Billing</option>
+
+ <option>Team Members</option>
+ </select>
+ </div>
+ <div class="hidden lg:block">
+ <div class="border-b border-gray-200">
+ <nav class="-mb-px flex space-x-8">
+ <!-- Current: "border-purple-500 text-purple-600", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
+ <a href="#"
+ class="border-purple-500 text-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >General</a
+ >
+
+ <a
+ href="#"
+ class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >Password</a
+ >
+
+ <a
+ href="#"
+ class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >Notifications</a
+ >
+
+ <a
+ href="#"
+ class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >Plan</a
+ >
+
+ <a
+ href="#"
+ class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >Billing</a
+ >
+
+ <a
+ href="#"
+ class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
+ >Team Members</a
+ >
+ </nav>
+ </div>
+ </div>
+
+ <!-- Description list with inline editing -->
+ <div class="mt-10 divide-y divide-gray-200">
+ <div class="space-y-1">
+ <h3 class="text-lg font-medium leading-6 text-gray-900">Profile</h3>
+ <p class="max-w-2xl text-sm text-gray-500">
+ This information will be displayed publicly so be careful what you share.
+ </p>
+ </div>
+ <div class="mt-6">
+ <dl class="divide-y divide-gray-200">
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
+ <dt class="text-sm font-medium text-gray-500">Name</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">Chelsea Hagon</span>
+ <span class="ml-4 flex-shrink-0">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ </span>
+ </dd>
+ </div>
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5">
+ <dt class="text-sm font-medium text-gray-500">Photo</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">
+ <img
+ class="h-8 w-8 rounded-full"
+ src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
+ alt=""
+ />
+ </span>
+ <span class="ml-4 flex flex-shrink-0 items-start space-x-4">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ <span class="text-gray-300" aria-hidden="true">|</span>
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Remove</button
+ >
+ </span>
+ </dd>
+ </div>
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5">
+ <dt class="text-sm font-medium text-gray-500">Email</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">chelsea.hagon@example.com</span>
+ <span class="ml-4 flex-shrink-0">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ </span>
+ </dd>
+ </div>
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:border-b sm:border-gray-200 sm:py-5">
+ <dt class="text-sm font-medium text-gray-500">Job title</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">Human Resources Manager</span>
+ <span class="ml-4 flex-shrink-0">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ </span>
+ </dd>
+ </div>
+ </dl>
+ </div>
+ </div>
+
+ <div class="mt-10 divide-y divide-gray-200">
+ <div class="space-y-1">
+ <h3 class="text-lg font-medium leading-6 text-gray-900">Account</h3>
+ <p class="max-w-2xl text-sm text-gray-500">Manage how information is displayed on your
+ account.</p>
+ </div>
+ <div class="mt-6">
+ <dl class="divide-y divide-gray-200">
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
+ <dt class="text-sm font-medium text-gray-500">Language</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">English</span>
+ <span class="ml-4 flex-shrink-0">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ </span>
+ </dd>
+ </div>
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5">
+ <dt class="text-sm font-medium text-gray-500">Date format</dt>
+ <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0">
+ <span class="flex-grow">DD-MM-YYYY</span>
+ <span class="ml-4 flex flex-shrink-0 items-start space-x-4">
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Update</button
+ >
+ <span class="text-gray-300" aria-hidden="true">|</span>
+ <button
+ type="button"
+ class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
+ >Remove</button
+ >
+ </span>
+ </dd>
+ </div>
+ <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5">
+ <dt class="text-sm font-medium text-gray-500" id="timezone-option-label">Automatic
+ timezone
+ </dt>
+ <Switch/>
+ </div>
+ </dl>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(app)/tickets/+page.svelte b/code/app/src/routes/(main)/(app)/tickets/+page.svelte
new file mode 100644
index 0000000..2a4792b
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/tickets/+page.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<h1>Tickets</h1>
diff --git a/code/app/src/routes/(main)/(app)/todo/+page.svelte b/code/app/src/routes/(main)/(app)/todo/+page.svelte
new file mode 100644
index 0000000..e29f263
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/todo/+page.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<h1>Todo</h1>
diff --git a/code/app/src/routes/(main)/(app)/wiki/+page.svelte b/code/app/src/routes/(main)/(app)/wiki/+page.svelte
new file mode 100644
index 0000000..1762d43
--- /dev/null
+++ b/code/app/src/routes/(main)/(app)/wiki/+page.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<h1>Wiki</h1>
diff --git a/code/app/src/routes/(main)/(public)/+layout.svelte b/code/app/src/routes/(main)/(public)/+layout.svelte
new file mode 100644
index 0000000..6da653c
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/+layout.svelte
@@ -0,0 +1,18 @@
+<script>
+ import { LocaleSwitcher } from "$components";
+ import LL from "$i18n/i18n-svelte";
+</script>
+
+<LocaleSwitcher tabindex={-1} />
+<slot />
+<footer class="grid sm:gap-5 grid-flow-row sm:justify-center px-2 sm:grid-flow-col">
+ <a href="https://greatoffice.life/privacy" class="link">
+ {$LL.privacyPolicy()}
+ </a>
+ <a href="https://greatoffice.life/terms" class="link">
+ {$LL.tos()}
+ </a>
+ <a href="https://greatoffice.life/docs" class="link">
+ {$LL.documentation()}
+ </a>
+</footer>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.svelte b/code/app/src/routes/(main)/(public)/portal/+page.svelte
new file mode 100644
index 0000000..b363e4b
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import type { PageData } from "./$types";
+ import type { PortalMessage } from "$configuration";
+ import { goto } from "$app/navigation";
+ import { sgs } from "$utilities/global-state";
+
+ export let data: PageData;
+
+ onMount(async () => {
+ switch (data.message as PortalMessage) {
+ case "emailValidated": {
+ sgs("showEmailValidatedAlertWhenLoggedIn", true);
+ await goto("/home");
+ break;
+ }
+ default: {
+ await goto("/home");
+ }
+ }
+ });
+</script>
+
+<div class="p-3">
+ <h1>Warping...</h1>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.ts b/code/app/src/routes/(main)/(public)/portal/+page.ts
new file mode 100644
index 0000000..72338cb
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.ts
@@ -0,0 +1,9 @@
+import type { PortalMessage } from '$configuration';
+import { redirect } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ url }) => {
+ const message = url.searchParams.get("msg") as PortalMessage;
+ if (!message) throw redirect(302, "/");
+ return { message };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
new file mode 100644
index 0000000..a45ccdd
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
@@ -0,0 +1,81 @@
+<script lang="ts">
+ import { Alert, Input, Button } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import { FormError } from "$models/internal/FormError";
+ import { PasswordResetService } from "$services/password-reset-service";
+
+ const formData = {
+ email: {
+ value: "",
+ errors: [],
+ },
+ };
+
+ const formError = new FormError();
+ const passwordResetService = PasswordResetService.resolve();
+
+ let loading = false;
+ let showSuccessAlert = false;
+ let showErrorAlert = false;
+
+ async function submit_form_async() {
+ formError.set();
+ showSuccessAlert = false;
+ showErrorAlert = false;
+ loading = true;
+ const response = await passwordResetService.create_request_async(formData.email.value);
+ loading = false;
+ if (response.isCreated) {
+ showSuccessAlert = true;
+ } else if (response.knownProblem) {
+ formError.set_from_known_problem(response.knownProblem);
+ for (const error of Object.entries(response.knownProblem.errors)) {
+ if (error[0] === "email") {
+ let errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.email.errors = errors;
+ }
+ }
+ } else {
+ formError.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ showErrorAlert = formError.has_error() && !showSuccessAlert;
+ }
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.resetPasswordPage.requestAPasswordReset()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ <form class="space-y-6" on:submit|preventDefault={submit_form_async}>
+ {#if showErrorAlert}
+ <Alert title={formError.title} message={formError.subtitle} type="error" />
+ {:else if showSuccessAlert}
+ <Alert type="success" title={$LL.success()} message={$LL.resetPasswordPage.requestSentMessage()} />
+ {/if}
+ <Input
+ id="email"
+ name="email"
+ type="email"
+ autocomplete="email"
+ errors={formData.email.errors}
+ bind:value={formData.email.value}
+ required
+ label={$LL.emailAddress()}
+ />
+ <Button text={$LL.submit()} type="submit" {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/+page.ts
new file mode 100644
index 0000000..c0859e0
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.resetPasswordPage.title(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts
new file mode 100644
index 0000000..22fa29d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts
@@ -0,0 +1,11 @@
+import { is_guid } from "$utilities/validators";
+import { redirect } from "@sveltejs/kit";
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async ({ params }) => {
+ const resetRequestId = params.id ?? "";
+ if (!is_guid(resetRequestId)) throw redirect(302, "/reset-password");
+ return {
+ resetRequestId,
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
new file mode 100644
index 0000000..27a1af5
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
@@ -0,0 +1,82 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import LL from "$i18n/i18n-svelte";
+ import { Alert, Input, Button } from "$components";
+ import type { PageServerData } from "./$types";
+ import { goto } from "$app/navigation";
+ import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+ import { PasswordResetService } from "$services/password-reset-service";
+
+ export let data: PageServerData;
+ const passwordResetService = PasswordResetService.resolve();
+
+ const formData = {
+ newPassword: {
+ value: "",
+ errors: [],
+ },
+ };
+
+ let finishedPreliminaryLoading = false;
+ let loading = false;
+ let canSubmit = true;
+ let requestIsInvalid = false;
+
+ async function submitFormAsync() {
+ if (!canSubmit) return;
+ loading = true;
+ const request = await passwordResetService.fulfill_request_async(data.resetRequestId, formData.newPassword.value);
+ if (request.isFulfilled) {
+ goto("/sign-in?" + signInPageMessageQueryKey + "=" + SignInPageMessage.AFTER_PASSWORD_RESET);
+ } else if (request.knownProblem) {
+ }
+ loading = false;
+ }
+
+ onMount(async () => {
+ const response = await passwordResetService.request_is_valid_async(data.resetRequestId);
+ requestIsInvalid = !response.isValid;
+ finishedPreliminaryLoading = true;
+ });
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ {#if finishedPreliminaryLoading}
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.resetPasswordPage.setANewPassword()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ <form class="space-y-6" on:submit|preventDefault={submitFormAsync}>
+ {#if requestIsInvalid}
+ <Alert
+ title={$LL.resetPasswordPage.invalidRequestTitle()}
+ message={$LL.resetPasswordPage.invalidRequestMessage()}
+ />
+ {/if}
+ <Input
+ id="password"
+ name="password"
+ type="password"
+ autocomplete="new-password"
+ required
+ bind:value={formData.newPassword.value}
+ label={$LL.resetPasswordPage.newPassword()}
+ />
+ <Button text={$LL.submit()} type="submit" {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+ {:else}
+ <p>Checking your request...</p>
+ {/if}
+</div>
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts
new file mode 100644
index 0000000..3252b7a
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.resetPasswordPage.fulfillTitle(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
new file mode 100644
index 0000000..66d4575
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
@@ -0,0 +1,155 @@
+<script lang="ts">
+ import { goto } from "$app/navigation";
+ import { Button, Checkbox, Input, Alert } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import pwKey from "$actions/pwKey";
+ import { onMount } from "svelte";
+ import { signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage } from ".";
+ import { AccountService } from "$services/account-service";
+ import type { LoginPayload } from "$services/abstractions/IAccountService";
+ import { FormError } from "$models/internal/FormError";
+ import type { IForm } from "$models/internal/IForm";
+
+ let messageType: SignInPageMessage | undefined = undefined;
+
+ const accountService = AccountService.resolve();
+ const form = {
+ fields: {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ persist: {
+ value: false,
+ errors: [],
+ },
+ },
+ error: new FormError(),
+ isLoading: false,
+ showError: false,
+ get_payload(): LoginPayload {
+ return {
+ password: form.fields.password.value,
+ username: form.fields.username.value,
+ persist: !form.fields.persist.value,
+ };
+ },
+ async submit_async() {
+ console.log("sadf");
+ form.error.set();
+ form.showError = form.error.has_error();
+ form.isLoading = true;
+ const loginResponse = await accountService.login_async(form.get_payload());
+ if (loginResponse.isLoggedIn) {
+ await goto("/home");
+ } else if (loginResponse.knownProblem) {
+ form.error.set_from_known_problem(loginResponse.knownProblem);
+ } else {
+ form.error.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ form.isLoading = false;
+ form.showError = form.error.has_error();
+ },
+ } as IForm;
+
+ onMount(() => {
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.get(signInPageMessageQueryKey)) {
+ messageType = queryParams.get(signInPageMessageQueryKey) as SignInPageMessage;
+ queryParams.delete(signInPageMessageQueryKey);
+ window.history.replaceState(null, "", window.location.origin + window.location.pathname);
+ }
+ });
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ {#if messageType}
+ <div class="sm:max-w-md sm:mx-auto sm:w-full">
+ {#if messageType === "after-password-reset"}
+ <Alert
+ title={$LL.signInPage.yourNewPasswordIsApplied()}
+ _pwKey={signInPageTestKeys.afterPasswordResetAlert}
+ message={$LL.signInPage.signInBelow()}
+ closeable
+ />
+ {:else if messageType === "user-disabled"}
+ <Alert
+ title={$LL.signInPage.yourAccountIsDisabled()}
+ _pwKey={signInPageTestKeys.userDisabledAlert}
+ message={$LL.signInPage.contactYourAdminIfDisabled()}
+ closeable
+ />
+ {:else if messageType === "user-inactivity"}
+ <Alert
+ title={$LL.signInPage.youHaveReachedInactivityLimit()}
+ _pwKey={signInPageTestKeys.userInactivityAlert}
+ message={$LL.signInPage.feelFreeToSignInAgain()}
+ closeable
+ />
+ {/if}
+ </div>
+ {/if}
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.signInPage.signIn()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-up" use:pwKey={signInPageTestKeys.signUpAnchor} class="link">
+ {$LL.createANewAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ {#if form.showError}
+ <Alert title={form.error.title} message={form.error.subtitle} type="error" _pwKey={signInPageTestKeys.formErrorAlert} />
+ {/if}
+ <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={() => form.submit_async()}>
+ <Input
+ id="username"
+ _pwKey={signInPageTestKeys.usernameInput}
+ name="username"
+ type="email"
+ label={$LL.emailAddress()}
+ required
+ errors={form.fields.username.errors}
+ bind:value={form.fields.username.value}
+ />
+
+ <Input
+ id="password"
+ name="password"
+ type="password"
+ label={$LL.password()}
+ _pwKey={signInPageTestKeys.passwordInput}
+ autocomplete="current-password"
+ required
+ errors={form.fields.password.errors}
+ bind:value={form.fields.password.value}
+ />
+
+ <div class="flex items-center justify-between">
+ <Checkbox
+ id="remember-me"
+ _pwKey={signInPageTestKeys.rememberMeCheckbox}
+ name="remember-me"
+ bind:checked={form.fields.persist.value}
+ label={$LL.signInPage.notMyComputer()}
+ />
+ <div class="text-sm">
+ <a href="/reset-password" class="link" use:pwKey={signInPageTestKeys.resetPasswordAnchor}>
+ {$LL.signInPage.resetPassword()}
+ </a>
+ </div>
+ </div>
+
+ <Button text={$LL.submit()} fullWidth type="submit" loading={form.isLoading} />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.ts b/code/app/src/routes/(main)/(public)/sign-in/+page.ts
new file mode 100644
index 0000000..bebc459
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.signInPage.title(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.spec.js b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js
new file mode 100644
index 0000000..9d0122d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js
@@ -0,0 +1,12 @@
+import { test, expect } from "@playwright/test";
+import { signInPageTestKeys } from "./index.js";
+import { get_test_context } from "$configuration";
+import { get_pw_key_selector } from "$utilities/testing-helpers";
+
+const context = get_test_context();
+
+test("form loads", async ({ page }) => {
+ page.goto("/sign-in");
+ const form = page.locator(get_pw_key_selector(signInPageTestKeys.signInForm));
+ expect(form.isVisible()).toBeTruthy();
+});
diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.ts b/code/app/src/routes/(main)/(public)/sign-in/index.ts
new file mode 100644
index 0000000..c1a1929
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/index.ts
@@ -0,0 +1,20 @@
+export enum SignInPageMessage {
+ AFTER_PASSWORD_RESET = "after-password-reset",
+ USER_INACTIVITY = "user-inactivity",
+ USER_DISABLED = "user-disabled",
+ LOGGED_OUT = "logged-out"
+}
+
+export const signInPageMessageQueryKey = "m";
+export const signInPageTestKeys = {
+ passwordInput: "password-input",
+ usernameInput: "username-input",
+ rememberMeCheckbox: "remember-me-checkbox",
+ signInForm: "sign-in-form",
+ userInactivityAlert: SignInPageMessage.USER_INACTIVITY + "-alert",
+ userDisabledAlert: SignInPageMessage.USER_DISABLED + "-alert",
+ afterPasswordResetAlert: SignInPageMessage.AFTER_PASSWORD_RESET + "-alert",
+ formErrorAlert: "form-error-alert",
+ resetPasswordAnchor: "reset-password-anchor",
+ signUpAnchor: "sign-up-anchor",
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.svelte b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte
new file mode 100644
index 0000000..470ac5d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte
@@ -0,0 +1,106 @@
+<script lang="ts">
+ import { goto } from "$app/navigation";
+ import { Button, Input, Alert } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import { FormError } from "$models/internal/FormError";
+ import type { CreateAccountPayload } from "$services/abstractions/IAccountService";
+ import { AccountService } from "$services/account-service";
+
+ const formData = {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ as_payload(): CreateAccountPayload {
+ return {
+ username: formData.username.value,
+ password: formData.password.value,
+ };
+ },
+ };
+
+ const formError = new FormError();
+ const accountService = new AccountService();
+
+ let loading = false;
+ let showErrorAlert = false;
+
+ async function submit_form_async() {
+ loading = true;
+ showErrorAlert = false;
+ formError.set();
+ formData.username.errors = [];
+ formData.password.errors = [];
+ const response = await accountService.create_account_async(formData.as_payload());
+ if (response.isCreated) {
+ await goto("/home");
+ } else if (response.knownProblem) {
+ formError.set_from_known_problem(response.knownProblem);
+ for (const error of Object.entries(response.knownProblem.errors)) {
+ if (error[0] === "username") {
+ const errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.username.errors = errors;
+ }
+ if (error[0] === "password") {
+ const errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.password.errors = errors;
+ }
+ }
+ } else {
+ formError.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ loading = false;
+ showErrorAlert = formError.has_error();
+ }
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.signUpPage.createYourNewAccount()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ {#if showErrorAlert}
+ <Alert title={formError.title} message={formError.subtitle} type="error" class="mb-2" />
+ {/if}
+ <form class="space-y-6" on:submit|preventDefault={submit_form_async}>
+ <Input
+ label={$LL.emailAddress()}
+ id="email"
+ name="email"
+ autocomplete="email"
+ required
+ type="email"
+ bind:value={formData.username.value}
+ errors={formData.username.errors}
+ />
+
+ <Input
+ label={$LL.password()}
+ id="password"
+ name="password"
+ required
+ type="password"
+ bind:value={formData.password.value}
+ errors={formData.password.errors}
+ />
+ <Button type="submit" text={$LL.submit()} {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.ts b/code/app/src/routes/(main)/(public)/sign-up/+page.ts
new file mode 100644
index 0000000..8c86f55
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-up/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.signUpPage.title(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/+layout.server.ts b/code/app/src/routes/(main)/+layout.server.ts
new file mode 100644
index 0000000..25043aa
--- /dev/null
+++ b/code/app/src/routes/(main)/+layout.server.ts
@@ -0,0 +1,45 @@
+import { api_base, CookieNames } from "$configuration";
+import { cached_result_async, CacheKeys } from "$utilities/cache";
+import { log_debug, log_error } from "$utilities/logger";
+import { get_md5_hash } from "$utilities/crypto-helpers";
+import { error, redirect } from "@sveltejs/kit";
+import type { LayoutServerLoad } from "./$types";
+
+export const load: LayoutServerLoad = async ({ route, cookies, locals, fetch }) => {
+ const isBaseRoute = route.id === "/(main)";
+ const isPortalRoute = route.id === "/(main)/(public)/portal";
+ const isPublicRoute = (isBaseRoute || (route.id?.startsWith("/(main)/(public)") ?? false)) ?? true;
+ const sessionCookieValue = cookies.get(CookieNames.session);
+ let sessionIsValid = false;
+ if ((sessionCookieValue?.length > 0 ?? false)) {
+ const sessionHash = get_md5_hash(sessionCookieValue);
+ sessionIsValid = (await cached_result_async<Response>(sessionHash + "_" + CacheKeys.isAuthenticated, 120, () => fetch(api_base("_/is-authenticated"), {
+ headers: {
+ Cookie: CookieNames.session + "=" + sessionCookieValue,
+ },
+ }).catch((e) => {
+ log_error(e);
+ throw error(503, {
+ message: "We are experiencing a service disruption! Have patience while we resolve the issue.",
+ });
+ }))).ok;
+ }
+
+ log_debug("Base Layout loaded", {
+ sessionIsValid,
+ isPublicRoute,
+ isBaseRoute,
+ isPortalRoute,
+ routeId: route.id,
+ });
+
+ if (sessionIsValid && isPublicRoute && !isPortalRoute) {
+ throw redirect(302, "/home");
+ } else if (!isPortalRoute && (isBaseRoute || !sessionIsValid && !isPublicRoute)) {
+ throw redirect(302, "/sign-in");
+ }
+
+ return {
+ locale: locals.locale,
+ };
+};
diff --git a/code/app/src/routes/(main)/+layout.svelte b/code/app/src/routes/(main)/+layout.svelte
new file mode 100644
index 0000000..7662d6a
--- /dev/null
+++ b/code/app/src/routes/(main)/+layout.svelte
@@ -0,0 +1,31 @@
+<script lang="ts">
+ import "../../app.pcss";
+ import { setLocale } from "$i18n/i18n-svelte";
+ import { ExclamationTriangleIcon } from "$components/icons";
+ import { page } from "$app/stores";
+ import type { LayoutData } from "./$types";
+
+ let online = true;
+ export let data: LayoutData;
+ setLocale(data.locale);
+</script>
+
+<svelte:window bind:online />
+<svelte:head>
+ <title>{$page.data.title ? $page.data.title + " - Greatoffice" : "Greatoffice"}</title>
+</svelte:head>
+
+{#if !online}
+ <div class="bg-yellow-50 relative z-50 p-4">
+ <div class="flex">
+ <div class="flex-shrink-0">
+ <ExclamationTriangleIcon class="bg-yellow-50 text-yellow-500" />
+ </div>
+ <div class="ml-3">
+ <p class="text-sm text-yellow-700">You seem to be offline, please check your internet connection.</p>
+ </div>
+ </div>
+ </div>
+{/if}
+
+<slot />
diff --git a/code/app/src/routes/(main)/+layout.ts b/code/app/src/routes/(main)/+layout.ts
new file mode 100644
index 0000000..3893260
--- /dev/null
+++ b/code/app/src/routes/(main)/+layout.ts
@@ -0,0 +1,10 @@
+import type { LayoutLoad } from "./$types";
+import type { Locales } from "$i18n/i18n-types";
+import { loadLocaleAsync } from "$i18n/i18n-util.async";
+import { setLocale } from "$i18n/i18n-svelte";
+
+export const load: LayoutLoad<{ locale: Locales }> = async ({ data: { locale } }) => {
+ await loadLocaleAsync(locale);
+ setLocale(locale);
+ return { locale };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/+page.svelte b/code/app/src/routes/(main)/+page.svelte
new file mode 100644
index 0000000..e507a19
--- /dev/null
+++ b/code/app/src/routes/(main)/+page.svelte
@@ -0,0 +1 @@
+<p class="text-bold p-1">Hold on...</p>
diff --git a/code/app/src/routes/book/+layout.svelte b/code/app/src/routes/book/+layout.svelte
new file mode 100644
index 0000000..385d0a6
--- /dev/null
+++ b/code/app/src/routes/book/+layout.svelte
@@ -0,0 +1,46 @@
+<script>
+ import { page } from "$app/stores";
+ import "../../app.pcss";
+</script>
+
+<div id="wrapper">
+ <nav>
+ <a href="/book/alerts" class="link" class:active={$page.url.pathname.startsWith("/book/alerts")}>Alerts</a>
+ <a href="/book/buttons" class="link" class:active={$page.url.pathname.startsWith("/book/buttons")}>Buttons</a>
+ <a href="/book/toggles" class="link" class:active={$page.url.pathname.startsWith("/book/toggles")}>Toggles</a>
+ <a href="/book/inputs" class="link" class:active={$page.url.pathname.startsWith("/book/inputs")}>Inputs</a>
+ <a href="/book/badges" class="link" class:active={$page.url.pathname.startsWith("/book/badges")}>Badges</a>
+ <a href="/book/notifications" class="link" class:active={$page.url.pathname.startsWith("/book/notifications")}>Notifications</a>
+ </nav>
+ <main>
+ <slot />
+ </main>
+</div>
+
+<style global lang="postcss">
+ #wrapper {
+ display: flex;
+ flex-direction: row;
+ }
+ nav {
+ min-width: 120px;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ position: sticky;
+ position: -webkit-sticky;
+ top: 0;
+ height: fit-content;
+ }
+ main {
+ width: 100%;
+ padding: 10px;
+ }
+ section {
+ margin-bottom: 25px;
+
+ h2 {
+ margin-bottom: 5px;
+ }
+ }
+</style>
diff --git a/code/app/src/routes/book/+layout.ts b/code/app/src/routes/book/+layout.ts
new file mode 100644
index 0000000..d297dfd
--- /dev/null
+++ b/code/app/src/routes/book/+layout.ts
@@ -0,0 +1,3 @@
+export const ssr = import.meta.env.DEV;
+export const csr = import.meta.env.DEV;
+export const prerender = import.meta.env.DEV; \ No newline at end of file
diff --git a/code/app/src/routes/book/+page.svelte b/code/app/src/routes/book/+page.svelte
new file mode 100644
index 0000000..635b3c2
--- /dev/null
+++ b/code/app/src/routes/book/+page.svelte
@@ -0,0 +1 @@
+<p>A showcase of greatoffices components</p>
diff --git a/code/app/src/routes/book/alerts/+page.svelte b/code/app/src/routes/book/alerts/+page.svelte
new file mode 100644
index 0000000..ed4c92b
--- /dev/null
+++ b/code/app/src/routes/book/alerts/+page.svelte
@@ -0,0 +1,70 @@
+<script>
+ import Alert from "$components/alert.svelte";
+</script>
+
+<section>
+ <h2>Info</h2>
+ <Alert type="info" message="This is message" title="This is title"/>
+</section>
+<section>
+ <h2>Warning</h2>
+ <Alert type="warning" message="This is message" title="This is title"/>
+</section>
+<section>
+ <h2>Error</h2>
+ <Alert type="error" message="This is message" title="This is title"/>
+</section>
+<section>
+ <h2>Success</h2>
+ <Alert type="success" message="This is message" title="This is title"/>
+</section>
+<section>
+ <h2>Actions</h2>
+ <Alert
+ type="info"
+ message="This is message"
+ title="This is title"
+ closeable
+ actions={[
+ {
+ id: "confirm",
+ text: "Yes!",
+ },
+ {
+ id: "cancel",
+ text: "No!",
+ color: "red",
+ },
+ ]}
+ />
+</section>
+<section>
+ <h2>Right link</h2>
+ <Alert
+ on:rightLinkCliked={() => alert("Right link clicked")}
+ rightLinkText="Link or action"
+ title="Go here"
+ message="Hehe"
+ type="error"
+ />
+</section>
+<section>
+ <h2>List</h2>
+ <Alert
+ title="This is title"
+ listItems={["Message 1", "Message 2"]}
+ type="error"
+ message="This is bad dude"
+ closeable
+ closeableCooldown="60"
+ id="alert-1"
+ on:actrepeat={() => {
+ alert("Repeat requested");
+ }}
+ actions={[{ id: "repeat", text: "Try again" }]}
+ />
+</section>
+<section>
+ <h2>Closeable</h2>
+ <Alert message="This is message" closeable type="info"/>
+</section>
diff --git a/code/app/src/routes/book/badges/+page.svelte b/code/app/src/routes/book/badges/+page.svelte
new file mode 100644
index 0000000..50ae61e
--- /dev/null
+++ b/code/app/src/routes/book/badges/+page.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+ import Badge from "$components/badge.svelte";
+</script>
+
+<section>
+ <h2>Variants</h2>
+ <Badge text="default"/>
+ <Badge type="blue" text="blue"/>
+ <Badge type="green" text="green"/>
+ <Badge type="red" text="red"/>
+ <Badge type="tame" text="tame"/>
+ <Badge type="yellow" text="yellow"/>
+ <Badge size="large" text="large"/>
+ <Badge text="with dot" withDot type="blue"/>
+ <Badge text="removable" removable id="badge-1" on:remove={(e) => alert("removed " + e.detail.id)}/>
+ <Badge text="with dot" size="large" withDot type="blue"/>
+ <Badge text="removable" removable size="large" id="badge-2" uppercase
+ on:remove={(e) => alert("removed " + e.detail.id)}/>
+</section>
diff --git a/code/app/src/routes/book/buttons/+page.svelte b/code/app/src/routes/book/buttons/+page.svelte
new file mode 100644
index 0000000..6668a64
--- /dev/null
+++ b/code/app/src/routes/book/buttons/+page.svelte
@@ -0,0 +1,23 @@
+<script>
+ import Button from "$components/button.svelte";
+</script>
+
+<section>
+ <h2>Primary</h2>
+ <Button kind="primary" text="Small" size="sm"/>
+ <Button kind="primary" text="Medium/Default"/>
+ <Button kind="primary" text="Large" size="lg"/>
+ <Button kind="primary" text="Extra large" size="xl"/>
+</section>
+<section>
+ <h2>Secondary</h2>
+ <Button kind="secondary" text="Click me!"/>
+</section>
+<section>
+ <h2>White</h2>
+ <Button kind="white" text="Click me!"/>
+</section>
+<section>
+ <h2>Loading</h2>
+ <Button kind="primary" loading={true} text="Wait"/>
+</section>
diff --git a/code/app/src/routes/book/inputs/+page.svelte b/code/app/src/routes/book/inputs/+page.svelte
new file mode 100644
index 0000000..433607b
--- /dev/null
+++ b/code/app/src/routes/book/inputs/+page.svelte
@@ -0,0 +1,75 @@
+<script lang="ts">
+ import {TextArea, Input, Combobox} from "$components";
+ import {DatabaseIcon} from "$components/icons";
+
+ let value;
+ let i = 0;
+ let options = [];
+ let tempOptions = [];
+ while (i < 101) {
+ tempOptions.push({
+ id: crypto.randomUUID(),
+ name: "Option " + i,
+ });
+ options = tempOptions;
+ i++;
+ }
+
+ async function add({name}) {
+ const copy = options;
+ copy.push({
+ id: crypto.randomUUID(),
+ name: name,
+ });
+ options = copy;
+ }
+</script>
+
+<section>
+ <h2>Combobox</h2>
+ <Combobox {options} label="Wiii" multiple createable on_create_async={add}/>
+</section>
+
+<section>
+ <h2>Default</h2>
+ <Input label="Input me" placeholder="Hello" bind:value/>
+</section>
+
+<section>
+ <h2>With icon</h2>
+ <Input label="Input me" placeholder="Hello" icon={DatabaseIcon} bind:value/>
+</section>
+
+<section>
+ <h2>With corner hint</h2>
+ <Input label="Input me ->" placeholder="Hello" cornerHint="Hint hint" bind:value/>
+</section>
+
+<section>
+ <h2>Disabled</h2>
+ <Input label="No" placeholder="Sorry" disabled bind:value/>
+</section>
+
+<section>
+ <h2>Errored</h2>
+ <Input label="No" placeholder="Sorry" errorText="That's not right" bind:value icon={DatabaseIcon}/>
+</section>
+
+<section>
+ <h2>Many errors</h2>
+ <Input label="No" placeholder="Sorry" errors={["That's not right", "Call help!", "Get it together"]} bind:value
+ icon={DatabaseIcon}/>
+</section>
+
+<section>
+ <h2>Help</h2>
+ <Input label="Go ahead" placeholder="Write here" helpText="Write above" bind:value/>
+</section>
+<section>
+ <h2>Addon</h2>
+ <Input label="Go ahead" placeholder="Write here" bind:value helpText="Write above" addon="To the right"/>
+</section>
+<section>
+ <h2>Textarea</h2>
+ <TextArea bind:value label="Hi"/>
+</section>
diff --git a/code/app/src/routes/book/notifications/+page.svelte b/code/app/src/routes/book/notifications/+page.svelte
new file mode 100644
index 0000000..1a6144d
--- /dev/null
+++ b/code/app/src/routes/book/notifications/+page.svelte
@@ -0,0 +1,50 @@
+<script lang="ts">
+ import { Notification } from "$components";
+ import type { NotificationType } from "$components/notification.svelte";
+
+ let type = "info" as NotificationType;
+ let nonClosable = false;
+ let title = "Title";
+ let subtitle = "Subtitle";
+ let hideAfterSeconds = -1;
+ let timeout;
+
+ function open(newtype: NotificationType) {
+ console.log(newtype);
+ type = newtype;
+ }
+</script>
+
+<section style="display: flex;flex-direction: column; max-width:200px;gap:5px">
+ <h2>Type:</h2>
+ <select
+ on:change={(e) => {
+ //@ts-ignore
+ open(e.target.selectedOptions[0].value);
+ }}
+ >
+ <option value="info">info</option>
+ <option value="warning">warning</option>
+ <option value="error">error</option>
+ <option value="success">success</option>
+ <option value="subtle">subtle</option>
+ </select>
+ <label for="nonClosable">
+ <input type="checkbox" id="nonClosable" bind:checked={nonClosable} />
+ nonClosable
+ </label>
+ <input type="text" bind:value={title} />
+ <input type="text" bind:value={subtitle} />
+ <input type="number" bind:value={timeout} placeholder="hideAfterSeconds" />
+ <small class="text-sm justify-end">
+ <span class="link" on:click={() => (hideAfterSeconds = timeout ?? -1)}>Apply</span>
+ <span
+ class="link"
+ on:click={() => {
+ hideAfterSeconds = -1;
+ timeout = 0;
+ }}>Reset</span
+ >
+ </small>
+ <Notification {title} {subtitle} show={true} {type} {nonClosable} {hideAfterSeconds} />
+</section>
diff --git a/code/app/src/routes/book/toggles/+page.svelte b/code/app/src/routes/book/toggles/+page.svelte
new file mode 100644
index 0000000..cb0adec
--- /dev/null
+++ b/code/app/src/routes/book/toggles/+page.svelte
@@ -0,0 +1,27 @@
+<script>
+ import Switch from "$components/switch.svelte";
+</script>
+
+<section>
+ <h2>Default</h2>
+ <Switch />
+</section>
+<section>
+ <h2>Short</h2>
+ <Switch type="short" />
+</section>
+<section>
+ <h2>Icon</h2>
+ <Switch type="icon" />
+</section>
+<section>
+ <h2>Label / Description</h2>
+ <div class="max-w-md">
+ <Switch label="Label" description="Some text" />
+ </div>
+</section>
+
+<section>
+ <h2>Label / Description (right aligned)</h2>
+ <Switch label="Label" description="Some text" rightAlignedLabelDescription />
+</section>
diff --git a/code/app/src/services/abstractions/IAccountService.ts b/code/app/src/services/abstractions/IAccountService.ts
new file mode 100644
index 0000000..d3d48b0
--- /dev/null
+++ b/code/app/src/services/abstractions/IAccountService.ts
@@ -0,0 +1,54 @@
+import type { KnownProblem } from "$models/internal/KnownProblem";
+import type { Writable } from "svelte/store";
+
+export interface IAccountService {
+ session: Writable<Session>,
+ login_async(payload: LoginPayload): Promise<LoginResponse>,
+ logout_async(): Promise<void>,
+ end_session_async(callback?: Function): Promise<void>,
+ create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse>,
+ delete_current_async(): Promise<DeleteAccountResponse>,
+ update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse>,
+}
+
+export type Session = {
+ username: string,
+ displayName: string,
+ id: string,
+ _lastUpdated: number
+}
+
+export type LoginPayload = {
+ username: string,
+ password: string,
+ persist: boolean
+}
+
+export type LoginResponse = {
+ isLoggedIn: boolean,
+ knownProblem?: KnownProblem
+}
+
+export type CreateAccountPayload = {
+ username: string,
+ password: string,
+}
+
+export type CreateAccountResponse = {
+ isCreated: boolean,
+ knownProblem?: KnownProblem
+}
+
+export type DeleteAccountResponse = {
+ isDeleted: boolean
+}
+
+export type UpdateAccountPayload = {
+ username: string,
+ password: string
+}
+
+export type UpdateAccountResponse = {
+ isUpdated: boolean,
+ knownProblem?: KnownProblem
+} \ No newline at end of file
diff --git a/code/app/src/services/abstractions/IApiTokenService.ts b/code/app/src/services/abstractions/IApiTokenService.ts
new file mode 100644
index 0000000..fdf82eb
--- /dev/null
+++ b/code/app/src/services/abstractions/IApiTokenService.ts
@@ -0,0 +1,34 @@
+import type { Temporal } from "temporal-polyfill"
+
+export interface IApiTokenService {
+ create_token_async(payload: CreateTokenPayload): Promise<CreateTokenResponse>,
+ delete_token_async(payload: DeleteTokenPayload): Promise<DeleteTokenResponse>,
+ get_tokens_async(query: TokenQuery): Promise<GetTokensResponse>
+}
+export type GetTokensResponse = {
+ results: Array<GetTokensTokenModel>
+};
+export type GetTokensTokenModel = {
+ id: string,
+ name: string,
+ permissions: string[]
+}
+export type TokenQuery = {
+ includeStale: boolean
+};
+export type DeleteTokenResponse = {
+ isDeleted: boolean
+};
+export type DeleteTokenPayload = {
+ id: string
+};
+export type CreateTokenResponse = {
+ isCreated: boolean
+};
+export type CreateTokenPayload = {
+ expiryDate: Temporal.PlainDateTime,
+ allowRead: boolean,
+ allowCreate: boolean,
+ allowUpdate: boolean,
+ allowDelete: boolean
+}; \ No newline at end of file
diff --git a/code/app/src/services/abstractions/IPasswordResetService.ts b/code/app/src/services/abstractions/IPasswordResetService.ts
new file mode 100644
index 0000000..59d2bc6
--- /dev/null
+++ b/code/app/src/services/abstractions/IPasswordResetService.ts
@@ -0,0 +1,21 @@
+import type { KnownProblem } from "$models/internal/KnownProblem"
+
+export interface IPasswordResetService {
+ create_request_async(email: string): Promise<CreateRequestResponse>,
+ fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse>,
+ request_is_valid_async(id: string): Promise<RequestIsValidResponse>
+}
+
+export type RequestIsValidResponse = {
+ isValid: boolean
+}
+
+export type FulfillRequestResponse = {
+ isFulfilled: boolean,
+ knownProblem?: KnownProblem
+}
+
+export type CreateRequestResponse = {
+ isCreated: boolean,
+ knownProblem?: KnownProblem
+} \ No newline at end of file
diff --git a/code/app/src/services/abstractions/ISettingsService.ts b/code/app/src/services/abstractions/ISettingsService.ts
new file mode 100644
index 0000000..366e337
--- /dev/null
+++ b/code/app/src/services/abstractions/ISettingsService.ts
@@ -0,0 +1,3 @@
+export interface ISettingsService {
+ get_user_settings(): Promise<void>,
+} \ No newline at end of file
diff --git a/code/app/src/services/account-service.ts b/code/app/src/services/account-service.ts
new file mode 100644
index 0000000..b2bb375
--- /dev/null
+++ b/code/app/src/services/account-service.ts
@@ -0,0 +1,124 @@
+import { http_delete_async, http_get_async, http_post_async } from "$utilities/_fetch";
+import { browser } from "$app/environment";
+import { api_base, CookieNames, StorageKeys } from "$configuration";
+import { is_known_problem } from "$models/internal/KnownProblem";
+import { log_debug } from "$utilities/logger";
+import { StoreType, create_writable_persistent } from "$utilities/persistent-store";
+import { get } from "svelte/store";
+import type { Writable } from "svelte/store";
+import { Temporal } from "temporal-polyfill";
+import type {
+ CreateAccountPayload,
+ CreateAccountResponse,
+ DeleteAccountResponse,
+ IAccountService,
+ LoginPayload,
+ LoginResponse,
+ Session,
+ UpdateAccountPayload,
+ UpdateAccountResponse,
+} from "./abstractions/IAccountService";
+
+export class AccountService implements IAccountService {
+ session: Writable<Session> | undefined;
+ private sessionCooldown = 3600;
+
+ constructor() {
+ if (browser) {
+ this.session = create_writable_persistent({
+ name: StorageKeys.session,
+ initialState: {} as Session,
+ options: {
+ store: StoreType.LOCAL,
+ },
+ });
+ this.refresh_session();
+ } else {
+ this.session = undefined;
+ }
+ }
+
+ static resolve(): IAccountService {
+ return new AccountService();
+ }
+
+ async refresh_session(forceRefresh: boolean = false): Promise<void> {
+ if (!this.session) return;
+ const currentValue = get(this.session);
+ const currentEpoch = Temporal.Now.instant().epochSeconds;
+ if (!forceRefresh && ((currentValue?._lastUpdated ?? 0) + this.sessionCooldown) > currentEpoch) {
+ log_debug("Session is not stale yet", {
+ currentEpoch,
+ staleEpoch: currentValue?._lastUpdated + this.sessionCooldown,
+ });
+ return;
+ }
+ const sessionResponse = await http_get_async(api_base("_/session-data"));
+ if (sessionResponse.ok) {
+ this.session.set(await sessionResponse.json());
+ } else {
+ this.session.set(null);
+ }
+ }
+
+ async end_session_async(callback: Function = undefined): Promise<void> {
+ if (!this.session) return;
+ await this.logout_async();
+ this.session.set(null);
+ if (callback && typeof callback === "function") callback();
+ }
+
+ async login_async(payload: LoginPayload): Promise<LoginResponse> {
+ const response = await http_post_async(api_base("_/account/login"), payload);
+ if (response.ok) return { isLoggedIn: true };
+ if (is_known_problem(response)) return {
+ isLoggedIn: false,
+ knownProblem: await response.json(),
+ };
+ return {
+ isLoggedIn: false,
+ };
+ }
+
+ async logout_async(): Promise<void> {
+ const response = await http_get_async(api_base("_/account/logout"));
+ if (!response.ok) {
+ const deleteCookieResponse = await fetch("/delete-cookie?key=" + CookieNames.session);
+ if (!deleteCookieResponse.ok) {
+ throw new Error("Could neither logout nor delete session cookie.");
+ }
+ }
+ return;
+ }
+
+ async create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse> {
+ const response = await http_post_async(api_base("_/account/create"), payload);
+ if (response.ok) return { isCreated: true };
+ if (is_known_problem(response)) return {
+ isCreated: false,
+ knownProblem: await response.json(),
+ };
+ return {
+ isCreated: false,
+ };
+ }
+
+ async delete_current_async(): Promise<DeleteAccountResponse> {
+ const response = await http_delete_async(api_base("_/account/delete"));
+ return {
+ isDeleted: response.ok,
+ };
+ }
+
+ async update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse> {
+ const response = await http_post_async(api_base("_/account/update"), payload);
+ if (response.ok) return { isUpdated: true };
+ if (is_known_problem(response)) return {
+ isUpdated: false,
+ knownProblem: await response.json(),
+ };
+ return {
+ isUpdated: false,
+ };
+ }
+} \ No newline at end of file
diff --git a/code/app/src/services/api-tokens-service.ts b/code/app/src/services/api-tokens-service.ts
new file mode 100644
index 0000000..fb8b126
--- /dev/null
+++ b/code/app/src/services/api-tokens-service.ts
@@ -0,0 +1,22 @@
+import { api_base } from "$configuration";
+import { http_delete_async, http_get_async, http_post_async } from "$utilities/_fetch";
+import type { CreateTokenPayload, CreateTokenResponse, DeleteTokenPayload, DeleteTokenResponse, GetTokensResponse, IApiTokenService, TokenQuery } from "./abstractions/IApiTokenService";
+
+export class ApiTokenService implements IApiTokenService {
+ constructor() { }
+ static resolve() {
+ return new ApiTokenService();
+ }
+ async create_token_async(payload: CreateTokenPayload): Promise<CreateTokenResponse> {
+ const response = await http_post_async(api_base("v1/api-tokens/create"), payload);
+ return;
+ };
+ async delete_token_async(payload: DeleteTokenPayload): Promise<DeleteTokenResponse> {
+ const response = await http_delete_async(api_base("v1/api-tokens/delete"), payload);
+ return;
+ };
+ async get_tokens_async(query: TokenQuery): Promise<GetTokensResponse> {
+ const response = await http_get_async(api_base("v1/api-tokens"));
+ return;
+ };
+} \ No newline at end of file
diff --git a/code/app/src/services/password-reset-service.ts b/code/app/src/services/password-reset-service.ts
new file mode 100644
index 0000000..4a174fa
--- /dev/null
+++ b/code/app/src/services/password-reset-service.ts
@@ -0,0 +1,48 @@
+import { http_get_async, http_post_async } from "$utilities/_fetch";
+import { api_base } from "$configuration";
+import { is_known_problem } from "$models/internal/KnownProblem";
+import type {
+ CreateRequestResponse,
+ FulfillRequestResponse,
+ IPasswordResetService,
+ RequestIsValidResponse,
+} from "./abstractions/IPasswordResetService";
+
+export class PasswordResetService implements IPasswordResetService {
+ static resolve(): IPasswordResetService {
+ return new PasswordResetService();
+ }
+ async create_request_async(email: string): Promise<CreateRequestResponse> {
+ const response = await http_post_async(api_base("_/password-reset-request/create"), { email });
+ if (response.ok) return { isCreated: true };
+ if (is_known_problem(response)) return {
+ isCreated: false,
+ knownProblem: await response.json(),
+ };
+
+ return {
+ isCreated: false,
+ };
+ }
+
+ async fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse> {
+ const response = await http_post_async(api_base("_/password-reset-request/fulfill"), { id: id, newPassword });
+ if (response.ok) return { isFulfilled: true };
+ if (is_known_problem(response)) return {
+ isFulfilled: false,
+ knownProblem: await response.json(),
+ };
+
+ return {
+ isFulfilled: false,
+ };
+ }
+
+ async request_is_valid_async(id: string): Promise<RequestIsValidResponse> {
+ const response = await http_get_async(api_base("_/password-reset-request/is-valid?id=" + id));
+ const responseBody = await response.json() as { isValid: boolean };
+ return {
+ isValid: responseBody.isValid,
+ };
+ }
+} \ No newline at end of file
diff --git a/code/app/src/services/settings-service.ts b/code/app/src/services/settings-service.ts
new file mode 100644
index 0000000..a0a77d4
--- /dev/null
+++ b/code/app/src/services/settings-service.ts
@@ -0,0 +1,10 @@
+import type { ISettingsService } from "./abstractions/ISettingsService";
+
+export class SettingService implements ISettingsService {
+ static resolve(): ISettingsService {
+ return new SettingService();
+ }
+ get_user_settings(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+} \ No newline at end of file
diff --git a/code/app/src/utilities/_fetch.ts b/code/app/src/utilities/_fetch.ts
new file mode 100644
index 0000000..992c7f5
--- /dev/null
+++ b/code/app/src/utilities/_fetch.ts
@@ -0,0 +1,94 @@
+import { Temporal } from "temporal-polyfill";
+import { redirect } from "@sveltejs/kit";
+import { browser } from "$app/environment";
+import { goto } from "$app/navigation";
+import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+import { log_error } from "$utilities/logger";
+import { AccountService } from "$services/account-service";
+
+export async function http_post_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> {
+ const init = make_request_init("post", body, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401");
+ return response;
+}
+
+export async function http_get_async(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> {
+ const init = make_request_init("get", undefined, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401");
+ return response;
+}
+
+export async function http_delete_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> {
+ const init = make_request_init("delete", body, abort_signal);
+ const response = await internal_fetch_async({ url, init, timeout });
+ if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401");
+ return response;
+}
+
+async function internal_fetch_async(request: InternalFetchRequest): Promise<Response> {
+ if (!request.init) throw new Error("request.init is required");
+ const fetch_request = new Request(request.url, request.init);
+ let response: any;
+
+ try {
+ if (request.timeout && request.timeout > 500) {
+ response = await Promise.race([
+ fetch(fetch_request),
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout)),
+ ]);
+ } else {
+ response = await fetch(fetch_request);
+ }
+ } catch (error: any) {
+ log_error(error);
+ if (error.message === "Timeout") {
+ console.error("Request timed out");
+ } else if (error.message === "Network request failed") {
+ console.error("No internet connection");
+ } else {
+ throw error;
+ }
+ }
+
+ return response;
+}
+
+async function redirect_if_401_async(response: Response): Promise<boolean> {
+ if (response.status === 401) {
+ const redirectUrl = `/sign-in?${signInPageMessageQueryKey}=${SignInPageMessage.LOGGED_OUT}`;
+ await AccountService.resolve().end_session_async();
+ if (browser) {
+ await goto(redirectUrl);
+ } else {
+ throw redirect(307, redirectUrl);
+ }
+ }
+ return false;
+}
+
+function make_request_init(method: string, body?: any, signal?: AbortSignal): RequestInit {
+ const init = {
+ method,
+ credentials: "include",
+ signal,
+ headers: {
+ "X-TimeZone": Temporal.Now.timeZone().id,
+ },
+ } as RequestInit;
+
+ if (body) {
+ init.body = JSON.stringify(body);
+ init.headers["Content-Type"] = "application/json;charset=UTF-8";
+ }
+
+ return init;
+}
+
+export type InternalFetchRequest = {
+ url: string,
+ init: RequestInit,
+ timeout?: number
+ retry_count?: number,
+} \ No newline at end of file
diff --git a/code/app/src/utilities/cache.ts b/code/app/src/utilities/cache.ts
new file mode 100644
index 0000000..db9be9a
--- /dev/null
+++ b/code/app/src/utilities/cache.ts
@@ -0,0 +1,38 @@
+import { Temporal } from "temporal-polyfill";
+import { log_debug } from "$utilities/logger";
+
+let cache = {};
+
+export const CacheKeys = {
+ isAuthenticated: "isAuthenticated"
+}
+
+export async function cached_result_async<T>(key: string, staleAfterSeconds: number, get_result: any, forceRefresh: boolean = false) {
+ if (!cache[key]) {
+ cache[key] = {
+ l: 0,
+ c: undefined as T,
+ };
+ }
+ const staleEpoch = ((cache[key]?.l ?? 0) + staleAfterSeconds);
+ const isStale = forceRefresh || (staleEpoch < Temporal.Now.instant().epochSeconds);
+ if (isStale || !cache[key]?.c) {
+ cache[key].c = typeof get_result === "function" ? await get_result() : get_result;
+ cache[key].l = Temporal.Now.instant().epochSeconds;
+ }
+
+ log_debug("Ran cached_result_async", {
+ cacheKey: key,
+ isStale,
+ cache: cache[key],
+ staleEpoch,
+ });
+
+ return cache[key].c as T;
+}
+
+export function clear_cache_key(key: string) {
+ if (!key) throw new Error("No key was specified");
+ cache[key].c = undefined;
+ log_debug("Cleared cache with key: " + key);
+} \ No newline at end of file
diff --git a/code/app/src/utilities/colors.ts b/code/app/src/utilities/colors.ts
new file mode 100644
index 0000000..34c7992
--- /dev/null
+++ b/code/app/src/utilities/colors.ts
@@ -0,0 +1,47 @@
+export function generate_random_hex_color(skip_contrast_check = false) {
+ let hex = __generate_random_hex_color();
+ if (skip_contrast_check) return hex;
+ while ((__calculate_contrast_ratio("#ffffff", hex) < 4.5) || (__calculate_contrast_ratio("#000000", hex) < 4.5)) {
+ hex = __generate_random_hex_color();
+ }
+
+ return hex;
+}
+
+// Largely copied from chroma js api
+function __generate_random_hex_color(): string {
+ let code = "#";
+ for (let i = 0; i < 6; i++) {
+ code += "0123456789abcdef".charAt(Math.floor(Math.random() * 16));
+ }
+ return code;
+}
+
+function __calculate_contrast_ratio(hex1: string, hex2: string): number {
+ const rgb1 = __hex_to_rgb(hex1);
+ const rgb2 = __hex_to_rgb(hex2);
+ const l1 = __get_luminance(rgb1[0], rgb1[1], rgb1[2]);
+ const l2 = __get_luminance(rgb2[0], rgb2[1], rgb2[2]);
+ const result = l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05);
+ return result;
+}
+
+function __hex_to_rgb(hex: string): number[] {
+ if (!hex.match(/^#([A-Fa-f0-9]{6})$/)) return [];
+ if (hex[0] === "#") hex = hex.substring(1, hex.length);
+ return [parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(2, 4), 16), parseInt(hex.substring(4, 6), 16)];
+}
+
+function __get_luminance(r: any, g: any, b: any) {
+ // relative luminance
+ // see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ r = __luminance_x(r);
+ g = __luminance_x(g);
+ b = __luminance_x(b);
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}
+
+function __luminance_x(x: any) {
+ x /= 255;
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+}
diff --git a/code/app/src/utilities/crypto-helpers.ts b/code/app/src/utilities/crypto-helpers.ts
new file mode 100644
index 0000000..49af7d3
--- /dev/null
+++ b/code/app/src/utilities/crypto-helpers.ts
@@ -0,0 +1,48 @@
+// A formatted version of a popular md5 implementation.
+// Original copyright (c) Paul Johnston & Greg Holt.
+// The function itself is now 42 lines long.
+// https://stackoverflow.com/a/60467595 "Don't deny."
+
+export function get_md5_hash(inputString: string): string {
+ const hc = "0123456789abcdef";
+ function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; }
+ function ad(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
+ function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
+ function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); }
+ function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); }
+ function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); }
+ function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); }
+ function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); }
+ function sb(x) {
+ var i; var nblk = ((x.length + 8) >> 6) + 1; var blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0;
+ for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8);
+ blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = x.length * 8; return blks;
+ }
+ var i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd;
+ for (i = 0; i < x.length; i += 16) {
+ olda = a; oldb = b; oldc = c; oldd = d;
+ a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819);
+ b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
+ c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
+ d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
+ a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
+ b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
+ c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691);
+ d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
+ a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961);
+ b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784);
+ c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558);
+ d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
+ a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632);
+ b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222);
+ c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487);
+ d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
+ a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
+ b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
+ c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
+ d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
+ a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259);
+ b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd);
+ }
+ return rh(a) + rh(b) + rh(c) + rh(d);
+} \ No newline at end of file
diff --git a/code/app/src/utilities/dom-helpers.ts b/code/app/src/utilities/dom-helpers.ts
new file mode 100644
index 0000000..94a74c1
--- /dev/null
+++ b/code/app/src/utilities/dom-helpers.ts
@@ -0,0 +1,105 @@
+interface CreateElementOptions {
+ name: string,
+ properties?: object,
+ children?: Array<HTMLElement | Function | Node>
+}
+
+export function create_element_from_object(elementOptions: CreateElementOptions): HTMLElement {
+ return create_element(elementOptions.name, elementOptions.properties, elementOptions.children);
+}
+
+export function create_element(name: string, properties?: object, children?: Array<HTMLElement | any>): HTMLElement {
+ if (!name || name.length < 1) {
+ throw new Error("name is required");
+ }
+ const node = document.createElement(name);
+ if (properties) {
+ for (const [key, value] of Object.entries(properties)) {
+ // @ts-ignore
+ node[key] = value;
+ }
+ }
+
+ if (children && children.length > 0) {
+ let actualChildren = children;
+ if (typeof children === "function") {
+ // @ts-ignore
+ actualChildren = children();
+ }
+ for (const child of actualChildren) {
+ node.appendChild(child as Node);
+ }
+ }
+ return node;
+}
+
+// https://stackoverflow.com/a/45215694/11961742
+export function get_selected_options(domElement: HTMLSelectElement): Array<string> {
+ const ret = [];
+
+ // fast but not universally supported
+ if (domElement.selectedOptions !== undefined) {
+ for (let i = 0; i < domElement.selectedOptions.length; i++) {
+ ret.push(domElement.selectedOptions[i].value);
+ }
+
+ // compatible, but can be painfully slow
+ } else {
+ for (let i = 0; i < domElement.options.length; i++) {
+ if (domElement.options[i].selected) {
+ ret.push(domElement.options[i].value);
+ }
+ }
+ }
+ return ret;
+}
+
+
+export function get_element_position(element: HTMLElement | any) {
+ if (!element) return {x: 0, y: 0};
+ let x = 0;
+ let y = 0;
+ while (true) {
+ x += element.offsetLeft;
+ y += element.offsetTop;
+ if (element.offsetParent === null) {
+ break;
+ }
+ element = element.offsetParent;
+ }
+ return {x, y};
+}
+
+export function restrict_input_to_numbers(element: HTMLElement, specials: Array<string> = [], mergeSpecialsWithDefaults: boolean = false): void {
+ if (!element) return;
+ element.addEventListener("keydown", (e) => {
+ const defaultSpecials = ["Backspace", "ArrowLeft", "ArrowRight", "Tab"];
+ let keys = specials.length > 0 ? specials : defaultSpecials;
+ if (mergeSpecialsWithDefaults && specials) {
+ keys = [...specials, ...defaultSpecials];
+ }
+ if (keys.indexOf(e.key) !== -1) {
+ return;
+ }
+ if (isNaN(parseInt(e.key))) {
+ e.preventDefault();
+ }
+ });
+}
+
+export function element_has_focus(element: HTMLElement): boolean {
+ return element === document.activeElement;
+}
+
+export function move_focus(element: HTMLElement): void {
+ if (!element) {
+ element = document.getElementsByTagName("body")[0];
+ }
+ element.focus();
+ // @ts-ignore
+ if (!element_has_focus(element)) {
+ element.setAttribute("tabindex", "-1");
+ element.focus();
+ }
+}
+
diff --git a/code/app/src/utilities/global-state.ts b/code/app/src/utilities/global-state.ts
new file mode 100644
index 0000000..b585ced
--- /dev/null
+++ b/code/app/src/utilities/global-state.ts
@@ -0,0 +1,22 @@
+import { get } from "svelte/store";
+import { create_writable_persistent } from "./persistent-store";
+
+const state = create_writable_persistent<any>({
+ initialState: {},
+ name: "global-state"
+});
+
+export type GlobalStateKeys = "isLoggedIn" | "showEmailValidatedAlertWhenLoggedIn" | "all";
+
+export function fgs(key: GlobalStateKeys): any {
+ const value = get(state);
+ if (key === "all") return value;
+ return value[key];
+}
+
+export function sgs(key: GlobalStateKeys, value: any) {
+ if (key === "all") throw new Error("Not allowed to set global state key: all");
+ const stateValue = get(state);
+ stateValue[key] = JSON.stringify(value)
+ state.set(stateValue);
+} \ No newline at end of file
diff --git a/code/app/src/utilities/logger.ts b/code/app/src/utilities/logger.ts
new file mode 100644
index 0000000..c21bd76
--- /dev/null
+++ b/code/app/src/utilities/logger.ts
@@ -0,0 +1,118 @@
+import { browser, dev } from "$app/environment";
+import { env } from '$env/dynamic/private';
+import { StorageKeys } from "$configuration";
+import pino, { type Logger, type LoggerOptions } from "pino";
+import { createStream } from "pino-seq";
+import type { SeqConfig } from "pino-seq";
+
+function get_pino_logger(): Logger {
+ const config = {
+ name: "greatoffice-app",
+ level: LogLevel.current().as_string(),
+ customLevels: {
+ "INFO": LogLevel.INFO,
+ "WARNING": LogLevel.WARNING,
+ "ERROR": LogLevel.ERROR,
+ "DEBUG": LogLevel.DEBUG,
+ "SILENT": LogLevel.SILENT,
+ }
+ } as LoggerOptions;
+
+ const seq = {
+ config: {
+ apiKey: browser ? env.SEQ_API_KEY : "",
+ serverUrl: browser ? env.SEQ_SERVER_URL : ""
+ } as SeqConfig,
+ streams: [{
+ level: LogLevel.to_string(LogLevel.DEBUG),
+ }],
+ enabled: () => (
+ !browser
+ && !dev
+ && seq.config.apiKey.length > 0
+ && seq.config.serverUrl.length > 0
+ )
+ };
+
+ return seq.enabled() ? pino(config, createStream(seq.config)) : pino(config);
+}
+
+type LogLevelString = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "SILENT";
+
+export const LogLevel = {
+ DEBUG: 0,
+ INFO: 1,
+ WARNING: 2,
+ ERROR: 3,
+ SILENT: 4,
+ current(): { as_string: Function, as_number: Function } {
+ const logLevelString = (browser ? window.sessionStorage.getItem(StorageKeys.logLevel) : env.LOG_LEVEL) as LogLevelString;
+ return {
+ as_number(): number {
+ return LogLevel.to_number_or_default(logLevelString, LogLevel.INFO)
+ },
+ as_string(): LogLevelString {
+ return logLevelString.length > 3 ? logLevelString : LogLevel.to_string(LogLevel.INFO);
+ }
+ }
+ },
+ to_string(levelInt: number): LogLevelString {
+ switch (levelInt) {
+ case 0:
+ return "DEBUG";
+ case 1:
+ return "INFO";
+ case 2:
+ return "WARNING";
+ case 3:
+ return "ERROR";
+ case 4:
+ return "SILENT";
+ default:
+ throw new Error("Unknown LogLevel number " + levelInt);
+ }
+ },
+ to_number_or_default(levelString?: string | null, defaultValue?: number): number {
+ if (!levelString && defaultValue) return defaultValue;
+ else if (!levelString && !defaultValue) throw new Error("levelString was empty, and no default value was specified");
+ switch (levelString?.toUpperCase()) {
+ case "DEBUG":
+ return 0;
+ case "INFO":
+ return 1;
+ case "WARNING":
+ return 2;
+ case "ERROR":
+ return 3;
+ case "SILENT":
+ return 4;
+ default:
+ if (!defaultValue) throw new Error("Unknown LogLevel string " + levelString + ", and no defaultValue");
+ else return defaultValue;
+ }
+ },
+};
+
+export function log_warning(message: string, ...additional: any[]): void {
+ if (LogLevel.current().as_number() <= LogLevel.WARNING) {
+ get_pino_logger().warn(message, additional);
+ }
+}
+
+export function log_debug(message: string, ...additional: any[]): void {
+ if (LogLevel.current().as_number() <= LogLevel.DEBUG) {
+ get_pino_logger().debug(message, additional);
+ }
+}
+
+export function log_info(message: string, ...additional: any[]): void {
+ if (LogLevel.current().as_number() <= LogLevel.INFO) {
+ get_pino_logger().info(message, additional);
+ }
+}
+
+export function log_error(message: any, ...additional: any[]): void {
+ if (LogLevel.current().as_number() <= LogLevel.ERROR) {
+ get_pino_logger().error(message, additional);
+ }
+} \ No newline at end of file
diff --git a/code/app/src/utilities/misc-helpers.ts b/code/app/src/utilities/misc-helpers.ts
new file mode 100644
index 0000000..afb20e7
--- /dev/null
+++ b/code/app/src/utilities/misc-helpers.ts
@@ -0,0 +1,77 @@
+export function merge_obj_arr<T>(a: Array<T>, b: Array<T>, props: Array<string>): Array<T> {
+ let start = 0;
+ let merge = [];
+
+ while (start < a.length) {
+
+ if (a[start] === b[start]) {
+ //pushing the merged objects into array
+ merge.push({ ...a[start], ...b[start] });
+ }
+ //incrementing start value
+ start = start + 1;
+ }
+ return merge;
+}
+
+export function no_type_check(x: any) {
+ return x;
+}
+
+export function capitalise(value: string): string {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
+
+export function get_query_string(params: any = {}): string {
+ const map = Object.keys(params).reduce((arr: Array<string>, key: string) => {
+ if (params[key] !== undefined) {
+ return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
+ }
+ return arr;
+ }, [] as any);
+
+ if (map.length) {
+ return `?${map.join("&")}`;
+ }
+
+ return "";
+}
+
+export function make_url(url: string, params: object): string {
+ return `${url}${get_query_string(params)}`;
+}
+
+export function noop() {
+}
+
+export function random_string(length: number): string {
+ if (!length) {
+ throw new Error("length is undefined");
+ }
+ let result = "";
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const charactersLength = characters.length;
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+}
+
+export function get_random_int(min: number, max: number): number {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+export function get_hash_code(value: string): number | undefined {
+ let hash = 0;
+ if (value.length === 0) {
+ return;
+ }
+ for (let i = 0; i < value.length; i++) {
+ const char = value.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash |= 0;
+ }
+ return hash;
+}
diff --git a/code/app/src/utilities/persistent-store.ts b/code/app/src/utilities/persistent-store.ts
new file mode 100644
index 0000000..3f56312
--- /dev/null
+++ b/code/app/src/utilities/persistent-store.ts
@@ -0,0 +1,111 @@
+import { browser } from "$app/environment";
+import { writable as _writable, readable as _readable } from "svelte/store";
+import type { Writable, Readable, StartStopNotifier } from "svelte/store";
+import { log_debug, log_info } from "./logger";
+
+enum StoreType {
+ SESSION = 0,
+ LOCAL = 1
+}
+
+interface StoreOptions {
+ store?: StoreType;
+}
+
+interface WritableStoreInit<T> {
+ name: string,
+ initialState: T,
+ options?: StoreOptions
+}
+
+interface ReadableStoreInit<T> {
+ name: string,
+ initialState: T,
+ callback: StartStopNotifier<any>,
+ options?: StoreOptions
+}
+
+function get_store(type: StoreType): Storage {
+ if (!browser) return undefined;
+ switch (type) {
+ case StoreType.SESSION:
+ return window.sessionStorage;
+ case StoreType.LOCAL:
+ return window.localStorage;
+ }
+}
+
+function prepared_store_value(value: any): string {
+ try {
+ return JSON.stringify(value);
+ } catch (e) {
+ console.error(e);
+ return "__INVALID__";
+ }
+}
+
+function get_store_value<T>(init: WritableStoreInit<T> | ReadableStoreInit<T>): any {
+ try {
+ const storage = get_store(init.options.store);
+ if (!storage) return;
+ const value = storage.getItem(init.name);
+ if (!value) return false;
+ return JSON.parse(value);
+ } catch (e) {
+ console.error(e);
+ return { __INVALID__: true };
+ }
+}
+
+function hydrate<T>(store: Writable<T>, init: WritableStoreInit<T> | ReadableStoreInit<T>): void {
+ const value = get_store_value<T>(init);
+ if (value && store.set) store.set(value);
+}
+
+function subscribe<T>(store: Writable<T> | Readable<T>, init: WritableStoreInit<T> | ReadableStoreInit<T>): void {
+ const storage = get_store(init.options.store);
+ if (!storage) return;
+ if (!store.subscribe) return;
+ store.subscribe((state: any) => {
+ storage.setItem(init.name, prepared_store_value(state));
+ });
+}
+
+function create_writable_persistent<T>(init: WritableStoreInit<T>): Writable<T> {
+ if (!browser) {
+ log_info("WARN: Persistent store is only available in the browser");
+ return;
+ }
+ if (init.options === undefined) throw new Error("init is a required parameter");
+ log_debug("creating writable store with options: ", init);
+ const store = _writable<T>(init.initialState);
+ hydrate(store, init);
+ subscribe(store, init);
+ return store;
+}
+
+function create_readable_persistent<T>(init: ReadableStoreInit<T>): Readable<T> {
+ if (!browser) {
+ log_info("WARN: Persistent store is only available in the browser");
+ return;
+ }
+ if (init.options === undefined) throw new Error("init is a required parameter");
+ log_debug("Creating readable store with options: ", init);
+ const store = _readable<T>(init.initialState, init.callback);
+ // hydrate(store, options);
+ subscribe(store, init);
+ return store;
+}
+
+export {
+ create_writable_persistent,
+ create_readable_persistent,
+ StoreType,
+};
+
+export type {
+ WritableStoreInit as WritableStore,
+ ReadableStoreInit as ReadableStore,
+ StoreOptions,
+};
+
diff --git a/code/app/src/utilities/storage-helpers.ts b/code/app/src/utilities/storage-helpers.ts
new file mode 100644
index 0000000..cce655c
--- /dev/null
+++ b/code/app/src/utilities/storage-helpers.ts
@@ -0,0 +1,26 @@
+import { browser } from "$app/environment";
+import { is_empty_object } from "./validators";
+
+export type StorageType = "local" | "session";
+export const browserStorage = {
+ remove_with_regex(type: StorageType, regex: RegExp): void {
+ if (!browser) return;
+ const storage = (type === "local" ? window.localStorage : window.sessionStorage);
+ let n = storage.length;
+ while (n--) {
+ const key = storage.key(n);
+ if (key && regex.test(key)) {
+ storage.removeItem(key);
+ }
+ }
+ },
+ set_stringified(type: StorageType, key: string, value: object): void {
+ if (!browser) return;
+ if (is_empty_object(value)) return;
+ (type === "local" ? window.localStorage : window.sessionStorage).setItem(key, JSON.stringify(value));
+ },
+ get_stringified<T>(type: StorageType, key: string): T | any {
+ if (!browser) return;
+ return JSON.parse((type === "local" ? window.localStorage : window.sessionStorage).getItem(key) ?? "{}");
+ }
+} \ No newline at end of file
diff --git a/code/app/src/utilities/testing-helpers.ts b/code/app/src/utilities/testing-helpers.ts
new file mode 100644
index 0000000..f21412e
--- /dev/null
+++ b/code/app/src/utilities/testing-helpers.ts
@@ -0,0 +1,7 @@
+export function get_element_by_pw_key(key: string): HTMLElement | null {
+ return document.querySelector("[pw-key='" + key + "']");
+}
+
+export function get_pw_key_selector(key: string): string {
+ return "[pw-key='" + key + "']";
+} \ No newline at end of file
diff --git a/code/app/src/utilities/validators.ts b/code/app/src/utilities/validators.ts
new file mode 100644
index 0000000..b69470e
--- /dev/null
+++ b/code/app/src/utilities/validators.ts
@@ -0,0 +1,34 @@
+export const EMAIL_REGEX = new RegExp(/^([a-z0-9]+(?:([._\-])[a-z0-9]+)*@(?:[a-z0-9]+(?:(-)[a-z0-9]+)?\.)+[a-z0-9](?:[a-z0-9]*[a-z0-9])?)$/i);
+export const URL_REGEX = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-.][a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm);
+export const GUID_REGEX = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
+export const NORWEGIAN_PHONE_NUMBER_REGEX = new RegExp(/(0047|\+47|47)?\d{8,12}/);
+
+export function is_email(value: string): boolean {
+ return EMAIL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_url(value: string): boolean {
+ return URL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_norwegian_phone_number(value: string): boolean {
+ if (value.length < 8 || value.length > 12) {
+ return false;
+ }
+ return NORWEGIAN_PHONE_NUMBER_REGEX.test(String(value));
+}
+
+export function is_guid(value: string): boolean {
+ if (!value) {
+ return false;
+ }
+ if (value[0] === "{") {
+ value = value.substring(1, value.length - 1);
+ }
+ return GUID_REGEX.test(value);
+}
+
+export function is_empty_object(obj: object): boolean {
+ if (!obj) return true;
+ return obj !== void 0 && Object.keys(obj).length > 0;
+} \ No newline at end of file
diff --git a/code/app/static/favicon.ico b/code/app/static/favicon.ico
new file mode 100644
index 0000000..6848441
--- /dev/null
+++ b/code/app/static/favicon.ico
Binary files differ
diff --git a/code/app/static/version.txt b/code/app/static/version.txt
new file mode 100644
index 0000000..626799f
--- /dev/null
+++ b/code/app/static/version.txt
@@ -0,0 +1 @@
+v1
diff --git a/code/app/svelte.config.js b/code/app/svelte.config.js
new file mode 100644
index 0000000..2b4277a
--- /dev/null
+++ b/code/app/svelte.config.js
@@ -0,0 +1,27 @@
+import adapter from "@sveltejs/adapter-node";
+import preprocess from "svelte-preprocess";
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: [
+ preprocess({
+ postcss: true,
+ }),
+ ],
+ kit: {
+ adapter: adapter(),
+ alias: {
+ "$actions": "./src/actions",
+ "$routes": "./src/routes",
+ "$models": "./src/models",
+ "$api": "./src/api",
+ "$components": "./src/components",
+ "$utilities": "./src/utilities",
+ "$i18n": "./src/i18n",
+ "$services": "./src/services",
+ "$configuration": "./src/configuration",
+ }
+ },
+};
+
+export default config;
diff --git a/code/app/tailwind.config.cjs b/code/app/tailwind.config.cjs
new file mode 100644
index 0000000..3727a79
--- /dev/null
+++ b/code/app/tailwind.config.cjs
@@ -0,0 +1,141 @@
+const defaultColors = require("tailwindcss/colors");
+
+const refactoringUiPalette4 = {
+ "blue": {
+ "50": "#DCEEFB",
+ "100": "#B6E0FE",
+ "200": "#84C5F4",
+ "300": "#62B0E8",
+ "400": "#4098D7",
+ "500": "#2680C2",
+ "600": "#186FAF",
+ "700": "#0F609B",
+ "800": "#0A558C",
+ "900": "#003E6B",
+ },
+ "red": {
+ "50": "#FFEEEE",
+ "100": "#FACDCD",
+ "200": "#F29B9B",
+ "300": "#E66A6A",
+ "400": "#D64545",
+ "500": "#BA2525",
+ "600": "#A61B1B",
+ "700": "#911111",
+ "800": "#780A0A",
+ "900": "#610404",
+ },
+ "yellow": {
+ "50": "#FFFAEB",
+ "100": "#FCEFC7",
+ "200": "#F8E3A3",
+ "300": "#F9DA8B",
+ "400": "#F7D070",
+ "500": "#E9B949",
+ "600": "#C99A2E",
+ "700": "#A27C1A",
+ "800": "#7C5E10",
+ "900": "#513C06",
+ },
+ "purple": {
+ "50": "#EAE2F8",
+ "100": "#CFBCF2",
+ "200": "#A081D9",
+ "300": "#8662C7",
+ "400": "#724BB7",
+ "500": "#653CAD",
+ "600": "#51279B",
+ "700": "#421987",
+ "800": "#34126F",
+ "900": "#240754",
+ },
+ "blue-grey": {
+ "50": "#F0F4F8",
+ "100": "#D9E2EC",
+ "200": "#BCCCDC",
+ "300": "#9FB3C8",
+ "400": "#829AB1",
+ "500": "#627D98",
+ "600": "#486581",
+ "700": "#334E68",
+ "800": "#243B53",
+ "900": "#102A43",
+ },
+ "teal": {
+ "50": "#EFFCF6",
+ "100": "#C6F7E2",
+ "200": "#8EEDC7",
+ "300": "#65D6AD",
+ "400": "#3EBD93",
+ "500": "#27AB83",
+ "600": "#199473",
+ "700": "#147D64",
+ "800": "#0C6B58",
+ "900": "#014D40",
+ }
+}
+
+const config = {
+ content: ["./src/**/*.{html,js,svelte,ts}"],
+ theme: {
+ colors: {
+ "blue": refactoringUiPalette4.blue,
+ "red": refactoringUiPalette4.red,
+ "yellow": refactoringUiPalette4.yellow,
+ "purple": refactoringUiPalette4.purple,
+ "teal": refactoringUiPalette4.teal,
+ "green": refactoringUiPalette4.teal,
+ "gray": defaultColors.gray,
+ "white": defaultColors.white
+ }
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ ],
+ safelist: [
+ "bg-blue-50",
+ "bg-yellow-50",
+ "bg-red-50",
+ "bg-green-50",
+ "bg-blue-100",
+ "bg-yellow-100",
+ "bg-red-100",
+ "bg-green-100",
+ "text-blue-400",
+ "text-yellow-400",
+ "text-red-400",
+ "text-green-400",
+ "text-blue-800",
+ "text-yellow-800",
+ "text-red-800",
+ "text-green-800",
+ "text-blue-700",
+ "text-yellow-700",
+ "text-red-700",
+ "text-green-700",
+ "text-blue-500",
+ "text-yellow-500",
+ "text-red-500",
+ "text-green-500",
+ "border-teal-500",
+ "ring-teal-500",
+ "hover:text-blue-600",
+ "hover:text-yellow-600",
+ "hover:text-red-600",
+ "hover:text-green-600",
+ "hover:bg-blue-100",
+ "hover:bg-yellow-100",
+ "hover:bg-red-100",
+ "hover:bg-green-100",
+ "focus:ring-blue-600",
+ "focus:ring-yellow-600",
+ "focus:ring-red-600",
+ "focus:ring-green-600",
+ "focus:ring-offset-blue-50",
+ "focus:ring-offset-yellow-50",
+ "focus:ring-offset-red-50",
+ "focus:ring-offset-green-50",
+ ]
+};
+
+module.exports = config;
diff --git a/code/app/tsconfig.json b/code/app/tsconfig.json
new file mode 100644
index 0000000..b38b189
--- /dev/null
+++ b/code/app/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "sourceMap": false,
+ }
+} \ No newline at end of file
diff --git a/code/app/vite.config.js b/code/app/vite.config.js
new file mode 100644
index 0000000..bc6ae94
--- /dev/null
+++ b/code/app/vite.config.js
@@ -0,0 +1,16 @@
+import { sveltekit } from "@sveltejs/kit/vite";
+import { SvelteKitPWA } from "@vite-pwa/sveltekit";
+/** @type {import('vite').UserConfig} */
+const config = {
+ plugins: [sveltekit(), SvelteKitPWA()],
+ build: {
+ target: "es2020",
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ target: "es2020",
+ },
+ },
+};
+
+export default config;
diff --git a/code/frontpage/.gitignore b/code/frontpage/.gitignore
new file mode 100644
index 0000000..85ea915
--- /dev/null
+++ b/code/frontpage/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+public
+resources
+.netlify
+.hugo_build.lock \ No newline at end of file
diff --git a/code/frontpage/LICENSE b/code/frontpage/LICENSE
new file mode 100644
index 0000000..a9431aa
--- /dev/null
+++ b/code/frontpage/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2018-present, Gridsome
+Copyright (c) 2020-present, Henk Verlinde
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/code/frontpage/archetypes/blog.md b/code/frontpage/archetypes/blog.md
new file mode 100644
index 0000000..24c911b
--- /dev/null
+++ b/code/frontpage/archetypes/blog.md
@@ -0,0 +1,15 @@
+---
+title: "{{ replace .Name "-" " " | title }}"
+description: ""
+excerpt: ""
+date: {{ .Date }}
+lastmod: {{ .Date }}
+draft: true
+weight: 50
+images: []
+categories: []
+tags: []
+contributors: []
+pinned: false
+homepage: false
+---
diff --git a/code/frontpage/archetypes/default.md b/code/frontpage/archetypes/default.md
new file mode 100644
index 0000000..d8210df
--- /dev/null
+++ b/code/frontpage/archetypes/default.md
@@ -0,0 +1,8 @@
+---
+title: "{{ replace .Name "-" " " | title }}"
+description: ""
+date: {{ .Date }}
+lastmod: {{ .Date }}
+draft: true
+images: []
+---
diff --git a/code/frontpage/archetypes/docs.md b/code/frontpage/archetypes/docs.md
new file mode 100644
index 0000000..8089a43
--- /dev/null
+++ b/code/frontpage/archetypes/docs.md
@@ -0,0 +1,15 @@
+---
+title: "{{ replace .Name "-" " " | title }}"
+description: ""
+lead: ""
+date: {{ .Date }}
+lastmod: {{ .Date }}
+draft: true
+images: []
+menu:
+ docs:
+ parent: ""
+ identifier: "{{ .Name }}-{{ delimit (shuffle (split (md5 .Name) "" )) "" }}"
+weight: 999
+toc: true
+---
diff --git a/code/frontpage/archetypes/docs/_index.md b/code/frontpage/archetypes/docs/_index.md
new file mode 100644
index 0000000..3ed0540
--- /dev/null
+++ b/code/frontpage/archetypes/docs/_index.md
@@ -0,0 +1,10 @@
+---
+title: "Docs"
+description: ""
+lead: ""
+date: 2022-01-25T14:40:56+01:00
+lastmod: 2022-01-25T14:40:56+01:00
+draft: false
+images: []
+type: docs
+---
diff --git a/code/frontpage/archetypes/docs/lorem/_index.md b/code/frontpage/archetypes/docs/lorem/_index.md
new file mode 100644
index 0000000..c1b50ae
--- /dev/null
+++ b/code/frontpage/archetypes/docs/lorem/_index.md
@@ -0,0 +1,10 @@
+---
+title: "Lorem"
+description: ""
+lead: ""
+date: 2022-01-25T14:41:21+01:00
+lastmod: 2022-01-25T14:41:21+01:00
+draft: false
+images: []
+type: docs
+---
diff --git a/code/frontpage/archetypes/docs/lorem/ipsum/index.md b/code/frontpage/archetypes/docs/lorem/ipsum/index.md
new file mode 100644
index 0000000..6264981
--- /dev/null
+++ b/code/frontpage/archetypes/docs/lorem/ipsum/index.md
@@ -0,0 +1,16 @@
+---
+title: "Ipsum"
+description: ""
+lead: ""
+date: 2022-01-25T14:41:39+01:00
+lastmod: 2022-01-25T14:41:39+01:00
+draft: false
+images: []
+type: docs
+menu:
+ {{ .Section }}:
+ parent: "lorem"
+ identifier: "{{ .Name }}-{{ delimit (shuffle (split (md5 .Name) "" )) "" }}"
+weight: 100
+toc: true
+---
diff --git a/code/frontpage/assets/fonts/.gitkeep b/code/frontpage/assets/fonts/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/assets/fonts/.gitkeep
diff --git a/code/frontpage/assets/images/default-image.png b/code/frontpage/assets/images/default-image.png
new file mode 100644
index 0000000..a34ff9f
--- /dev/null
+++ b/code/frontpage/assets/images/default-image.png
Binary files differ
diff --git a/code/frontpage/assets/js/alert-init.js b/code/frontpage/assets/js/alert-init.js
new file mode 100644
index 0000000..af3ac32
--- /dev/null
+++ b/code/frontpage/assets/js/alert-init.js
@@ -0,0 +1,5 @@
+Object.keys(localStorage).forEach(function(key) {
+ if (/^global-alert-/.test(key)) {
+ document.documentElement.setAttribute('data-global-alert', 'closed');
+ }
+}); \ No newline at end of file
diff --git a/code/frontpage/assets/js/alert.js b/code/frontpage/assets/js/alert.js
new file mode 100644
index 0000000..1956103
--- /dev/null
+++ b/code/frontpage/assets/js/alert.js
@@ -0,0 +1,20 @@
+var announcement = document.getElementById('announcement');
+
+if (announcement !== null) {
+
+ var id = announcement.dataset.id;
+
+ Object.keys(localStorage).forEach(function(key) {
+ if (/^global-alert-/.test(key)) {
+ if (key !== id ) {
+ localStorage.removeItem(key);
+ document.documentElement.removeAttribute('data-global-alert');
+ }
+ }
+ });
+
+ announcement.addEventListener('closed.bs.alert', () => {
+ localStorage.setItem(id, 'closed');
+ });
+
+} \ No newline at end of file
diff --git a/code/frontpage/assets/js/app.js b/code/frontpage/assets/js/app.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/assets/js/app.js
diff --git a/code/frontpage/assets/js/bootstrap.js b/code/frontpage/assets/js/bootstrap.js
new file mode 100644
index 0000000..8d6da8d
--- /dev/null
+++ b/code/frontpage/assets/js/bootstrap.js
@@ -0,0 +1,2 @@
+import 'bootstrap/dist/js/bootstrap.bundle.min.js'
+// import 'bootstrap/dist/js/bootstrap.min.js'
diff --git a/code/frontpage/assets/js/clipboard.js b/code/frontpage/assets/js/clipboard.js
new file mode 100644
index 0000000..55eec7b
--- /dev/null
+++ b/code/frontpage/assets/js/clipboard.js
@@ -0,0 +1,37 @@
+import Clipboard from 'clipboard';
+
+var pre = document.getElementsByTagName('pre');
+
+for (var i = 0; i < pre.length; ++ i)
+{
+ var element = pre[i];
+ var mermaid = element.getElementsByClassName('language-mermaid')[0];
+
+ if (mermaid == null) {
+ element.insertAdjacentHTML('afterbegin', '<button class="btn btn-copy"></button>');
+ }
+}
+
+var clipboard = new Clipboard('.btn-copy', {
+
+ target: function(trigger) {
+ return trigger.nextElementSibling;
+ },
+
+});
+
+clipboard.on('success', function(e) {
+
+ /*
+ console.info('Action:', e.action);
+ console.info('Text:', e.text);
+ console.info('Trigger:', e.trigger);
+ */
+
+ e.clearSelection();
+});
+
+clipboard.on('error', function(e) {
+ console.error('Action:', e.action);
+ console.error('Trigger:', e.trigger);
+});
diff --git a/code/frontpage/assets/js/darkmode-init.js b/code/frontpage/assets/js/darkmode-init.js
new file mode 100644
index 0000000..0f3508d
--- /dev/null
+++ b/code/frontpage/assets/js/darkmode-init.js
@@ -0,0 +1,21 @@
+const globalDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+const localMode = localStorage.getItem('theme');
+
+if (globalDark && (localMode === null)) {
+
+ localStorage.setItem('theme', 'dark');
+ document.documentElement.setAttribute('data-dark-mode', '');
+
+}
+
+if (globalDark && (localMode === 'dark')) {
+
+ document.documentElement.setAttribute('data-dark-mode', '');
+
+}
+
+if (localMode === 'dark') {
+
+ document.documentElement.setAttribute('data-dark-mode', '');
+
+}
diff --git a/code/frontpage/assets/js/darkmode.js b/code/frontpage/assets/js/darkmode.js
new file mode 100644
index 0000000..e81db47
--- /dev/null
+++ b/code/frontpage/assets/js/darkmode.js
@@ -0,0 +1,38 @@
+const mode = document.getElementById('mode');
+
+if (mode !== null) {
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
+
+ if (event.matches) {
+
+ localStorage.setItem('theme', 'dark');
+ document.documentElement.setAttribute('data-dark-mode', '');
+
+ } else {
+
+ localStorage.setItem('theme', 'light');
+ document.documentElement.removeAttribute('data-dark-mode');
+
+ }
+
+ })
+
+ mode.addEventListener('click', () => {
+
+ document.documentElement.toggleAttribute('data-dark-mode');
+ localStorage.setItem('theme', document.documentElement.hasAttribute('data-dark-mode') ? 'dark' : 'light');
+
+ });
+
+ if (localStorage.getItem('theme') === 'dark') {
+
+ document.documentElement.setAttribute('data-dark-mode', '');
+
+ } else {
+
+ document.documentElement.removeAttribute('data-dark-mode');
+
+ }
+
+}
diff --git a/code/frontpage/assets/js/highlight.js b/code/frontpage/assets/js/highlight.js
new file mode 100644
index 0000000..4ad6017
--- /dev/null
+++ b/code/frontpage/assets/js/highlight.js
@@ -0,0 +1,26 @@
+import hljs from 'highlight.js/lib/core';
+
+import javascript from 'highlight.js/lib/languages/javascript';
+import json from 'highlight.js/lib/languages/json';
+import bash from 'highlight.js/lib/languages/bash';
+import xml from 'highlight.js/lib/languages/xml';
+import ini from 'highlight.js/lib/languages/ini';
+import yaml from 'highlight.js/lib/languages/yaml';
+import markdown from 'highlight.js/lib/languages/markdown';
+import python from 'highlight.js/lib/languages/python';
+
+hljs.registerLanguage('javascript', javascript);
+hljs.registerLanguage('json', json);
+hljs.registerLanguage('bash', bash);
+hljs.registerLanguage('html', xml);
+hljs.registerLanguage('ini', ini);
+hljs.registerLanguage('toml', ini);
+hljs.registerLanguage('yaml', yaml);
+hljs.registerLanguage('md', markdown);
+hljs.registerLanguage('python', python);
+
+document.addEventListener('DOMContentLoaded', () => {
+ document.querySelectorAll('pre code:not(.language-mermaid)').forEach((block) => {
+ hljs.highlightElement(block);
+ });
+});
diff --git a/code/frontpage/assets/js/index.js b/code/frontpage/assets/js/index.js
new file mode 100644
index 0000000..15e09bb
--- /dev/null
+++ b/code/frontpage/assets/js/index.js
@@ -0,0 +1,179 @@
+var suggestions = document.getElementById('suggestions');
+var search = document.getElementById('search');
+
+if (search !== null) {
+ document.addEventListener('keydown', inputFocus);
+}
+
+function inputFocus(e) {
+ if (e.ctrlKey && e.key === '/' ) {
+ e.preventDefault();
+ search.focus();
+ }
+ if (e.key === 'Escape' ) {
+ search.blur();
+ suggestions.classList.add('d-none');
+ }
+}
+
+document.addEventListener('click', function(event) {
+
+ var isClickInsideElement = suggestions.contains(event.target);
+
+ if (!isClickInsideElement) {
+ suggestions.classList.add('d-none');
+ }
+
+});
+
+/*
+Source:
+ - https://dev.to/shubhamprakash/trap-focus-using-javascript-6a3
+*/
+
+document.addEventListener('keydown',suggestionFocus);
+
+function suggestionFocus(e) {
+ const suggestionsHidden = suggestions.classList.contains('d-none');
+ if (suggestionsHidden) return;
+
+ const focusableSuggestions= [...suggestions.querySelectorAll('a')];
+ if (focusableSuggestions.length === 0) return;
+
+ const index = focusableSuggestions.indexOf(document.activeElement);
+
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ const nextIndex = index > 0 ? index - 1 : 0;
+ focusableSuggestions[nextIndex].focus();
+ }
+ else if (e.key === "ArrowDown") {
+ e.preventDefault();
+ const nextIndex= index + 1 < focusableSuggestions.length ? index + 1 : index;
+ focusableSuggestions[nextIndex].focus();
+ }
+
+}
+
+/*
+Source:
+ - https://github.com/nextapps-de/flexsearch#index-documents-field-search
+ - https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html
+*/
+
+(function(){
+
+ var index = new FlexSearch.Document({
+ tokenize: "forward",
+ cache: 100,
+ document: {
+ id: 'id',
+ store: [
+ "href", "title", "description"
+ ],
+ index: ["title", "description", "content"]
+ }
+ });
+
+
+ // Not yet supported: https://github.com/nextapps-de/flexsearch#complex-documents
+
+ /*
+ var docs = [
+ {{ range $index, $page := (where .Site.Pages "Section" "docs") -}}
+ {
+ id: {{ $index }},
+ href: "{{ .Permalink }}",
+ title: {{ .Title | jsonify }},
+ description: {{ .Params.description | jsonify }},
+ content: {{ .Content | jsonify }}
+ },
+ {{ end -}}
+ ];
+ */
+
+ // https://discourse.gohugo.io/t/range-length-or-last-element/3803/2
+
+ {{ $list := slice }}
+ {{- if and (isset .Site.Params.options "searchsectionsindex") (not (eq (len .Site.Params.options.searchSectionsIndex) 0)) }}
+ {{- if eq .Site.Params.options.searchSectionsIndex "ALL" }}
+ {{- $list = .Site.Pages }}
+ {{- else }}
+ {{- $list = (where .Site.Pages "Type" "in" .Site.Params.options.searchSectionsIndex) }}
+ {{- if (in .Site.Params.options.searchSectionsIndex "HomePage") }}
+ {{ $list = $list | append .Site.Home }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ {{- $list = (where .Site.Pages "Section" "docs") }}
+ {{- end }}
+
+ {{ $len := (len $list) -}}
+
+ {{ range $index, $element := $list -}}
+ index.add(
+ {
+ id: {{ $index }},
+ href: "{{ .RelPermalink }}",
+ title: {{ .Title | jsonify }},
+ {{ with .Description -}}
+ description: {{ . | jsonify }},
+ {{ else -}}
+ description: {{ .Summary | plainify | jsonify }},
+ {{ end -}}
+ content: {{ .Plain | jsonify }}
+ }
+ );
+ {{ end -}}
+
+ search.addEventListener('input', show_results, true);
+
+ function show_results(){
+ const maxResult = 5;
+ var searchQuery = this.value;
+ var results = index.search(searchQuery, {limit: maxResult, enrich: true});
+
+ // flatten results since index.search() returns results for each indexed field
+ const flatResults = new Map(); // keyed by href to dedupe results
+ for (const result of results.flatMap(r => r.result)) {
+ if (flatResults.has(result.doc.href)) continue;
+ flatResults.set(result.doc.href, result.doc);
+ }
+
+ suggestions.innerHTML = "";
+ suggestions.classList.remove('d-none');
+
+ // inform user that no results were found
+ if (flatResults.size === 0 && searchQuery) {
+ const noResultsMessage = document.createElement('div')
+ noResultsMessage.innerHTML = `No results for "<strong>${searchQuery}</strong>"`
+ noResultsMessage.classList.add("suggestion__no-results");
+ suggestions.appendChild(noResultsMessage);
+ return;
+ }
+
+ // construct a list of suggestions
+ for(const [href, doc] of flatResults) {
+ const entry = document.createElement('div');
+ suggestions.appendChild(entry);
+
+ const a = document.createElement('a');
+ a.href = href;
+ entry.appendChild(a);
+
+ const title = document.createElement('span');
+ title.textContent = doc.title;
+ title.classList.add("suggestion__title");
+ a.appendChild(title);
+
+ const description = document.createElement('span');
+ description.textContent = doc.description;
+ description.classList.add("suggestion__description");
+ a.appendChild(description);
+
+ suggestions.appendChild(entry);
+
+ if(suggestions.childElementCount == maxResult) break;
+ }
+ }
+}());
diff --git a/code/frontpage/assets/js/instant.page.js b/code/frontpage/assets/js/instant.page.js
new file mode 100644
index 0000000..b394bcc
--- /dev/null
+++ b/code/frontpage/assets/js/instant.page.js
@@ -0,0 +1 @@
+import 'instant.page';
diff --git a/code/frontpage/assets/js/katex.js b/code/frontpage/assets/js/katex.js
new file mode 100644
index 0000000..e0543ea
--- /dev/null
+++ b/code/frontpage/assets/js/katex.js
@@ -0,0 +1,10 @@
+document.addEventListener('DOMContentLoaded', function() {
+ renderMathInElement(document.body, {
+ delimiters: [
+ {left: '$$', right: '$$', display: true},
+ {left: '$', right: '$', display: false},
+ {left: '\\(', right: '\\)', display: false},
+ {left: '\\[', right: '\\]', display: true},
+ ],
+ });
+});
diff --git a/code/frontpage/assets/js/lazysizes.js b/code/frontpage/assets/js/lazysizes.js
new file mode 100644
index 0000000..c12ed58
--- /dev/null
+++ b/code/frontpage/assets/js/lazysizes.js
@@ -0,0 +1 @@
+import 'lazysizes';
diff --git a/code/frontpage/assets/js/mermaid.js b/code/frontpage/assets/js/mermaid.js
new file mode 100644
index 0000000..98d67e1
--- /dev/null
+++ b/code/frontpage/assets/js/mermaid.js
@@ -0,0 +1,11 @@
+import mermaid from 'mermaid';
+
+var config = {
+ theme: 'default',
+ fontFamily: '-apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";',
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ mermaid.initialize(config);
+ mermaid.init(undefined, '.language-mermaid');
+});
diff --git a/code/frontpage/assets/js/scroll-lock.js b/code/frontpage/assets/js/scroll-lock.js
new file mode 100644
index 0000000..069b8c2
--- /dev/null
+++ b/code/frontpage/assets/js/scroll-lock.js
@@ -0,0 +1,14 @@
+// Adds scroll position lock for default docs sidebar
+
+if (document.querySelector('#sidebar-default') !== null) {
+ let sidebar = document.getElementById('sidebar-default');
+
+ let pos = sessionStorage.getItem('sidebar-scroll');
+ if (pos !== null) {
+ sidebar.scrollTop = parseInt(pos, 10);
+ }
+
+ window.addEventListener('beforeunload', () => {
+ sessionStorage.setItem('sidebar-scroll', sidebar.scrollTop);
+ });
+}
diff --git a/code/frontpage/assets/js/to-top.js b/code/frontpage/assets/js/to-top.js
new file mode 100644
index 0000000..3287f43
--- /dev/null
+++ b/code/frontpage/assets/js/to-top.js
@@ -0,0 +1,20 @@
+var topbutton = document.getElementById('toTop');
+
+if (topbutton !== null) {
+
+ topbutton.style.display = 'none';
+ window.onscroll = function() {
+ scrollFunction()
+ };
+
+}
+
+function scrollFunction() {
+
+ if (document.body.scrollTop > 40 || document.documentElement.scrollTop > 40) {
+ topbutton.style.display = 'block';
+ } else {
+ topbutton.style.display = 'none';
+ }
+
+}
diff --git a/code/frontpage/assets/js/vendor/.gitkeep b/code/frontpage/assets/js/vendor/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/assets/js/vendor/.gitkeep
diff --git a/code/frontpage/assets/scss/app.scss b/code/frontpage/assets/scss/app.scss
new file mode 100644
index 0000000..e50a403
--- /dev/null
+++ b/code/frontpage/assets/scss/app.scss
@@ -0,0 +1,31 @@
+/** Import Bootstrap functions */
+@import "bootstrap/scss/functions";
+
+/** Import theme variables */
+@import "common/variables";
+
+/** Import Bootstrap */
+@import "bootstrap/scss/bootstrap";
+
+/** Import highlight.js */
+// @import "highlight.js/scss/github-dark-dimmed";
+
+/** Import theme styles */
+@import "common/global";
+@import "common/dark";
+@import "components/alerts";
+@import "components/buttons";
+@import "components/code";
+@import "components/details";
+@import "components/syntax";
+@import "components/comments";
+@import "components/forms";
+@import "components/images";
+@import "components/mermaid";
+@import "components/search";
+@import "components/tables";
+@import "layouts/footer";
+@import "layouts/header";
+@import "layouts/pages";
+@import "layouts/posts";
+@import "layouts/sidebar"; \ No newline at end of file
diff --git a/code/frontpage/assets/scss/common/_dark.scss b/code/frontpage/assets/scss/common/_dark.scss
new file mode 100644
index 0000000..2f77262
--- /dev/null
+++ b/code/frontpage/assets/scss/common/_dark.scss
@@ -0,0 +1,582 @@
+/** Theme variables */
+
+// Source: https://material.io/design/color/dark-theme.html
+
+$body-bg-dark: $gray-900;
+$body-overlay-dark: darken($body-bg-dark, 2.5%);
+
+/*
+$border-dark: darken($body-bg-dark, 2.5%);
+*/
+$border-dark: $gray-800;
+$body-color-dark: $gray-300;
+$dots-dark: darken($body-color-dark, 50%);
+
+$link-color-dark: $blue-300;
+$button-color-dark: $link-color-dark;
+$focus-color-dark: lighten($link-color-dark, 2.5%);
+
+$navbar-dark-color: $body-color-dark;
+$navbar-dark-hover-color: $link-color-dark;
+$navbar-dark-active-color: $link-color-dark;
+
+/** Theme styles */
+
+[data-dark-mode] body {
+ background: $body-bg-dark;
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body a {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body a.text-body {
+ color: $body-color-dark !important;
+}
+
+[data-dark-mode] body .btn-primary {
+ @include button-variant($button-color-dark, $button-color-dark);
+
+ color: $body-bg-dark !important;
+}
+
+[data-dark-mode] body .btn-outline-primary {
+ @include button-outline-variant($button-color-dark, $button-color-dark);
+
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .btn-outline-primary:hover {
+ color: $body-bg-dark;
+}
+
+[data-dark-mode] body .btn-doks-light {
+ color: $navbar-dark-color;
+}
+
+[data-dark-mode] body .show > .btn-doks-light,
+[data-dark-mode] body .btn-doks-light:hover,
+[data-dark-mode] body .btn-doks-light:active {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .btn-menu svg {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .doks-sidebar-toggle {
+ color: $navbar-dark-color;
+}
+
+[data-dark-mode] body .btn-menu:hover,
+[data-dark-mode] body .btn-doks-light:hover,
+[data-dark-mode] body .doks-sidebar-toggle:hover {
+ background: $body-overlay-dark;
+}
+
+/*
+[data-dark-mode] body .dropdown-menu {
+ @extend .dropdown-menu-dark;
+}
+*/
+
+[data-dark-mode] body .navbar,
+[data-dark-mode] body .doks-subnavbar {
+ background-color: rgba(33, 37, 41, 0.95);
+ border-bottom: 1px solid $border-dark;
+}
+
+[data-dark-mode] body.home .navbar {
+ border-bottom: 0;
+}
+
+[data-dark-mode] body .offcanvas-header {
+ border-bottom: 1px solid $gray-800;
+}
+
+[data-dark-mode] body .offcanvas .nav-link {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .offcanvas .nav-link:hover,
+[data-dark-mode] body .offcanvas .nav-link:focus {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .offcanvas .nav-link.active {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .navbar-light .navbar-brand {
+ color: $navbar-dark-color !important;
+}
+
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link {
+ color: $navbar-dark-color;
+}
+
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link:hover,
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link:focus {
+ color: $navbar-dark-hover-color;
+}
+
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link.disabled {
+ color: $navbar-dark-disabled-color;
+}
+
+[data-dark-mode] body .navbar-light .navbar-nav .show > .nav-link,
+[data-dark-mode] body .navbar-light .navbar-nav .active > .nav-link,
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link.show,
+[data-dark-mode] body .navbar-light .navbar-nav .nav-link.active {
+ color: $navbar-dark-active-color;
+}
+
+[data-dark-mode] body .navbar-light .navbar-text {
+ color: $navbar-dark-color;
+}
+
+[data-dark-mode] body .alert-primary a {
+ color: $body-bg-dark;
+}
+
+[data-dark-mode] body .alert-doks {
+ background: $body-overlay-dark;
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .alert-doks a {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .page-links a {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .btn-toggle-nav a {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .showcase-meta a {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .showcase-meta a:hover,
+[data-dark-mode] body .showcase-meta a:focus {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .docs-link:hover,
+[data-dark-mode] body .docs-link.active,
+[data-dark-mode] body .page-links a:hover {
+ text-decoration: none;
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .btn-toggle {
+ color: $body-color-dark;
+ background-color: transparent;
+ border: 0;
+}
+
+[data-dark-mode] body .btn-toggle:hover,
+[data-dark-mode] body .btn-toggle:focus {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .btn-toggle::before {
+ width: 1.25em;
+ line-height: 0;
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28222, 226, 230, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+ transition: transform 0.35s ease;
+ transform-origin: 0.5em 50%;
+ margin-bottom: 0.125rem;
+}
+
+[data-dark-mode] body .btn-toggle[aria-expanded="true"] {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .btn-toggle[aria-expanded="true"]::before {
+ transform: rotate(90deg);
+}
+
+[data-dark-mode] body .btn-toggle-nav a:hover,
+[data-dark-mode] body .btn-toggle-nav a:focus {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .btn-toggle-nav a.active {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .navbar-light .navbar-text a {
+ color: $navbar-dark-active-color;
+}
+
+[data-dark-mode] body .docs-links h3.sidebar-link a,
+[data-dark-mode] body .page-links h3.sidebar-link a {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .navbar-light .navbar-text a:hover,
+[data-dark-mode] body .navbar-light .navbar-text a:focus {
+ color: $navbar-dark-active-color;
+}
+
+[data-dark-mode] body .navbar .btn-link {
+ color: $navbar-dark-color;
+}
+
+[data-dark-mode] body .content .btn-link {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .content .btn-link:hover {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .content img[src^="https://latex.codecogs.com/svg.latex"] {
+ filter: invert(1);
+}
+
+[data-dark-mode] body .navbar .btn-link:hover {
+ color: $navbar-dark-hover-color;
+}
+
+[data-dark-mode] body .navbar .btn-link:active {
+ color: $navbar-dark-active-color;
+}
+
+[data-dark-mode] body .form-control.is-search {
+ background: $body-overlay-dark;
+ border: 1px solid transparent;
+ color: $gray-300;
+
+ /*
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-search'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+ */
+}
+
+[data-dark-mode] body .form-control.is-search:focus {
+ border: 1px solid $link-color-dark;
+}
+
+[data-dark-mode] body .doks-search::after {
+ color: $gray-300;
+ border: 1px solid $gray-700;
+}
+
+[data-dark-mode] body .text-dark {
+ color: $body-color-dark !important;
+}
+
+/*
+[data-dark-mode] body .navbar-form::after {
+ color: $gray-600;
+ border: 1px solid $gray-800;
+}
+*/
+
+[data-dark-mode] body .form-control {
+ color: $gray-300;
+}
+
+[data-dark-mode] body .form-control::placeholder {
+ color: $gray-400;
+ opacity: 1;
+}
+
+[data-dark-mode] body .border-top {
+ border-top: 1px solid $border-dark !important;
+}
+
+@include media-breakpoint-up(lg) {
+ [data-dark-mode] body .docs-sidebar {
+ order: 0;
+ border-right: 1px solid $border-dark;
+ }
+}
+
+[data-dark-mode] body .docs-navigation {
+ border-top: 1px solid $border-dark;
+}
+
+[data-dark-mode] body pre code::-webkit-scrollbar-thumb {
+ background: $gray-400;
+}
+
+[data-dark-mode] body code:not(.hljs) {
+ background: $body-overlay-dark;
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body pre code:hover {
+ scrollbar-width: thin;
+ scrollbar-color: $border-dark transparent;
+}
+
+[data-dark-mode] body pre code::-webkit-scrollbar-thumb:hover {
+ background: $gray-500;
+}
+
+[data-dark-mode] body blockquote {
+ border-left: 3px solid $border-dark;
+}
+
+[data-dark-mode] body .footer {
+ border-top: 1px solid $border-dark;
+}
+
+[data-dark-mode] body .docs-links,
+[data-dark-mode] body .docs-toc {
+ scrollbar-width: thin;
+ scrollbar-color: $body-bg-dark $body-bg-dark;
+}
+
+[data-dark-mode] body .docs-links::-webkit-scrollbar,
+[data-dark-mode] body .docs-toc::-webkit-scrollbar {
+ width: 5px;
+}
+
+[data-dark-mode] body .docs-links::-webkit-scrollbar-track,
+[data-dark-mode] body .docs-toc::-webkit-scrollbar-track {
+ background: $body-bg-dark;
+}
+
+[data-dark-mode] body .docs-links::-webkit-scrollbar-thumb,
+[data-dark-mode] body .docs-toc::-webkit-scrollbar-thumb {
+ background: $body-bg-dark;
+}
+
+[data-dark-mode] body .docs-links:hover,
+[data-dark-mode] body .docs-toc:hover {
+ scrollbar-width: thin;
+ scrollbar-color: $border-dark $body-bg-dark;
+}
+
+[data-dark-mode] body .docs-links:hover::-webkit-scrollbar-thumb,
+[data-dark-mode] body .docs-toc:hover::-webkit-scrollbar-thumb {
+ background: $border-dark;
+}
+
+[data-dark-mode] body .docs-links::-webkit-scrollbar-thumb:hover,
+[data-dark-mode] body .docs-toc::-webkit-scrollbar-thumb:hover {
+ background: $border-dark;
+}
+
+[data-dark-mode] body .docs-links h3:not(:first-child) {
+ border-top: 1px solid $border-dark;
+}
+
+[data-dark-mode] body a.docs-link {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .page-links li:not(:first-child) {
+ border-top: 1px dashed $border-dark;
+}
+
+[data-dark-mode] body .card {
+ background: $body-bg-dark;
+ border: 1px solid $border-dark;
+}
+
+[data-dark-mode] body .card.bg-light {
+ background: $body-overlay-dark !important;
+}
+
+[data-dark-mode] body .navbar .menu-icon .navicon {
+ background: $navbar-dark-color;
+}
+
+[data-dark-mode] body .navbar .menu-icon .navicon::before,
+[data-dark-mode] body .navbar .menu-icon .navicon::after {
+ background: $navbar-dark-color;
+}
+
+[data-dark-mode] body .logo-light {
+ display: none !important;
+}
+
+[data-dark-mode] body .logo-dark {
+ display: inline-block !important;
+}
+
+[data-dark-mode] body .bg-light {
+ background: darken($body-bg-dark, 1.5%) !important;
+}
+
+[data-dark-mode] body .bg-dots {
+ background-image: radial-gradient($dots-dark 15%, transparent 15%);
+}
+
+[data-dark-mode] body .text-muted {
+ color: darken($body-color-dark, 7.5%) !important;
+}
+
+[data-dark-mode] body .alert-primary {
+ background: $link-color-dark;
+ color: $body-bg-dark;
+}
+
+[data-dark-mode] body .figure-caption {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body table {
+ @extend .table-dark;
+}
+
+[data-dark-mode] body .copy-status::after {
+ content: "Copy";
+ display: block;
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .copy-status:hover::after {
+ content: "Copy";
+ display: block;
+ color: $link-color-dark;
+}
+
+[data-dark-mode] body .copy-status:focus::after,
+[data-dark-mode] body .copy-status:active::after {
+ content: "Copied";
+ display: block;
+ color: $link-color-dark;
+}
+
+/*
+[data-dark-mode] body .dropdown-toggle:focus,
+[data-dark-mode] body .doks-sidebar-toggle:focus {
+ box-shadow: 0 0 0 0.2rem $focus-color-dark;
+}
+*/
+
+[data-dark-mode] body .offcanvas {
+ background-color: $body-bg-dark;
+}
+
+[data-dark-mode] body .btn-close {
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNkZWUyZTYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLXgiPjxsaW5lIHgxPSIxOCIgeTE9IjYiIHgyPSI2IiB5Mj0iMTgiPjwvbGluZT48bGluZSB4MT0iNiIgeTE9IjYiIHgyPSIxOCIgeTI9IjE4Ij48L2xpbmU+PC9zdmc+");
+ background-size: 1.5rem;
+}
+
+@include media-breakpoint-up(md) {
+ [data-dark-mode] body .alert-dismissible .btn-close {
+ background-size: 1.25rem;
+ }
+}
+
+/*
+[data-dark-mode] body .btn-close:focus {
+ box-shadow: 0 0 0 0.2rem $focus-color-dark;
+}
+*/
+
+[data-dark-mode] .dropdown-item {
+ color: $body-bg-dark;
+}
+
+[data-dark-mode] body hr.text-black-50 {
+ color: $gray-600 !important;
+}
+
+[data-dark-mode] body .email-form .form-control {
+ background: $body-overlay-dark;
+ border: 1px solid transparent;
+}
+
+[data-dark-mode] body .email-form .form-control:focus {
+ border: 1px solid $link-color-dark;
+}
+
+[data-dark-mode] .page-link {
+ color: $link-color-dark;
+ background-color: transparent;
+ border: $pagination-border-width solid $border-dark;
+
+ &:hover {
+ color: $body-bg-dark;
+ background-color: $body-color-dark;
+ border-color: $body-color-dark;
+ }
+
+ &:focus {
+ color: $body-bg-dark;
+ background-color: $body-color-dark;
+ }
+}
+
+[data-dark-mode] .page-item {
+ &.active .page-link {
+ color: $body-bg-dark;
+
+ @include gradient-bg($link-color-dark);
+
+ border-color: $link-color-dark;
+ }
+
+ &.disabled .page-link {
+ color: $pagination-disabled-color;
+ background-color: $body-overlay-dark;
+ border-color: $border-dark;
+ }
+}
+
+[data-dark-mode] .dropdown-menu {
+ background: $body-overlay-dark;
+}
+
+[data-dark-mode] .dropdown-menu .dropdown-item {
+ color: $body-color-dark;
+}
+
+[data-dark-mode] .dropdown-menu .dropdown-item:hover {
+ color: $link-color-dark;
+ background: $body-bg-dark;
+}
+
+[data-dark-mode] .dropdown-menu .dropdown-item.active,
+[data-dark-mode] .dropdown-menu .dropdown-item:focus {
+ color: $link-color-dark;
+ background: $body-bg-dark;
+}
+
+[data-dark-mode] .doks-navbar .dropdown-item.current,
+[data-dark-mode] .doks-subnavbar .dropdown-item.current {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23dee2e6' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 1rem top 0.6rem;
+ background-size: 0.75rem 0.75rem;
+}
+
+[data-dark-mode] details {
+ border: 1px solid $border-dark;
+}
+
+[data-dark-mode] summary:hover {
+ background: $body-overlay-dark;
+}
+
+[data-dark-mode] details[open] > summary {
+ border-bottom: 1px solid $border-dark;
+}
+
+[data-dark-mode] details summary::before {
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28222, 226, 230, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+}
+
+[data-dark-mode] #toc a.active {
+ color: $link-color-dark;
+}
+
+[data-dark-mode] .btn-light {
+ color: $link-color-dark;
+ background: $body-overlay-dark;
+ border: 1px solid $body-overlay-dark;
+}
diff --git a/code/frontpage/assets/scss/common/_global.scss b/code/frontpage/assets/scss/common/_global.scss
new file mode 100644
index 0000000..475ca6d
--- /dev/null
+++ b/code/frontpage/assets/scss/common/_global.scss
@@ -0,0 +1,288 @@
+.contributors .content,
+.blog .content,
+.page .content,
+.error404 .content,
+.docs.list .content,
+.tutorial.list .content,
+.showcase.list .content,
+.categories.list .content,
+.tags.list .content {
+ padding-top: 1rem;
+ padding-bottom: 3rem;
+}
+
+.content img {
+ max-width: 100%;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6 {
+ margin: 2rem 0 1rem;
+}
+
+.offcanvas-header {
+ border-bottom: 1px solid $gray-300;
+ padding-top: 1.0625rem;
+ padding-bottom: 0.8125rem;
+}
+
+h5.offcanvas-title {
+ margin: 0;
+}
+
+body.docs {
+ padding-top: 0 !important;
+}
+
+@include media-breakpoint-up(md) {
+ body {
+ font-size: $font-size-md;
+
+ /*
+ padding-top: 4rem !important;
+ */
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ .h1,
+ .h2,
+ .h3,
+ .h4,
+ .h5,
+ .h6 {
+ margin-bottom: 1.125rem;
+ }
+}
+
+.home h1 {
+ /* font-size: calc(1.375rem + 1.5vw); */
+ font-size: calc(1.875rem + 1.5vw);
+}
+
+a:hover,
+a:focus {
+ text-decoration: underline;
+}
+
+a.btn:hover,
+a.btn:focus {
+ text-decoration: none;
+}
+
+.section {
+ padding-top: 5rem;
+ padding-bottom: 5rem;
+}
+
+.section-md {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+}
+
+.section-sm {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+/*
+.section svg {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-top;
+}
+*/
+
+/*
+body {
+ padding-top: 3.5625rem;
+}
+*/
+
+.docs-sidebar {
+ order: 2;
+}
+
+@include media-breakpoint-up(lg) {
+ .docs-sidebar {
+ order: 0;
+ border-right: 1px solid $gray-200;
+ }
+
+ @supports ((position:-webkit-sticky) or (position:sticky)) {
+ .docs-sidebar {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 4rem;
+ z-index: 1000;
+ height: calc(100vh - 4rem);
+ }
+
+ .docs-sidebar-top {
+ top: 0;
+ }
+ }
+}
+
+@include media-breakpoint-up(xl) {
+ .docs-sidebar {
+ flex: 0 1 320px;
+ }
+}
+
+.docs-links {
+ padding-bottom: 5rem;
+}
+
+@include media-breakpoint-up(lg) {
+ @supports ((position: -webkit-sticky) or (position: sticky)) {
+ .docs-links {
+ max-height: calc(100vh - 4rem);
+ overflow-y: scroll;
+ }
+ }
+}
+
+@include media-breakpoint-up(lg) {
+ .docs-links {
+ display: block;
+ width: auto;
+ margin-right: -1.5rem;
+ padding-bottom: 4rem;
+ }
+}
+
+.docs-toc {
+ order: 2;
+}
+
+@supports ((position:-webkit-sticky) or (position:sticky)) {
+ .docs-toc {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 4rem;
+ height: calc(100vh - 4rem);
+ overflow-y: auto;
+ }
+
+ .docs-toc-top {
+ top: 0;
+ }
+}
+
+.docs-content {
+ padding-bottom: 3rem;
+ order: 1;
+}
+
+.docs-navigation {
+ border-top: 1px solid $gray-200;
+ margin-top: 2rem;
+ margin-bottom: 0;
+ padding-top: 2rem;
+}
+
+.docs-navigation a {
+ font-size: $font-size-base * 0.9;
+}
+
+@include media-breakpoint-up(lg) {
+ .docs-navigation {
+ margin-bottom: -1rem;
+ }
+
+ .docs-navigation a {
+ font-size: $font-size-base;
+ }
+}
+
+.navbar a:hover,
+.navbar a:focus {
+ text-decoration: none;
+}
+
+#TableOfContents ul,
+#toc ul {
+ padding-left: 0;
+ list-style: none;
+}
+
+#toc a.active {
+ color: $primary;
+ font-weight: 500;
+}
+
+::selection {
+ background: rgba(212, 53, 159, 0.2);
+}
+
+.bg-dots {
+ background-image: radial-gradient($gray-300 15%, transparent 15%);
+ background-position: 0 0;
+ background-size: 1rem 1rem;
+ -webkit-mask: linear-gradient(to top, #fff, transparent);
+ mask: linear-gradient(to top, #fff, transparent);
+ width: 100%;
+ height: 9rem;
+ margin-top: -10rem;
+ z-index: -1;
+}
+
+.bg-dots-md {
+ margin-top: -11rem;
+}
+
+.bg-dots-lg {
+ margin-top: -12rem;
+}
+
+// https://fossheim.io/writing/posts/css-text-gradient/
+.gradient-text {
+ background-color: $primary;
+ background-image: linear-gradient(90deg, $primary, $blue-300 50%, $pink-500);
+ background-size: 100%;
+ background-repeat: repeat;
+ -webkit-background-clip: text;
+ -moz-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ -moz-text-fill-color: transparent;
+}
+
+.katex {
+ font-size: $font-size-md;
+}
+
+.card-bar {
+ border-top: 4px solid;
+ border-image-source: linear-gradient(90deg, $primary, #8ed6fb 50%, #d32e9d);
+ border-image-slice: 1;
+}
+
+.modal-backdrop {
+ background-color: #fff;
+}
+
+.modal-backdrop.show {
+ opacity: 0.7;
+}
+
+@include media-breakpoint-up(md) {
+ .modal-backdrop.show {
+ opacity: 0;
+ }
+}
diff --git a/code/frontpage/assets/scss/common/_variables.scss b/code/frontpage/assets/scss/common/_variables.scss
new file mode 100644
index 0000000..2737ce0
--- /dev/null
+++ b/code/frontpage/assets/scss/common/_variables.scss
@@ -0,0 +1,190 @@
+// Color system
+
+$white: #fff;
+$gray-100: #f8f9fa;
+$gray-200: #e9ecef;
+$gray-300: #dee2e6;
+$gray-400: #ced4da;
+$gray-500: #adb5bd;
+$gray-600: #6c757d;
+$gray-700: #495057;
+$gray-800: #343a40;
+$gray-900: #212529;
+$black: #000;
+
+$yellow: #ffe000;
+$black: #1d2d35;
+$beige: #fbf7f0;
+
+// $red: #e55235;
+$purple: #5d2f86;
+$brown: #aa9c84;
+
+$blue-300: #8ed6fb;
+$pink-100: #fcfaff;
+$pink-500: #d32e9d;
+
+$primary: $purple;
+
+$color-btn-bg: $pink-500;
+$color-btn-border: darken($pink-500, 5%);
+$color-btn-text: $white;
+
+// Options
+//
+// Quickly modify global styling by enabling or disabling optional features.
+
+$enable-caret: true;
+$enable-rounded: true;
+$enable-shadows: false;
+$enable-gradients: false;
+$enable-transitions: true;
+$enable-reduced-motion: true;
+$enable-smooth-scroll: true;
+$enable-grid-classes: true;
+$enable-button-pointers: true;
+$enable-rfs: true;
+$enable-validation-icons: true;
+$enable-negative-margins: true;
+$enable-deprecation-messages: true;
+$enable-important-utilities: true;
+
+/** Bootstrap navbar fix (https://git.io/fADqW) */
+$navbar-dark-toggler-icon-bg: none;
+$navbar-light-toggler-icon-bg: none;
+
+// Options
+//
+// Quickly modify global styling by enabling or disabling optional features.
+
+// $enable-responsive-font-sizes: true;
+
+// Body
+//
+// Settings for the `<body>` element.
+
+$body-bg: $white;
+$body-color: $black;
+
+// Links
+//
+// Style anchor elements.
+
+$link-color: $primary;
+$link-decoration: none;
+
+// Grid containers
+//
+// Define the maximum width of `.container` for different screen sizes.
+
+$container-max-widths: (
+ sm: 540px,
+ md: 720px,
+ lg: 960px,
+ xl: 1240px,
+ xxl: 1320px
+);
+
+@include _assert-ascending($container-max-widths, "$container-max-widths");
+
+// Grid columns
+//
+// Set the number of columns and specify the width of the gutters.
+
+$grid-columns: 16;
+$grid-gutter-width: 48px;
+$grid-row-columns: 6;
+
+// Components
+//
+// Define common padding and border radius sizes and more.
+
+$border-color: $gray-200;
+
+// Typography
+//
+// Font, line-height, and color for body text, headings, and more.
+
+// stylelint-disable value-keyword-case
+$font-family-sans-serif: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+$font-family-monospace: "Jetbrains Mono", sfmono-regular, menlo, monaco, consolas, "Liberation Mono", "Courier New", monospace;
+$font-family-base: $font-family-sans-serif;
+// stylelint-enable value-keyword-case
+
+$font-size-base: 1rem; // Assumes the browser default, typically `16px`
+$font-size-xl: $font-size-base * 1.375;
+$font-size-lg: $font-size-base * 1.25;
+$font-size-md: $font-size-base * 1.125;
+$font-size-sm: $font-size-base * 0.875;
+
+// $line-height-base: 1.5;
+
+$headings-font-family: null;
+$headings-font-weight: 700;
+
+$lead-font-weight: 400;
+
+// Spacing
+//
+// Control the default styling of most Bootstrap elements by modifying these
+// variables. Mostly focused on spacing.
+// You can add more entries to the $spacers map, should you need more variation.
+
+$spacer: 1rem;
+
+// Navbar
+
+$navbar-padding-y: $spacer / 2;
+$navbar-padding-x: null;
+
+$navbar-nav-link-padding-x: 0.5rem;
+
+$navbar-light-color: $black;
+$navbar-light-hover-color: $primary;
+$navbar-light-active-color: $primary;
+
+// Cards
+
+$card-border-color: $gray-200;
+
+// Alerts
+//
+// Define alert colors, border radius, and padding.
+
+$alert-padding-y: $spacer;
+$alert-padding-x: $spacer * 1.5;
+$alert-margin-bottom: 0;
+$alert-border-radius: 0;
+$alert-link-font-weight: $headings-font-weight;
+$alert-border-width: 0;
+
+$alert-bg-scale: 0;
+$alert-border-scale: 0;
+$alert-color-scale: 0;
+
+// docsearch
+$dropdown-config: (
+ main-color: $purple,
+ layout-type: normal,
+ layout-width: normal,
+ layout-alignment: align,
+ background-color: $white,
+ border-radius: 4,
+ border-width: 1,
+ border-color: $gray-200,
+ box-shadow: none,
+ branding-position: bottom,
+ spacing: normal,
+ include-desc: yes,
+ background-category-header: $white,
+ font-size: normal,
+ header-color: $black,
+ title-color: $black,
+ subtitle-color: $black,
+ text-color: $black,
+ highlight-color: $purple,
+ highlight-opacity: 0.1,
+ highlight-type: underline
+);
+
+$input-btn-focus-width: 0; \ No newline at end of file
diff --git a/code/frontpage/assets/scss/components/_alerts.scss b/code/frontpage/assets/scss/components/_alerts.scss
new file mode 100644
index 0000000..0e4ed83
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_alerts.scss
@@ -0,0 +1,164 @@
+.alert {
+ font-family: $font-family-monospace;
+ font-size: $font-size-sm;
+}
+
+.alert-icon {
+ margin-right: 0.75rem;
+}
+
+.docs main .alert {
+ margin: 2rem -1.5rem;
+}
+
+.alert .alert-link {
+ text-decoration: underline;
+}
+
+.alert-doks {
+ background: $beige;
+ color: $black;
+}
+
+/*
+.alert-light {
+ color: #215888;
+ background: linear-gradient(-45deg, rgb(212, 245, 255), rgb(234, 250, 255), rgb(234, 250, 255), #d3f6ef);
+}
+
+.alert-light .alert-link {
+ color: #215888;
+}
+*/
+
+.alert-white {
+ background-color: rgba(255, 255, 255, 0.95);
+}
+
+.alert-primary {
+ color: $white;
+ background-color: $primary;
+}
+
+.alert a {
+ text-decoration: underline;
+}
+
+.alert-primary .alert-link {
+ color: $white;
+}
+
+/*
+.alert-primary {
+ color: #084298;
+ background-color: #cfe2ff;
+ border-color: #b6d4fe;
+}
+
+.alert-primary .alert-link {
+ color: #06357a;
+}
+*/
+
+.alert-secondary {
+ color: #41464b;
+ background-color: #e2e3e5;
+ border-color: #d3d6d8;
+}
+
+.alert-secondary .alert-link {
+ color: #34383c;
+}
+
+.alert-success {
+ color: #0f5132;
+ background-color: #d1e7dd;
+ border-color: #badbcc;
+}
+
+.alert-success .alert-link {
+ color: #0c4128;
+}
+
+.alert-info {
+ color: #055160;
+ background-color: #cff4fc;
+ border-color: #b6effb;
+}
+
+.alert-info .alert-link {
+ color: #04414d;
+}
+
+.alert-warning {
+ color: #664d03;
+ background-color: #fff3cd;
+ border-color: #ffecb5;
+}
+
+.alert-warning .alert-link {
+ color: #523e02;
+}
+
+.alert-danger {
+ color: #842029;
+ background-color: #f8d7da;
+ border-color: #f5c2c7;
+}
+
+.alert-danger .alert-link {
+ color: #6a1a21;
+}
+
+.alert-light {
+ color: #636464;
+ background-color: #fefefe;
+ border-color: #fdfdfe;
+}
+
+.alert-light .alert-link {
+ color: #4f5050;
+}
+
+.alert-dark {
+ color: #141619;
+ background-color: #d3d3d4;
+ border-color: #bcbebf;
+}
+
+.alert-dark .alert-link {
+ color: #101214;
+}
+
+.alert .alert-link:hover,
+.alert .alert-link:focus {
+ text-decoration: none;
+}
+
+.alert-dismissible .btn-close {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ right: 1rem;
+ z-index: 2;
+ padding: 0.5rem;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-x'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
+ background-size: 1.5rem;
+ filter: invert(1) grayscale(100%) brightness(200%);
+}
+
+@include media-breakpoint-up(md) {
+ .alert-dismissible .btn-close {
+ background-size: 1.25rem;
+ }
+}
+
+[data-global-alert="closed"] #announcement {
+ display: none;
+}
+
+.alert code {
+ background: darken($beige, 5%);
+ color: $black;
+ padding: 0.25rem 0.5rem;
+}
diff --git a/code/frontpage/assets/scss/components/_buttons.scss b/code/frontpage/assets/scss/components/_buttons.scss
new file mode 100644
index 0000000..e923ab3
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_buttons.scss
@@ -0,0 +1,255 @@
+.navbar .btn-link {
+ color: $navbar-light-color;
+ padding: 0.4375rem 0;
+}
+
+#mode {
+ padding: 0.5rem;
+}
+
+.btn-link:focus {
+ outline: 0;
+ box-shadow: none;
+}
+
+#navigation {
+ margin-left: 1.25rem;
+}
+
+@include media-breakpoint-up(lg) {
+ #mode {
+ margin-left: 0.5rem;
+ margin-right: 0.25rem;
+ }
+
+ .navbar .btn-link {
+ padding: 0.5625em 0.25rem 0.5rem 0.125rem;
+ }
+}
+
+.navbar .btn-link:hover {
+ color: $navbar-light-hover-color;
+}
+
+.navbar .btn-link:active {
+ color: $navbar-light-active-color;
+}
+
+body .toggle-dark {
+ display: block;
+}
+
+body .toggle-light {
+ display: none;
+}
+
+[data-dark-mode] body .toggle-light {
+ display: block;
+}
+
+[data-dark-mode] body .toggle-dark {
+ display: none;
+}
+
+pre {
+ position: relative;
+}
+
+@include media-breakpoint-down(md) {
+ .btn-copy {
+ display: none;
+ }
+}
+
+.btn-copy {
+ transition: opacity 0.3s ease-in-out;
+ visibility: hidden !important;
+ position: absolute;
+ right: 0.25rem;
+ top: 0.25rem;
+ z-index: 10;
+ font-family: $font-family-sans-serif;
+ font-size: $font-size-sm;
+ padding: 0.25rem 0.5rem;
+ color: $color-btn-text;
+ background-color: $color-btn-bg;
+ border-color: $color-btn-border;
+}
+
+.btn-copy:hover {
+ color: $color-btn-text;
+ background-color: lighten($color-btn-bg, 5%);
+ border-color: lighten($color-btn-border, 15%);
+}
+
+.btn-copy:focus {
+ color: $color-btn-text;
+ background-color: $color-btn-bg;
+ border-color: lighten($color-btn-border, 15%);
+ box-shadow: none;
+}
+
+.btn-copy:active,
+.btn-copy.active {
+ color: $color-btn-text;
+ background-color: $color-btn-bg;
+ border-color: lighten($color-btn-border, 15%);
+}
+
+.btn-copy:active:focus,
+.btn-copy.active:focus {
+ box-shadow: none;
+}
+
+@include media-breakpoint-up(md) {
+ pre:hover .btn-copy {
+ visibility: visible !important;
+ }
+}
+
+.btn-copy::after {
+ content: "Copy";
+ display: block;
+ color: $color-btn-text;
+}
+
+.btn-copy:hover::after {
+ content: "Copy";
+ display: block;
+ color: $color-btn-text;
+}
+
+.btn-copy:focus::after,
+.btn-copy:active::after {
+ content: "Copied";
+ display: block;
+ color: $color-btn-text;
+}
+
+.collapsible-sidebar {
+ margin: 2.125rem 0;
+}
+
+.btn-toggle {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem 0.25rem 0;
+ font-weight: $headings-font-weight;
+ font-size: $font-size-base;
+ text-transform: uppercase;
+ color: $body-color;
+ background-color: transparent;
+ border: 0;
+}
+
+.btn-toggle:hover,
+.btn-toggle:focus {
+ color: $body-color;
+ background-color: transparent;
+ outline: 0;
+ box-shadow: none;
+}
+
+.btn-toggle::before {
+ width: 1.25em;
+ line-height: 0;
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%2829, 45, 53, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+ transition: transform 0.35s ease;
+ transform-origin: 0.5em 50%;
+ margin-bottom: 0.125rem;
+}
+
+.btn-toggle[aria-expanded="true"] {
+ color: $body-color;
+}
+
+.btn-toggle[aria-expanded="true"]::before {
+ transform: rotate(90deg);
+}
+
+.btn-toggle-nav a {
+ display: inline-flex;
+ padding: 0.1875rem 0.5rem;
+ margin-top: 0.125rem;
+ margin-left: 1.25rem;
+ text-decoration: none;
+}
+
+.btn-toggle-nav a:hover,
+.btn-toggle-nav a:focus {
+ background-color: transparent;
+ color: $link-color;
+}
+
+.btn-toggle-nav a.active {
+ color: $link-color;
+}
+
+.dropdown-menu {
+ /*
+ width: 100%;
+ */
+
+ width: auto;
+}
+
+@include media-breakpoint-up(lg) {
+ .dropdown-menu {
+ width: auto;
+ }
+}
+
+.doks-navbar .dropdown-menu,
+.doks-subnavbar .dropdown-menu {
+ font-size: 0.875rem;
+}
+
+.doks-navbar .dropdown-item.current,
+.doks-subnavbar .dropdown-item.current {
+ font-weight: 600;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23292b2c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 1rem top 0.6rem;
+ background-size: 0.75rem 0.75rem;
+}
+
+.btn-close {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-x'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
+ background-size: 1.5rem;
+}
+
+.offcanvas-header .btn-close {
+ margin-right: 0 !important;
+}
+
+.dropdown-toggle::after {
+ display: none;
+}
+
+.dropdown-caret {
+ margin-left: -0.1875rem;
+ margin-right: -0.3125rem;
+}
+
+.dropdown-menu-main .dropdown-item {
+ color: inherit;
+ font-size: $font-size-base;
+ font-weight: 400;
+ text-decoration: none;
+}
+
+.dropdown-menu-main .dropdown-item:hover {
+ background-color: transparent;
+ color: $primary;
+}
+
+.dropdown-menu-main .dropdown-item.active {
+ color: $primary;
+ font-weight: 400;
+ text-decoration: none;
+ background-color: inherit;
+}
+
+.dropdown-menu-main .dropdown-item.active:hover {
+ background-color: transparent;
+}
diff --git a/code/frontpage/assets/scss/components/_code.scss b/code/frontpage/assets/scss/components/_code.scss
new file mode 100644
index 0000000..84ad2bc
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_code.scss
@@ -0,0 +1,66 @@
+pre,
+code,
+kbd,
+samp {
+ font-family: $font-family-monospace;
+ font-size: $font-size-sm;
+ border-radius: $border-radius;
+}
+
+code {
+ background: $beige;
+ color: $black;
+ padding: 0.25rem 0.5rem;
+}
+
+pre {
+ margin: 2rem 0;
+}
+
+pre code {
+ display: block;
+ overflow-x: auto;
+ line-height: $line-height-base;
+ padding: 1.25rem 1.5rem;
+ tab-size: 4;
+ scrollbar-width: thin;
+ scrollbar-color: transparent transparent;
+}
+
+.hljs {
+ padding: 1.5rem !important;
+}
+
+@include media-breakpoint-down(sm) {
+ pre,
+ code,
+ kbd,
+ samp {
+ border-radius: 0;
+ }
+
+ pre {
+ margin: 2rem -1.5rem;
+ }
+}
+
+pre code::-webkit-scrollbar {
+ height: 5px;
+}
+
+pre code::-webkit-scrollbar-thumb {
+ background: $gray-400;
+}
+
+pre code:hover {
+ scrollbar-width: thin;
+ scrollbar-color: $gray-500 transparent;
+}
+
+pre code::-webkit-scrollbar-thumb:hover {
+ background: $gray-500;
+}
+
+code.language-mermaid {
+ background: none;
+}
diff --git a/code/frontpage/assets/scss/components/_comments.scss b/code/frontpage/assets/scss/components/_comments.scss
new file mode 100644
index 0000000..18f610c
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_comments.scss
@@ -0,0 +1,30 @@
+.comment-list {
+ @extend .list-unstyled;
+}
+
+.comment-list ol {
+ list-style: none;
+}
+
+.comment-form p {
+ @extend .form-group !optional;
+}
+
+.comment-form input[type="text"],
+.comment-form input[type="email"],
+.comment-form input[type="url"],
+.comment-form textarea {
+ @extend .form-control;
+}
+
+.comment-form input[type="submit"] {
+ @extend .btn;
+ @extend .btn-secondary;
+}
+
+blockquote {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+ border-left: 3px solid $gray-300;
+ padding-left: 1rem;
+}
diff --git a/code/frontpage/assets/scss/components/_details.scss b/code/frontpage/assets/scss/components/_details.scss
new file mode 100644
index 0000000..fb719d3
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_details.scss
@@ -0,0 +1,77 @@
+details {
+ display: block;
+ border: 1px solid $gray-200;
+ border-radius: 0.25rem;
+ padding: 0.5rem 1rem 0;
+ margin: 0.5rem 0;
+}
+
+/*
+details summary {
+ &::marker {
+ content: "";
+ }
+}
+*/
+
+summary {
+ list-style: none;
+ display: inline-block;
+ width: calc(100% + 2rem);
+ margin: -0.5rem -1rem 0;
+ padding: 0.5rem 0.75rem;
+}
+
+summary::-webkit-details-marker {
+ display: none;
+}
+
+summary:hover {
+ background: $gray-100;
+}
+
+details summary::before {
+ display: inline-block;
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%2829, 45, 53, 0.75%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+ transition: transform 0.35s ease;
+ transform-origin: center center;
+ margin-right: 0.375rem;
+}
+
+details[open] > summary::before {
+ transform: rotate(90deg);
+}
+
+/*
+details summary > * {
+ display: inline-block;
+}
+*/
+
+details[open] {
+ padding: 0.5rem 1rem;
+}
+
+details[open] > summary {
+ border-bottom: 1px solid $gray-300;
+ margin-bottom: 0.5rem;
+}
+
+details h2,
+details h3,
+details h4 {
+ margin: 1rem 0 0.5rem;
+}
+
+details p:last-child {
+ margin-bottom: 0;
+}
+
+details ul,
+details ol {
+ margin-bottom: 0;
+}
+
+details pre {
+ margin: 0 0 1rem;
+}
diff --git a/code/frontpage/assets/scss/components/_forms.scss b/code/frontpage/assets/scss/components/_forms.scss
new file mode 100644
index 0000000..9732838
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_forms.scss
@@ -0,0 +1,19 @@
+/** Search form */
+.search-form {
+ @extend .form-inline !optional;
+}
+
+.search-form label {
+ @extend .form-group;
+
+ font-weight: normal;
+}
+
+.search-form .search-field {
+ @extend .form-control;
+}
+
+.search-form .search-submit {
+ @extend .btn;
+ @extend .btn-secondary;
+}
diff --git a/code/frontpage/assets/scss/components/_images.scss b/code/frontpage/assets/scss/components/_images.scss
new file mode 100644
index 0000000..efe3d1c
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_images.scss
@@ -0,0 +1,62 @@
+figure {
+ margin: 0 0 1rem;
+ display: inline-block;
+}
+
+figure img {
+ margin-bottom: 0.5rem;
+ line-height: 1;
+ max-width: 100%;
+ height: auto;
+}
+
+figure figcaption {
+ margin: 0.25rem 0 0.75rem;
+ font-size: 0.875em;
+ color: #6c757d;
+}
+
+.figure-caption {
+ margin: 0.25rem 0 0.75rem;
+}
+
+figure.wide {
+ margin: 2rem -1.5rem;
+}
+
+figure.wide .figure-caption {
+ margin: 0.25rem 1.5rem 0.75rem;
+}
+
+@include media-breakpoint-up(md) {
+ figure.wide {
+ margin: 2rem -2.5rem;
+ }
+
+ figure.wide .figure-caption {
+ margin: 0.25rem 2.5rem 0.75rem;
+ }
+}
+
+@include media-breakpoint-up(lg) {
+ figure.wide {
+ margin: 2rem -5rem;
+ }
+
+ figure.wide .figure-caption {
+ margin: 0.25rem 5rem 0.75rem;
+ }
+}
+
+.blur-up {
+ filter: blur(5px);
+}
+
+.blur-up.lazyloaded {
+ filter: unset;
+}
+
+.img-simple {
+ margin-top: 0.375rem;
+ margin-bottom: 1.25rem;
+}
diff --git a/code/frontpage/assets/scss/components/_mermaid.scss b/code/frontpage/assets/scss/components/_mermaid.scss
new file mode 100644
index 0000000..3ff2488
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_mermaid.scss
@@ -0,0 +1,8 @@
+.mermaid {
+ margin: 1.5rem 0;
+ padding: 1.5rem;
+}
+
+.mermaid svg {
+ height: auto;
+}
diff --git a/code/frontpage/assets/scss/components/_search.scss b/code/frontpage/assets/scss/components/_search.scss
new file mode 100644
index 0000000..531121a
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_search.scss
@@ -0,0 +1,91 @@
+.navbar-form {
+ position: relative;
+}
+
+#suggestions {
+ position: absolute;
+ right: 0;
+ margin-top: 0.5rem;
+ width: calc(100vw - 3rem);
+ max-width: calc(400px - 3rem);
+ z-index: $zindex-dropdown;
+
+ @include media-breakpoint-up(md) {
+ right: -2rem;
+ }
+
+ @include media-breakpoint-up(lg) {
+ right: 0;
+ }
+}
+
+#suggestions a,
+.suggestion__no-results {
+ padding: 0.75rem;
+ margin: 0 0.5rem;
+}
+
+#suggestions a {
+ display: block;
+ text-decoration: none;
+}
+
+#suggestions a:focus {
+ background: $gray-100;
+ outline: 0;
+}
+
+#suggestions div:not(:first-child) {
+ border-top: 1px dashed $gray-200;
+}
+
+#suggestions div:first-child {
+ margin-top: 0.5rem;
+}
+
+#suggestions div:last-child {
+ margin-bottom: 0.5rem;
+}
+
+#suggestions a:hover {
+ background: $gray-100;
+}
+
+#suggestions span {
+ display: flex;
+ font-size: $font-size-base;
+}
+
+.suggestion__title {
+ font-weight: $headings-font-weight;
+ color: $black;
+}
+
+.suggestion__description,
+.suggestion__no-results {
+ color: $gray-700;
+}
+
+@include media-breakpoint-up(lg) {
+ #suggestions {
+ width: 31.125rem;
+ max-width: 31.125rem;
+ }
+
+ #suggestions a {
+ display: flex;
+ }
+
+ .suggestion__title {
+ width: 9rem;
+ padding-right: 1rem;
+ border-right: 1px solid $gray-200;
+ display: inline-block;
+ text-align: right;
+ }
+
+ .suggestion__description {
+ width: 19rem;
+ padding-left: 1rem;
+ }
+}
diff --git a/code/frontpage/assets/scss/components/_syntax.scss b/code/frontpage/assets/scss/components/_syntax.scss
new file mode 100644
index 0000000..2be315d
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_syntax.scss
@@ -0,0 +1,62 @@
+/*
+
+Based on Ascetic by (c) Ivan Sagalaev <Maniac@SoftwareManiacs.Org>
+
+*/
+
+.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1.25rem 1.5rem;
+ background: $beige;
+ color: $body-color;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-section,
+.hljs-addition,
+.hljs-attribute,
+.hljs-link {
+ color: $pink-500;
+}
+
+.hljs-comment,
+.hljs-quote,
+.hljs-meta,
+.hljs-deletion {
+ color: #888;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-section,
+.hljs-name,
+.hljs-type,
+.hljs-strong {
+ font-weight: bold;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+[data-dark-mode] body .hljs {
+ background: $body-overlay-dark;
+ color: $body-color-dark;
+}
+
+[data-dark-mode] body .hljs-string,
+[data-dark-mode] body .hljs-variable,
+[data-dark-mode] body .hljs-template-variable,
+[data-dark-mode] body .hljs-symbol,
+[data-dark-mode] body .hljs-bullet,
+[data-dark-mode] body .hljs-section,
+[data-dark-mode] body .hljs-addition,
+[data-dark-mode] body .hljs-attribute,
+[data-dark-mode] body .hljs-link {
+ color: $blue-300;
+}
diff --git a/code/frontpage/assets/scss/components/_tables.scss b/code/frontpage/assets/scss/components/_tables.scss
new file mode 100644
index 0000000..b1f8c2e
--- /dev/null
+++ b/code/frontpage/assets/scss/components/_tables.scss
@@ -0,0 +1,5 @@
+table {
+ @extend .table;
+
+ margin: 3rem 0;
+}
diff --git a/code/frontpage/assets/scss/layouts/_footer.scss b/code/frontpage/assets/scss/layouts/_footer.scss
new file mode 100644
index 0000000..7d21811
--- /dev/null
+++ b/code/frontpage/assets/scss/layouts/_footer.scss
@@ -0,0 +1,20 @@
+.footer {
+ border-top: 1px solid $gray-200;
+ padding-top: 1.125rem;
+ padding-bottom: 1.125rem;
+}
+
+.footer ul {
+ margin-bottom: 0;
+}
+
+.footer li {
+ font-size: $font-size-sm;
+ margin-bottom: 0;
+}
+
+@include media-breakpoint-up(md) {
+ .footer li {
+ font-size: $font-size-base;
+ }
+}
diff --git a/code/frontpage/assets/scss/layouts/_header.scss b/code/frontpage/assets/scss/layouts/_header.scss
new file mode 100644
index 0000000..a0b4a0a
--- /dev/null
+++ b/code/frontpage/assets/scss/layouts/_header.scss
@@ -0,0 +1,493 @@
+.banner .nav li {
+ @extend .nav-item;
+}
+
+.banner .nav a {
+ @extend .nav-link;
+}
+
+.navbar-text {
+ margin-left: 1rem;
+}
+
+.navbar-brand {
+ font-weight: $headings-font-weight;
+}
+
+/*
+.navbar-light .navbar-brand,
+.navbar-light .navbar-brand:hover,
+.navbar-light .navbar-brand:active {
+ color: $body-color;
+}
+
+.navbar-light .navbar-nav .active .nav-link {
+ color: $primary;
+}
+*/
+
+.navbar {
+ z-index: 1000;
+ background-color: rgba(255, 255, 255, 0.95);
+ border-bottom: 1px solid $gray-200;
+
+ /*
+ margin-top: 4px;
+ */
+}
+
+@include media-breakpoint-up(lg) {
+ .navbar {
+ z-index: 1025;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ }
+}
+
+@include media-breakpoint-up(md) {
+ .navbar-brand {
+ font-size: $font-size-xl;
+ }
+
+ .navbar-text {
+ margin-left: 1.25rem;
+ }
+}
+
+.navbar-nav {
+ flex-direction: row;
+}
+
+.nav-item {
+ margin-left: 0;
+}
+
+@include media-breakpoint-up(md) {
+ .nav-item {
+ margin-left: 0.5rem;
+ }
+}
+
+/*
+@include media-breakpoint-down(sm) {
+ .nav-item:first-child {
+ margin-left: 0;
+ }
+}
+*/
+
+@include media-breakpoint-down(md) {
+ .navbar .container {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+}
+
+.break {
+ flex-basis: 100%;
+ height: 0;
+}
+
+button#doks-languages {
+ margin: 0.25rem 0 0;
+
+ @include media-breakpoint-up(lg) {
+ margin: 0.25rem 0.5rem 0 0.25rem;
+ }
+}
+
+button#doks-versions {
+ margin: 0.25rem 0 0;
+
+ @include media-breakpoint-up(lg) {
+ margin: 0.25rem 0.5rem 0 0.25rem;
+ }
+}
+
+.offcanvas .nav-link {
+ color: $body-color;
+}
+
+.doks-subnavbar {
+ background-color: rgba(255, 255, 255, 0.95);
+ border-bottom: 1px solid $gray-200;
+}
+
+.doks-subnavbar .nav-link {
+ padding: 0.5rem 1.5rem 0.5rem 0;
+}
+
+.doks-subnavbar .nav-link:first-child {
+ padding: 0.5rem 1.5rem 0.5rem 0;
+}
+
+.offcanvas .nav-link:hover,
+.offcanvas .nav-link:focus {
+ color: $link-color;
+}
+
+.offcanvas .nav-link.active {
+ color: $link-color;
+}
+
+/*
+.navbar {
+ background-color: rgba(255, 255, 255, 0.95);
+ border-bottom: 1px solid $gray-200;
+ margin-top: 4px;
+}
+*/
+
+.header-bar {
+ border-top: 4px solid;
+ border-image-source: linear-gradient(90deg, $primary, #8ed6fb 50%, #d32e9d);
+ border-image-slice: 1;
+}
+
+.offcanvas .header-bar {
+ margin-bottom: -4px;
+}
+
+.home .navbar {
+ border-bottom: 0;
+}
+
+/*
+.navbar-form {
+ position: relative;
+ margin-top: 0.25rem;
+}
+*/
+
+@include media-breakpoint-up(md) {
+ .navbar-brand {
+ margin-right: 0.75rem !important;
+ }
+
+ .main-nav .nav-item:first-child .nav-link,
+ .social-nav .nav-item:first-child .nav-link {
+ padding-left: 0;
+ }
+
+ .main-nav .nav-item:last-child .nav-link,
+ .social-nav .nav-item:last-child .nav-link {
+ padding-right: 0;
+ }
+
+ .doks-search {
+ max-width: 20rem;
+ margin-top: 0.125rem;
+ margin-bottom: 0.125rem;
+ }
+
+ /*
+ .navbar-form {
+ margin-top: 0;
+ margin-left: 6rem;
+ margin-right: 1.5rem;
+ }
+ */
+}
+
+.form-control.is-search {
+ padding-right: 4rem;
+ border: 1px solid transparent;
+ background: $gray-100;
+
+ @include media-breakpoint-up(md) {
+ width: calc(100% + 2rem);
+ }
+
+ @include media-breakpoint-up(lg) {
+ width: 100%;
+ }
+}
+
+.form-control.is-search:focus {
+ border: 1px solid $primary;
+}
+
+.doks-search::after {
+ position: absolute;
+ top: 0.4625rem;
+ right: 0.5375rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 1.5rem;
+ padding-right: 0.3125rem;
+ padding-left: 0.3125rem;
+ font-size: $font-size-base * 0.75;
+ color: $gray-700;
+ content: "Ctrl + /";
+ border: 1px solid $gray-300;
+ border-radius: 0.25rem;
+
+ @include media-breakpoint-up(md) {
+ right: -1.4625rem;
+ }
+
+ @include media-breakpoint-up(lg) {
+ right: 0.3125rem;
+ }
+}
+
+/*
+@include media-breakpoint-up(lg) {
+ .navbar-form {
+ margin-left: 15rem;
+ }
+}
+
+@include media-breakpoint-up(xl) {
+ .navbar-form {
+ margin-left: 30rem;
+ }
+}
+*/
+
+/*
+.form-control.is-search {
+*/
+
+/*
+ padding-right: calc(1.5em + 0.75rem);
+ */
+
+/*
+ padding-right: 2.5rem;
+ background: $gray-100;
+ border: 0;
+ */
+
+/*
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-search'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+ */
+
+/*
+}
+*/
+
+/*
+.navbar-form::after {
+ position: absolute;
+ top: 0.4625rem;
+ right: 0.5375rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 1.5rem;
+ padding-right: 0.4375rem;
+ padding-left: 0.4375rem;
+ font-size: $font-size-base * 0.75;
+ color: $gray-700;
+ content: "/";
+ border: 1px solid $gray-300;
+ border-radius: 0.25rem;
+}
+*/
+
+/*! purgecss start ignore */
+.algolia-autocomplete {
+ display: flex !important;
+}
+
+.algolia-autocomplete .ds-dropdown-menu {
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+@include media-breakpoint-down(sm) {
+ .algolia-autocomplete .ds-dropdown-menu {
+ max-width: 512px !important;
+ min-width: 312px !important;
+ width: auto !important;
+ }
+
+ .algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column {
+ font-weight: normal;
+ }
+
+ .algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column::after {
+ content: "/";
+ margin-right: 0.25rem;
+ }
+}
+
+.algolia-autocomplete .algolia-docsearch-suggestion--category-header {
+ color: $black;
+}
+
+.algolia-autocomplete .algolia-docsearch-suggestion--title {
+ margin-bottom: 0;
+}
+
+.algolia-autocomplete .algolia-docsearch-suggestion--highlight {
+ padding: 0 0.05em;
+}
+
+.algolia-autocomplete .algolia-docsearch-footer {
+ margin-top: 1rem;
+ margin-right: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+/*! purgecss end ignore */
+
+/*
+ * Source: https://medium.com/creative-technology-concepts-code/responsive-mobile-dropdown-navigation-using-css-only-7218e4498a99
+*/
+
+/* Style the menu icon for the dropdown */
+
+.navbar .menu-icon {
+ cursor: pointer;
+
+ /* display: inline-block; */
+
+ /* float: right; */
+ padding: 1.125rem 0.625rem;
+ margin: 0 0 0 -0.625rem;
+
+ /* position: relative; */
+ user-select: none;
+}
+
+.navbar .menu-icon .navicon {
+ background: $navbar-light-color;
+ display: block;
+ height: 2px;
+ position: relative;
+ transition: background 0.2s ease-out;
+ width: 18px;
+}
+
+.navbar .menu-icon .navicon::before,
+.navbar .menu-icon .navicon::after {
+ background: $navbar-light-color;
+ content: "";
+ display: block;
+ height: 100%;
+ position: absolute;
+ transition: all 0.2s ease-out;
+ width: 100%;
+}
+
+.navbar .menu-icon .navicon::before {
+ top: 5px;
+}
+
+.navbar .menu-icon .navicon::after {
+ top: -5px;
+}
+
+/* Add the icon and menu animations when the checkbox is clicked */
+
+.navbar .menu-btn {
+ display: none;
+}
+
+.navbar .menu-btn:checked ~ .navbar-collapse {
+ display: block;
+ max-height: 100vh;
+}
+
+.navbar .menu-btn:checked ~ .menu-icon .navicon {
+ background: transparent;
+}
+
+.navbar .menu-btn:checked ~ .menu-icon .navicon::before {
+ transform: rotate(-45deg);
+}
+
+.navbar .menu-btn:checked ~ .menu-icon .navicon::after {
+ transform: rotate(45deg);
+}
+
+.navbar .menu-btn:checked ~ .menu-icon:not(.steps) .navicon::before,
+.navbar .menu-btn:checked ~ .menu-icon:not(.steps) .navicon::after {
+ top: 0;
+}
+
+.btn-menu {
+ margin-left: 1rem;
+ border: transparent;
+}
+
+.btn-doks-light {
+ border: transparent;
+}
+
+.btn-menu,
+.doks-sidebar-toggle {
+ padding-right: 0.25rem;
+ padding-left: 0.25rem;
+ margin-right: -0.5rem;
+}
+
+.btn-menu:hover,
+.btn-doks-light:hover,
+.doks-sidebar-toggle:hover {
+ background: $pink-100;
+ border: transparent;
+}
+
+.btn-menu:focus,
+.btn-doks-light:focus,
+.doks-sidebar-toggle:focus,
+.doks-mode-toggle:focus {
+ outline: 0;
+ border: transparent;
+}
+
+.doks-sidebar-toggle .doks-collapse,
+.doks-toc-toggle .doks-collapse {
+ display: none;
+}
+
+.doks-sidebar-toggle:not(.collapsed) .doks-expand,
+.doks-toc-toggle:not(.collapsed) .doks-expand {
+ display: none;
+}
+
+.doks-sidebar-toggle:not(.collapsed) .doks-collapse,
+.doks-toc-toggle:not(.collapsed) .doks-collapse {
+ display: inline-block;
+}
+
+.navbar-light .navbar-brand,
+.navbar-light .navbar-brand:hover,
+.navbar-light .navbar-brand:active {
+ color: $body-color;
+}
+
+.navbar-light .navbar-nav .active .nav-link {
+ color: $primary;
+}
+
+.dropdown-divider {
+ border-top: 1px dashed $gray-200;
+}
+
+.dropdown-item:hover {
+ background: $gray-100;
+}
+
+.dropdown-item:active {
+ color: inherit;
+}
+
+.social-link {
+ padding-left: 0.5rem;
+
+ @include media-breakpoint-up(md) {
+ padding-left: 0;
+ }
+
+ @include media-breakpoint-up(lg) {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+}
diff --git a/code/frontpage/assets/scss/layouts/_pages.scss b/code/frontpage/assets/scss/layouts/_pages.scss
new file mode 100644
index 0000000..405ede9
--- /dev/null
+++ b/code/frontpage/assets/scss/layouts/_pages.scss
@@ -0,0 +1,64 @@
+.docs-content > h2[id]::before,
+.docs-content > h3[id]::before,
+.docs-content > h4[id]::before {
+ display: block;
+ height: 6rem;
+ margin-top: -6rem;
+ content: "";
+}
+
+.anchor {
+ visibility: hidden;
+}
+
+h1:hover a,
+h2:hover a,
+h3:hover a,
+h4:hover a {
+ visibility: visible;
+ text-decoration: none;
+}
+
+.card-list {
+ margin-top: 2.25rem;
+}
+
+.page-footer-meta {
+ margin-top: 3rem;
+}
+
+.edit-page,
+.last-modified {
+ font-size: $font-size-sm;
+ margin-top: 0.25rem;
+ margin-bottom: 0.25rem;
+}
+
+@include media-breakpoint-up(md) {
+ .edit-page,
+ .last-modified {
+ font-size: $font-size-base;
+ margin-top: 0.75rem;
+ margin-bottom: 0.25rem;
+ }
+}
+
+.edit-page svg,
+.last-modified svg {
+ margin-right: 0.25rem;
+ margin-bottom: 0.25rem;
+}
+
+p.meta {
+ margin-top: 0.5rem;
+ font-size: $font-size-base;
+}
+
+.breadcrumb {
+ margin-top: 2.25rem;
+ font-size: $font-size-base;
+}
+
+.page-link:hover {
+ text-decoration: none;
+}
diff --git a/code/frontpage/assets/scss/layouts/_posts.scss b/code/frontpage/assets/scss/layouts/_posts.scss
new file mode 100644
index 0000000..27d316a
--- /dev/null
+++ b/code/frontpage/assets/scss/layouts/_posts.scss
@@ -0,0 +1,57 @@
+.home .card,
+.contributors.list .card,
+.blog.list .card,
+.blog.single .card,
+.categories.list .card,
+.tags.list .card {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ transition: transform 0.3s;
+}
+
+.home .card:hover,
+.contributors.list .card:hover,
+.blog.list .card:hover,
+.blog.single .card:hover,
+.categories.list .card:hover,
+.tags.list .card:hover {
+ transform: scale(1.025);
+}
+
+.contributors.list .card.card-terms:hover,
+.categories.list .card.card-terms:hover,
+.tags.list .card.card-terms:hover {
+ transform: none;
+}
+
+.home .card-body,
+.contributors.list .card-body,
+.blog.list .card-body,
+.blog.single .card-body,
+.categories.list .card-body,
+.tags.list .card-body {
+ padding: 0 2rem 1rem;
+}
+
+.contributors.list .card-terms .card-body,
+.categories.list .card-terms .card-body,
+.tags.list .card-terms .card-body {
+ padding: 1rem;
+}
+
+.blog-header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.blog-footer {
+ text-align: center;
+}
+
+.related-posts {
+ margin-top: 4rem;
+}
+
+h2.section-title {
+ margin-bottom: 1.25rem;
+}
diff --git a/code/frontpage/assets/scss/layouts/_sidebar.scss b/code/frontpage/assets/scss/layouts/_sidebar.scss
new file mode 100644
index 0000000..88959bf
--- /dev/null
+++ b/code/frontpage/assets/scss/layouts/_sidebar.scss
@@ -0,0 +1,116 @@
+.docs-links,
+.docs-toc {
+ scrollbar-width: thin;
+ scrollbar-color: $white $white;
+}
+
+.docs-links::-webkit-scrollbar,
+.docs-toc::-webkit-scrollbar {
+ width: 5px;
+}
+
+.docs-links::-webkit-scrollbar-track,
+.docs-toc::-webkit-scrollbar-track {
+ background: $white;
+}
+
+.docs-links::-webkit-scrollbar-thumb,
+.docs-toc::-webkit-scrollbar-thumb {
+ background: $white;
+}
+
+.docs-links:hover,
+.docs-toc:hover {
+ scrollbar-width: thin;
+ scrollbar-color: $gray-200 $white;
+}
+
+.docs-links:hover::-webkit-scrollbar-thumb,
+.docs-toc:hover::-webkit-scrollbar-thumb {
+ background: $gray-200;
+}
+
+.docs-links::-webkit-scrollbar-thumb:hover,
+.docs-toc::-webkit-scrollbar-thumb:hover {
+ background: $gray-200;
+}
+
+.docs-links h3,
+.page-links h3 {
+ text-transform: uppercase;
+ font-size: $font-size-base;
+ margin: 1.25rem 0 0.5rem;
+ padding: 1.5rem 0 0;
+}
+
+@include media-breakpoint-up(lg) {
+ .docs-links h3,
+ .page-links h3 {
+ margin: 1.125rem 1.5rem 0.75rem 0;
+ padding: 1.375rem 0 0;
+ }
+}
+
+.docs-links h3:not(:first-child) {
+ border-top: 1px solid $gray-200;
+}
+
+a.docs-link {
+ color: $body-color;
+ display: block;
+ padding: 0.125rem 0;
+ font-size: $font-size-base;
+}
+
+.page-links li {
+ margin-top: 0.375rem;
+ padding-top: 0.375rem;
+}
+
+.page-links li ul li {
+ border-top: none;
+ padding-left: 1rem;
+ margin-top: 0.125rem;
+ padding-top: 0.125rem;
+}
+
+.page-links li:not(:first-child) {
+ border-top: 1px dashed $gray-200;
+}
+
+.page-links a {
+ color: $body-color;
+ display: block;
+ padding: 0.125rem 0;
+ font-size: $font-size-base * 0.9375;
+}
+
+.docs-link:hover,
+.docs-link.active,
+.page-links a:hover {
+ text-decoration: none;
+ color: $link-color;
+}
+
+.nav-link.active,
+.dropdown-menu-main .dropdown-item.active,
+.docs-link.active {
+ font-weight: 500;
+}
+
+.docs-links h3.sidebar-link,
+.page-links h3.sidebar-link {
+ text-transform: none;
+ font-size: $font-size-md;
+ font-weight: normal;
+}
+
+.docs-links h3.sidebar-link a,
+.page-links h3.sidebar-link a {
+ color: $body-color;
+}
+
+.docs-links h3.sidebar-link a:hover,
+.page-links h3.sidebar-link a:hover {
+ text-decoration: underline;
+}
diff --git a/code/frontpage/assets/scss/vendor/.gitkeep b/code/frontpage/assets/scss/vendor/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/assets/scss/vendor/.gitkeep
diff --git a/code/frontpage/babel.config.js b/code/frontpage/babel.config.js
new file mode 100644
index 0000000..ce9c9de
--- /dev/null
+++ b/code/frontpage/babel.config.js
@@ -0,0 +1,17 @@
+module.exports = {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: {
+ browsers: [
+ // Best practice: https://github.com/babel/babel/issues/7789
+ '>=1%',
+ 'not ie 11',
+ 'not op_mini all'
+ ]
+ }
+ }
+ ]
+ ]
+}; \ No newline at end of file
diff --git a/code/frontpage/config/_default/config.toml b/code/frontpage/config/_default/config.toml
new file mode 100644
index 0000000..20881f8
--- /dev/null
+++ b/code/frontpage/config/_default/config.toml
@@ -0,0 +1,91 @@
+baseurl = "https://greatoffice.life"
+canonifyURLs = false
+disableAliases = true
+disableHugoGeneratorInject = true
+enableEmoji = false
+enableGitInfo = false
+enableRobotsTXT = true
+paginate = 7
+rssLimit = 10
+
+# Multilingual
+defaultContentLanguage = "en"
+disableLanguages = ["de", "nl"]
+# defaultContentLanguageInSubdir = true
+
+# add redirects/headers
+[outputs]
+home = ["HTML", "RSS"]
+section = ["HTML", "RSS", "SITEMAP"]
+
+# add output format for section sitemap.xml
+[outputFormats.SITEMAP]
+mediaType = "application/xml"
+baseName = "sitemap"
+isHTML = false
+isPlainText = true
+noUgly = true
+rel = "sitemap"
+
+[caches]
+ [caches.getjson]
+ dir = ":cacheDir/:project"
+ maxAge = "10s"
+
+[sitemap]
+ changefreq = "weekly"
+ filename = "sitemap.xml"
+ priority = 0.5
+
+[taxonomies]
+ contributor = "contributors"
+ category = "categories"
+ tag = "tags"
+
+[permalinks]
+ blog = "/blog/:title/"
+# docs = "/docs/1.0/:sections[1:]/:title/"
+
+[minify.tdewolff.html]
+ keepWhitespace = false
+
+[related]
+ threshold = 80
+ includeNewer = true
+ toLower = false
+ [[related.indices]]
+ name = "categories"
+ weight = 100
+ [[related.indices]]
+ name = "tags"
+ weight = 80
+ [[related.indices]]
+ name = "date"
+ weight = 10
+
+[module]
+ [module.hugoVersion]
+ extended = true
+ min = "0.80.0"
+ max = ""
+ [[module.mounts]]
+ source = "assets"
+ target = "assets"
+ [[module.mounts]]
+ source = "static"
+ target = "static"
+ [[module.mounts]]
+ source = "layouts"
+ target = "layouts"
+ [[module.mounts]]
+ source = "node_modules/flexsearch"
+ target = "assets/js/vendor/flexsearch"
+ [[module.mounts]]
+ source = "node_modules/katex"
+ target = "assets/js/vendor/katex"
+ [[module.mounts]]
+ source = "node_modules/mermaid"
+ target = "assets/js/vendor/mermaid"
+ [[module.mounts]]
+ source = "node_modules/@hyas/images/layouts"
+ target = "layouts"
diff --git a/code/frontpage/config/_default/languages.toml b/code/frontpage/config/_default/languages.toml
new file mode 100644
index 0000000..3866f83
--- /dev/null
+++ b/code/frontpage/config/_default/languages.toml
@@ -0,0 +1,7 @@
+[en]
+ languageName = "English"
+ contentDir = "content/en"
+ weight = 10
+ [en.params]
+ languageISO = "EN"
+ languageTag = "en-US" \ No newline at end of file
diff --git a/code/frontpage/config/_default/markup.toml b/code/frontpage/config/_default/markup.toml
new file mode 100644
index 0000000..2880e30
--- /dev/null
+++ b/code/frontpage/config/_default/markup.toml
@@ -0,0 +1,29 @@
+defaultMarkdownHandler = "goldmark"
+
+[goldmark]
+ [goldmark.extensions]
+ linkify = false
+ [goldmark.parser]
+ autoHeadingID = true
+ autoHeadingIDType = "github"
+ [goldmark.parser.attribute]
+ block = true
+ title = true
+ [goldmark.renderer]
+ unsafe = true
+
+[highlight]
+ codeFences = false
+ guessSyntax = false
+ hl_Lines = ""
+ lineNoStart = 1
+ lineNos = false
+ lineNumbersInTable = true
+ noClasses = false
+ style = "dracula"
+ tabWidth = 4
+
+[tableOfContents]
+ endLevel = 3
+ ordered = false
+ startLevel = 2
diff --git a/code/frontpage/config/_default/menus/menus.en.toml b/code/frontpage/config/_default/menus/menus.en.toml
new file mode 100644
index 0000000..59e8fbb
--- /dev/null
+++ b/code/frontpage/config/_default/menus/menus.en.toml
@@ -0,0 +1,24 @@
+[[main]]
+ name = "Docs"
+ url = "/docs/"
+ weight = 10
+
+[[main]]
+ name = "Contact"
+ url = "/contact/"
+ weight = 20
+
+[[main]]
+ name = "Open app"
+ url = "https://stage.greatoffice.app"
+ weight = 30
+
+[[footer]]
+ name = "Privacy"
+ url = "/privacy/"
+ weight = 10
+
+[[footer]]
+ name = "Terms of service"
+ url = "/terms/"
+ weight = 10 \ No newline at end of file
diff --git a/code/frontpage/config/_default/params.toml b/code/frontpage/config/_default/params.toml
new file mode 100644
index 0000000..476fe7e
--- /dev/null
+++ b/code/frontpage/config/_default/params.toml
@@ -0,0 +1,98 @@
+# Meta Data for SEO
+
+## Homepage
+title = "Greatoffice"
+titleSeparator = "-"
+titleAddition = "Great"
+description = "Greatoffice is a set of tools you can use to manage your business."
+
+## Documentation
+# docsVersion = "0.3"
+
+## Open Graph
+#images = [""]
+ogLocale = "en_US"
+domainTLD = "greatoffice.life"
+titleHome = "Greatoffice"
+
+## Twitter Cards
+#twitterSite = "@greatoffice"
+#twitterCreator = "@greatoffice"
+
+## JSON-LD
+# schemaType = "Person"
+schemaType = "Organization"
+schemaName = "Greatoffice"
+schemaAuthor = "Greatoffice"
+#schemaAuthorTwitter = "https://twitter.com/greatoffice"
+#schemaAuthorLinkedIn = "https://www.linkedin.com/in/ivar-lovlie/"
+#schemaAuthorGitHub = "https://github.com/h-enk"
+schemaLocale = "en-GB"
+#schemaLogo = ""
+#schemaLogoWidth = 512
+#schemaLogoHeight = 512
+#schemaImage = ""
+#schemaImageWidth = 1280
+#schemaImageHeight = 640
+#schemaTwitter = "https://twitter.com/"
+#schemaLinkedIn = ""
+#schemaGitHub = "https://github.com/h-enk/doks"
+#schemaSection = "blog"
+
+## Sitelinks Search Box
+siteLinksSearchBox = true
+
+## Chrome Browser
+themeColor = "#fff"
+
+# Images
+quality = 85
+bgColor = "#fff"
+landscapePhotoWidths = [900, 800, 700, 600, 500]
+portraitPhotoWidths = [800, 700, 600, 500]
+lqipWidth = "20x"
+smallLimit = "300"
+
+# Images
+imageResponsive = true
+imageConvertTo = "webp"
+imageImageSizes = ["480", "720", "1080", "1280", "1600", "2048"]
+singleSize = false
+imageAddClass = "img-fluid lazyload"
+
+### Image template
+#defaultImage = "default-image.png" # put in `./assets/images/`
+#fillImage = "1270x740 Center" # normalize image size
+
+# Footer
+footer = "This site is created with <a class=\"text-muted\" href=\"https://gohugo.io/\">Hugo</a> and <a class=\"text-muted\" href=\"https://getdoks.org/\">Doks</a>"
+
+# Feed
+copyRight = "Copyright (c) I2R"
+
+[sections]
+sectionNav = ["docs"]
+
+[options]
+lazySizes = true
+clipBoard = true
+instantPage = true
+flexSearch = false
+searchSectionsShow = []
+searchSectionsIndex = []
+darkMode = true
+bootStrapJs = true
+breadCrumb = true
+highLight = false
+kaTex = false
+multilingualMode = false
+docsVersioning = false
+fullWidth = true
+navbarSticky = true
+toTopButton = true
+scrollSpy = false # experimental; needs Bootstrap >= 5.2.0-beta1
+
+[menu]
+[menu.section]
+auto = true
+collapsibleSidebar = true
diff --git a/code/frontpage/config/next/config.toml b/code/frontpage/config/next/config.toml
new file mode 100644
index 0000000..9c5e90d
--- /dev/null
+++ b/code/frontpage/config/next/config.toml
@@ -0,0 +1 @@
+canonifyURLs = false
diff --git a/code/frontpage/config/postcss.config.js b/code/frontpage/config/postcss.config.js
new file mode 100644
index 0000000..3e0eff8
--- /dev/null
+++ b/code/frontpage/config/postcss.config.js
@@ -0,0 +1,44 @@
+const autoprefixer = require('autoprefixer');
+const purgecss = require('@fullhuman/postcss-purgecss');
+const whitelister = require('purgecss-whitelister');
+
+module.exports = {
+ plugins: [
+ autoprefixer(),
+ purgecss({
+ content: [
+ './layouts/**/*.html',
+ './content/**/*.md',
+ ],
+ safelist: [
+ 'lazyloaded',
+ 'table',
+ 'thead',
+ 'tbody',
+ 'tr',
+ 'th',
+ 'td',
+ 'h5',
+ 'alert-link',
+ 'container-xxl',
+ 'container-fluid',
+ 'offcanvas-backdrop',
+ 'img-fluid',
+ 'lazyload',
+ 'blur-up',
+ 'figcaption',
+ ...whitelister([
+ './assets/scss/components/_alerts.scss',
+ './assets/scss/components/_buttons.scss',
+ './assets/scss/components/_code.scss',
+ './assets/scss/components/_diagrams.scss',
+ './assets/scss/components/_syntax.scss',
+ './assets/scss/components/_search.scss',
+ './assets/scss/common/_dark.scss',
+ './node_modules/bootstrap/scss/_dropdown.scss',
+ './node_modules/katex/dist/katex.css',
+ ]),
+ ],
+ }),
+ ],
+}
diff --git a/code/frontpage/config/production/config.toml b/code/frontpage/config/production/config.toml
new file mode 100644
index 0000000..2dcfde1
--- /dev/null
+++ b/code/frontpage/config/production/config.toml
@@ -0,0 +1,2 @@
+canonifyURLs = false
+baseURL = "https://greatoffice.life" \ No newline at end of file
diff --git a/code/frontpage/content/en/_index.md b/code/frontpage/content/en/_index.md
new file mode 100644
index 0000000..36095c4
--- /dev/null
+++ b/code/frontpage/content/en/_index.md
@@ -0,0 +1,9 @@
+---
+title : "Greatoffice"
+description: "Greatoffice is a set of useful tools you can use to manage your business."
+lead: "Greatoffice is a set of useful tools you can use to manage your business."
+date: 2020-10-06T08:47:36+00:00
+lastmod: 2020-10-06T08:47:36+00:00
+draft: false
+images: []
+---
diff --git a/code/frontpage/content/en/blog/_index.md b/code/frontpage/content/en/blog/_index.md
new file mode 100644
index 0000000..d8bd151
--- /dev/null
+++ b/code/frontpage/content/en/blog/_index.md
@@ -0,0 +1,8 @@
+---
+title: "Blog"
+description: "The Greatoffice Blog."
+date: 2020-10-06T08:49:55+00:00
+lastmod: 2020-10-06T08:49:55+00:00
+draft: false
+images: []
+---
diff --git a/code/frontpage/content/en/blog/hi-greatoffice/index.md b/code/frontpage/content/en/blog/hi-greatoffice/index.md
new file mode 100644
index 0000000..49950eb
--- /dev/null
+++ b/code/frontpage/content/en/blog/hi-greatoffice/index.md
@@ -0,0 +1,17 @@
+---
+title: "Say hello to Greatoffice"
+description: "Introducing Greatoffice, not a Hugo theme helping you build modern documentation websites that are secure, fast, and SEO-ready — by default."
+excerpt: "Introducing Greatoffice, not a Hugo theme helping you build modern documentation websites that are secure, fast, and SEO-ready — by default."
+date: 2020-11-04T09:19:42+01:00
+lastmod: 2022-12-11T09:19:42+01:00
+draft: false
+weight: 50
+images: []
+categories: ["News"]
+tags: ["security", "performance", "legitness"]
+contributors: ["Ivar Løvlie"]
+pinned: false
+homepage: false
+---
+
+Introducing Greatoffice, not a Hugo theme helping you build modern documentation websites that are secure, fast, and SEO-ready — by default.
diff --git a/code/frontpage/content/en/contact/index.md b/code/frontpage/content/en/contact/index.md
new file mode 100644
index 0000000..6d7ba0a
--- /dev/null
+++ b/code/frontpage/content/en/contact/index.md
@@ -0,0 +1,11 @@
+---
+title : "Contact"
+description: "How to contact us."
+lead: ""
+date: 2020-10-06T08:48:23+00:00
+lastmod: 2020-10-06T08:48:23+00:00
+draft: false
+images: []
+---
+
+Contact us via email: {{< email user="ahuman" domain="greatoffice.life" >}} \ No newline at end of file
diff --git a/code/frontpage/content/en/docs/index.md b/code/frontpage/content/en/docs/index.md
new file mode 100644
index 0000000..ce52359
--- /dev/null
+++ b/code/frontpage/content/en/docs/index.md
@@ -0,0 +1,11 @@
+---
+title : "Docs"
+description: "How to use Greatoffice."
+lead: ""
+date: 2020-10-06T08:48:23+00:00
+lastmod: 2020-10-06T08:48:23+00:00
+draft: false
+images: []
+---
+
+Here will be docs. \ No newline at end of file
diff --git a/code/frontpage/content/en/privacy/index.md b/code/frontpage/content/en/privacy/index.md
new file mode 100644
index 0000000..680ccd1
--- /dev/null
+++ b/code/frontpage/content/en/privacy/index.md
@@ -0,0 +1,33 @@
+---
+title: "Privacy Policy"
+description: "We don't care about your privacy, at all."
+date: 2020-08-27T19:23:18+02:00
+lastmod: 2020-08-27T19:23:18+02:00
+draft: false
+---
+
+__TLDR__: We will never sell or share your data with anyone unless it is you, us or any number of our third party services providers.
+
+## .greatoffice.life
+
+- No personal information is collected.
+- No information is stored in the browser.
+- No information is shared with, sent to or sold to third-parties.
+- No information is shared with advertising companies.
+- No information is mined and harvested for personal and behavioral trends.
+- No information is monetized.
+- Your IP address is collected and stored for up to thirty days.
+
+## .greatoffice.app
+
+- No information is shared with advertising companies.
+- No information is mined and harvested for personal and behavioral trends.
+- No information is monetized.
+- We store application data such as your preffered language and theme in your browser, we also store an indentifier that tells our systems who you are logged in as.
+- When we send you an email, your email address and provided name is shared with our email provider (Postmark), read their privacy policy [here](https://postmarkapp.com/privacy-policy).
+
+## Contact us
+
+[Contact us]({{<relref "contact/index.md">}}) if you have any questions.
+
+Effective Date: _11th December 2022_
diff --git a/code/frontpage/content/en/terms/index.md b/code/frontpage/content/en/terms/index.md
new file mode 100644
index 0000000..6d8e650
--- /dev/null
+++ b/code/frontpage/content/en/terms/index.md
@@ -0,0 +1,18 @@
+---
+title: "Terms of service"
+description: ""
+date: 2020-08-27T19:23:18+02:00
+lastmod: 2020-08-27T19:23:18+02:00
+draft: false
+images: []
+---
+
+__TLDR__: You can use our services if you do as outlined below.
+
+It is currently fritt vilt.
+
+## Contact us
+
+[Contact us]({{<relref "contact/index.md">}}) if you have any questions.
+
+Effective Date: _11th December 2022_
diff --git a/code/frontpage/data/docs-versions.yml b/code/frontpage/data/docs-versions.yml
new file mode 100644
index 0000000..d617d09
--- /dev/null
+++ b/code/frontpage/data/docs-versions.yml
@@ -0,0 +1,5 @@
+- group: v1.x
+ baseurl: "/docs"
+ description: "Every minor and patch release from v1 is listed below. Last update was v1.0.0."
+ versions:
+ - v: "1.0"
diff --git a/code/frontpage/i18n/de.yaml b/code/frontpage/i18n/de.yaml
new file mode 100644
index 0000000..d1f125e
--- /dev/null
+++ b/code/frontpage/i18n/de.yaml
@@ -0,0 +1,5 @@
+- id: get-started
+ translation: "Loslegen"
+
+- id: on-this-page
+ translation: "Auf dieser Seite"
diff --git a/code/frontpage/i18n/en.yaml b/code/frontpage/i18n/en.yaml
new file mode 100644
index 0000000..05ff248
--- /dev/null
+++ b/code/frontpage/i18n/en.yaml
@@ -0,0 +1,17 @@
+- id: get-started
+ translation: "Get Started"
+
+- id: on-this-page
+ translation: "On this page"
+
+- id: search-text
+ translation: "Search docs..."
+
+- id: 404-title
+ translation: "Page not found :("
+
+- id: 404-text
+ translation: "The page you are looking for doesn't exist or has been moved."
+
+- id: browse
+ translation: "Browse"
diff --git a/code/frontpage/i18n/nl.yaml b/code/frontpage/i18n/nl.yaml
new file mode 100644
index 0000000..2899eda
--- /dev/null
+++ b/code/frontpage/i18n/nl.yaml
@@ -0,0 +1,17 @@
+- id: get-started
+ translation: "Aan de slag"
+
+- id: on-this-page
+ translation: "Op deze pagina"
+
+- id: search-text
+ translation: "Zoeken..."
+
+- id: 404-title
+ translation: "Pagina niet gevonden :("
+
+- id: 404-text
+ translation: "De gezochte pagina bestaat niet of deze is verplaatst."
+
+- id: browse
+ translation: "Browse"
diff --git a/code/frontpage/layouts/404.html b/code/frontpage/layouts/404.html
new file mode 100644
index 0000000..76d447a
--- /dev/null
+++ b/code/frontpage/layouts/404.html
@@ -0,0 +1,10 @@
+{{ define "main" }}
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-10 col-xl-8">
+ <article>
+ <h1 class="text-center">{{ i18n "404-title" }}</h1>
+ <p class="text-center">{{ i18n "404-text" }}</p>
+ </article>
+ </div>
+</div>
+{{ end }} \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/_markup/render-heading.html b/code/frontpage/layouts/_default/_markup/render-heading.html
new file mode 100644
index 0000000..8abeac6
--- /dev/null
+++ b/code/frontpage/layouts/_default/_markup/render-heading.html
@@ -0,0 +1 @@
+<h{{ .Level }} id="{{ .Anchor | safeURL }}">{{ .Text | safeHTML }} <a href="#{{ .Anchor | safeURL }}" class="anchor" aria-hidden="true">#</a></h{{ .Level }}>
diff --git a/code/frontpage/layouts/_default/baseof.html b/code/frontpage/layouts/_default/baseof.html
new file mode 100644
index 0000000..155ff84
--- /dev/null
+++ b/code/frontpage/layouts/_default/baseof.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang="{{ .Site.Params.languageTag | default "en-US" }}">
+ {{ partial "head/head.html" . }}
+ {{ if eq .Kind "home" -}}
+ {{ .Scratch.Set "class" "home" -}}
+ {{ else if eq .Kind "404" -}}
+ {{ .Scratch.Set "class" "error404" -}}
+ {{ else if eq .Kind "page" -}}
+ {{ .Scratch.Set "class" .Type -}}
+ {{ .Scratch.Add "class" " single" -}}
+ {{ else -}}
+ {{ .Scratch.Set "class" .Type -}}
+ {{ .Scratch.Add "class" " list" -}}
+ {{ end -}}
+ <body class="{{ .Scratch.Get "class" }}"{{ if eq .Site.Params.options.scrollSpy true }} data-bs-spy="scroll" data-bs-target="#toc" data-bs-root-margin="0px 0px -90%" data-bs-smooth-scroll="true" tabindex="0"{{ end }}>
+ {{ partial "header/header.html" . }}
+ <div class="wrap container-{{ if .Site.Params.options.fullWidth }}fluid{{ else }}xxl{{ end }}" role="document">
+ <div class="content">
+ {{ block "main" . }}{{ end }}
+ </div>
+ </div>
+ {{ block "sidebar-prefooter" . }}{{ end }}
+ {{ block "sidebar-footer" . }}{{ end }}
+ {{ partial "footer/footer.html" . }}
+ {{ partial "footer/script-footer.html" . }}
+ {{ if eq .Site.Params.options.toTopButton true -}}
+ <div class="d-flex fixed-bottom pb-4 pb-lg-5 pe-4 pe-lg-5">
+ <a id="toTop" href="#" class="btn btn-outline-primary rounded-circle ms-auto p-2"><span class="visually-hidden">Top</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg></a>
+ </div>
+ {{ end }}
+ </body>
+</html> \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/index.js b/code/frontpage/layouts/_default/index.js
new file mode 100644
index 0000000..9f764f9
--- /dev/null
+++ b/code/frontpage/layouts/_default/index.js
@@ -0,0 +1,10 @@
+var docs = [
+{{ range $index, $page := (where .Site.Pages "Section" "docs") -}}
+ {
+ id: {{ $index }},
+ title: "{{ .Title }}",
+ description: "{{ .Params.description }}",
+ href: "{{ .URL | relURL }}"
+ },
+{{ end -}}
+]; \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/index.json b/code/frontpage/layouts/_default/index.json
new file mode 100644
index 0000000..6842871
--- /dev/null
+++ b/code/frontpage/layouts/_default/index.json
@@ -0,0 +1,5 @@
+{{- $.Scratch.Add "index" slice -}}
+{{- range .Site.RegularPages -}}
+ {{- $.Scratch.Add "index" (dict "title" .Title "description" .Params.description "contents" .Plain "RelPermalink" .RelPermalink) -}}
+{{- end -}}
+{{- $.Scratch.Get "index" | jsonify -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/list.html b/code/frontpage/layouts/_default/list.html
new file mode 100644
index 0000000..455b2af
--- /dev/null
+++ b/code/frontpage/layouts/_default/list.html
@@ -0,0 +1,31 @@
+{{ define "main" }}
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-9">
+ <h1 class="text-center">{{ .Title }}</h1>
+ {{ with .Content -}}<div class="text-center">{{ . }}</div>{{ end -}}
+ </div>
+</div>
+<div class="row row-cols-1 row-cols-lg-2 g-lg-5">
+ {{ $paginator := .Paginate (.Data.Pages) -}}
+ {{ range $paginator.Pages -}}
+ <div class="col">
+ <div class="card">
+ {{- .Scratch.Set "fillImage" "1270x620 Center" -}}
+ <div class="card-body">
+ <article>
+ <h2 class="h3"><a class="stretched-link text-body" href="{{ .RelPermalink }}">{{ .Params.title }}</a></h2>
+ <p>{{ .Params.excerpt | safeHTML }}</p>
+ {{ partial "main/blog-meta.html" . -}}
+ </article>
+ </div>
+ </div>
+ </div>
+ {{ end -}}
+</div>
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-9">
+ {{ $.Scratch.Set "paginator" true }}
+ {{ template "_internal/pagination.html" . }}
+ </div>
+</div>
+{{ end }} \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/section.sitemap.xml b/code/frontpage/layouts/_default/section.sitemap.xml
new file mode 100644
index 0000000..701951d
--- /dev/null
+++ b/code/frontpage/layouts/_default/section.sitemap.xml
@@ -0,0 +1,46 @@
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML -}}
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
+ {{ range $i, $e := .Data.Pages -}}
+ {{ if ne .Params.sitemap_exclude true }}
+ <url>
+ <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
+ <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
+ <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
+ <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{ end -}}
+ {{ end -}}
+ {{ range .Sections -}}
+ {{ range $i, $e := .Data.Pages -}}
+ {{ if ne .Params.sitemap_exclude true -}}
+ <url>
+ <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
+ <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
+ <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
+ <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{ end -}}
+ {{ end -}}
+ {{ end -}}
+</urlset> \ No newline at end of file
diff --git a/code/frontpage/layouts/_default/single.html b/code/frontpage/layouts/_default/single.html
new file mode 100644
index 0000000..2f8fc99
--- /dev/null
+++ b/code/frontpage/layouts/_default/single.html
@@ -0,0 +1,10 @@
+{{ define "main" }}
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-10 col-xl-8">
+ <article>
+ <h1>{{ .Title }}</h1>
+ {{ .Content }}
+ </article>
+ </div>
+</div>
+{{ end }}
diff --git a/code/frontpage/layouts/_default/terms.html b/code/frontpage/layouts/_default/terms.html
new file mode 100644
index 0000000..73fdeb8
--- /dev/null
+++ b/code/frontpage/layouts/_default/terms.html
@@ -0,0 +1,20 @@
+{{ define "main" }}
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-10 col-xl-8">
+ <h1 class="text-center">{{ .Title }}</h1>
+ <div class="text-center">{{ .Content }}</div>
+ <div class="card-list">
+ {{ range .Paginator.Pages }}
+ <div class="card card-terms my-3">
+ <div class="card-body">
+ <article>
+ <a class="stretched-link" href="{{ .RelPermalink }}">{{ .Params.title | title }} &rarr;</a>
+ </article>
+ </div>
+ </div>
+ {{ end }}
+ </div>
+ {{ template "_internal/pagination.html" . }}
+ </div>
+</div>
+{{ end }}
diff --git a/code/frontpage/layouts/_default/versions.html b/code/frontpage/layouts/_default/versions.html
new file mode 100644
index 0000000..3101b82
--- /dev/null
+++ b/code/frontpage/layouts/_default/versions.html
@@ -0,0 +1,27 @@
+{{ define "main" }}
+ <article>
+ <h1>{{ .Title }}</h1>
+ <p class="lead">{{ .Params.lead | safeHTML }}</p>
+ {{ .Content }}
+ <div class="row">
+ {{ range $release := sort (index $.Site.Data "docs-versions") "group" "desc" -}}
+ <div class="col-md-8 col-lg-4 col-xl mb-4">
+ <h2>{{ $release.group }}</h2>
+ <p>{{ $release.description }}</p>
+ {{ $versions := sort $release.versions "v" "desc" -}}
+ {{ range $i, $version := $versions -}}
+ {{ $len := len $versions -}}
+ {{ if (eq $i 0) }}<div class="list-group">{{ end }}
+ <a class="list-group-item list-group-item-action py-2 text-primary{{ if (eq $version.v $.Site.Params.docsVersion) }} d-flex justify-content-between align-items-center{{ end }}" href="{{ $release.baseurl }}/{{ $version.v }}/">
+ {{ $version.v }}
+ {{ if (eq $version.v $.Site.Params.docsVersion) -}}
+ <span class="badge bg-primary">Latest</span>
+ {{ end -}}
+ </a>
+ {{ if (eq (add $i 1) $len) }}</div>{{ end }}
+ {{ end -}}
+ </div>
+ {{ end -}}
+ </div>
+ </article>
+{{ end }}
diff --git a/code/frontpage/layouts/blog/single.html b/code/frontpage/layouts/blog/single.html
new file mode 100644
index 0000000..7e6e4d2
--- /dev/null
+++ b/code/frontpage/layouts/blog/single.html
@@ -0,0 +1,53 @@
+{{ define "main" }}
+<article>
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-10">
+ <div class="blog-header">
+ <h1>{{ .Title }}</h1>
+ {{ partial "main/blog-meta.html" . }}
+ </div>
+ </div>
+ <div class="col-md-12 col-lg-9">
+ {{ .Content }}
+ {{ if .Params.tags -}}
+ <div class="mt-4">
+ {{ range $index, $tag := .Params.tags -}}
+ <a class="btn btn-light" href="{{ "/tags/" | absURL }}{{ . | urlize }}/" role="button">{{ . }}</a>
+ {{ end -}}
+ </div>
+ {{ end -}}
+ </div>
+</div>
+</article>
+
+{{ $related := .Site.RegularPages.Related . | first 3 -}}
+{{ with $related -}}
+<div class="related-posts">
+<div class="row justify-content-center">
+ <div class="col">
+ <h2 class="section-title">Related posts</h2>
+ </div>
+</div>
+<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-lg-5">
+ {{ range . -}}
+ <div class="col">
+ <div class="card">
+ {{- .Scratch.Set "fillImageCard" "1270x620 Center" -}}
+ {{ partial "content/card-image.html" . }}
+ <div class="card-body">
+ <article>
+ <h2 class="h3"><a class="stretched-link text-body" href="{{ .RelPermalink }}">{{ .Params.title }}</a></h2>
+ <p>{{ .Params.excerpt | safeHTML }}</p>
+ {{ partial "main/blog-meta.html" . -}}
+ </article>
+ </div>
+ </div>
+ </div>
+ {{ end -}}
+</div>
+</div>
+{{ end -}}
+
+{{ end }}
+
+
diff --git a/code/frontpage/layouts/docs/list.html b/code/frontpage/layouts/docs/list.html
new file mode 100644
index 0000000..4775e0c
--- /dev/null
+++ b/code/frontpage/layouts/docs/list.html
@@ -0,0 +1,22 @@
+{{ define "main" }}
+<div class="row justify-content-center">
+ <div class="col-md-12 col-lg-10 col-xl-8">
+ <article>
+ <h1 class="text-center">{{ if eq .CurrentSection .FirstSection }}{{ .Section | humanize }}{{ else }}{{ .Title }}{{ end }}</h1>
+ <div class="text-center">{{ .Content }}</div>
+ <div class="card-list">
+ {{ $currentSection := .CurrentSection }}
+ {{ range where .Site.RegularPages.ByTitle "Section" .Section }}
+ {{ if in (.RelPermalink | string) $currentSection.RelPermalink }}
+ <div class="card my-3">
+ <div class="card-body">
+ <a class="stretched-link" href="{{ .RelPermalink }}">{{ .Params.title | title }} &rarr;</a>
+ </div>
+ </div>
+ {{ end }}
+ {{ end }}
+ </div>
+ </article>
+ </div>
+</div>
+{{ end }} \ No newline at end of file
diff --git a/code/frontpage/layouts/docs/single.html b/code/frontpage/layouts/docs/single.html
new file mode 100644
index 0000000..3f88e71
--- /dev/null
+++ b/code/frontpage/layouts/docs/single.html
@@ -0,0 +1,53 @@
+{{ define "main" }}
+ <div class="row flex-xl-nowrap">
+ <div class="col-lg-5 col-xl-4 docs-sidebar{{ if ne .Site.Params.options.navbarSticky true }} docs-sidebar-top{{ end }} d-none d-lg-block">
+ <nav {{ if eq .Site.Params.menu.section.collapsibleSidebar false }}id="sidebar-default" {{ end }}class="docs-links" aria-label="Main navigation">
+ {{ partial "sidebar/docs-menu.html" . }}
+ </nav>
+ </div>
+ {{ if ne .Params.toc false -}}
+ <nav class="docs-toc{{ if ne .Site.Params.options.navbarSticky true }} docs-toc-top{{ end }} d-none d-xl-block col-xl-3" aria-label="Secondary navigation">
+ {{ partial "sidebar/docs-toc.html" . }}
+ </nav>
+ {{ end -}}
+ {{ if .Params.toc -}}
+ <main class="docs-content col-lg-11 col-xl{{ if eq .Site.Params.options.fullWidth false }}-9{{ end }}">
+ {{ else -}}
+ <main class="docs-content col-lg-11 col-xl-9 mx-xl-auto">
+ {{ end -}}
+ {{ if .Site.Params.options.breadCrumb -}}
+ <!-- https://discourse.gohugo.io/t/breadcrumb-navigation-for-highly-nested-content/27359/6 -->
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ {{ partial "main/breadcrumb" . -}}
+ <li class="breadcrumb-item active" aria-current="page">{{ .Title }}</li>
+ </ol>
+ </nav>
+ {{ end }}
+ <h1>{{ .Title }}</h1>
+ <p class="lead">{{ .Params.lead | safeHTML }}</p>
+ {{ if ne .Params.toc false -}}
+ <nav class="d-xl-none" aria-label="Quaternary navigation">
+ {{ partial "sidebar/docs-toc.html" . }}
+ </nav>
+ {{ end -}}
+ {{ .Content }}
+ <div class="page-footer-meta d-flex flex-column flex-md-row justify-content-between">
+ {{ if .Site.Params.lastMod -}}
+ {{ partial "main/last-modified.html" . }}
+ {{ end -}}
+ {{ if .Site.Params.editPage -}}
+ {{ partial "main/edit-page.html" . }}
+ {{ end -}}
+ </div>
+ {{ partial "main/docs-navigation.html" . }}
+ <!--
+ {{ if not .Site.Params.options.collapsibleSidebar -}}
+ {{ partial "main/docs-navigation.html" . }}
+ {{ else -}}
+ <div class="my-n3"></div>
+ {{ end -}}
+ -->
+ </main>
+ </div>
+{{ end }}
diff --git a/code/frontpage/layouts/index.headers b/code/frontpage/layouts/index.headers
new file mode 100644
index 0000000..a44c93a
--- /dev/null
+++ b/code/frontpage/layouts/index.headers
@@ -0,0 +1,10 @@
+/*
+ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
+ X-Content-Type-Options: nosniff
+ X-XSS-Protection: 1; mode=block
+ Content-Security-Policy: default-src 'self'; frame-ancestors https://jamstackthemes.dev; manifest-src 'self' https://*.netlify.app; connect-src 'self' https://*.netlify.app; font-src 'self' https://*.netlify.app; img-src 'self' https://*.netlify.app data: https://i.giphy.com; script-src 'self' https://*.netlify.app 'sha512-RGGByJUOP98hE4wFZM78RM/3MijWJs0Tm0DbfrFhCDCXKXfDx60fii+syp5iMs3UcNX/1H4zJNgmqSejfhHrYw==' 'sha512-RBYr6Ld4w1yVqaACrgrBLQfPgGhj/1jyacA74WxJ1KM6KVcSWymwrdDwb3HDcdpwiNJ5yssot1He0U9vXoQVlg==' 'sha256-aWZ3y/RxbBYKHXH0z8+8ljrHG1mSBvyzSfxSMjBSaXk=' 'sha256-vOgyKS2vkH4n5TxBJpeh9SgzrE6LVGsAeOAvEST6oCc='; style-src 'self' https://*.netlify.app 'unsafe-inline'
+ X-Frame-Options: SAMEORIGIN
+ Referrer-Policy: strict-origin
+ Feature-Policy: geolocation 'self'
+ Cache-Control: public, max-age=31536000
+ Access-Control-Allow-Origin: *
diff --git a/code/frontpage/layouts/index.html b/code/frontpage/layouts/index.html
new file mode 100644
index 0000000..ca44125
--- /dev/null
+++ b/code/frontpage/layouts/index.html
@@ -0,0 +1,68 @@
+{{ define "main" }}
+<section class="section container-fluid mt-n3 pb-3">
+ <div class="row">
+ <div class="col-lg-12">
+ <h1 class="mt-0">{{ .Title }}</h1>
+ </div>
+ <div class="col-lg-9 col-xl-8">
+ <p class="lead">{{ .Params.lead | safeHTML }}</p>
+ <p class="meta">GPLv3 Licensed.
+ <a href="https://git.ivar.systems/greatoffice/tree/COPYING">
+ License
+ </a>
+ -
+ <a href="https://git.ivar.systems/greatoffice/about">
+ Source
+ </a>
+ </p>
+ </div>
+ </div>
+</section>
+{{ end }}
+
+{{ define "sidebar-prefooter" }}
+{{ if eq $.Site.Language.LanguageName "English" }}
+<section class="section section-sm">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-5">
+ <h2 class="h4">Projects</h2>
+ <p>Manage your projects with time tracking, planning and ecomnomics all built-in.</p>
+ </div>
+ <div class="col-lg-5">
+ <h2 class="h4">Task management</h2>
+ <p>Manage your tasks across projects and personal objectives.</p>
+ </div>
+ <div class="col-lg-5">
+ <h2 class="h4">Support tickets</h2>
+ <p>Follow-up your customers with built-in support for email, chat and phone support channels.</p>
+ </div>
+ <div class="col-lg-5">
+ <h2 class="h4">Wiki</h2>
+ <p>Manage your companys' global, project and personal documentation needs with a lightweight and
+ complete wiki.</p>
+ </div>
+ <div class="col-lg-5">
+ <h2 class="h4">Transparency</h2>
+ <p>Practise transparency with your customers by giving them a dashboard to get relevant information</p>
+ </div>
+ <div class="col-lg-5">
+ <h2 class="h4">Integrate</h2>
+ <p>We expose our own production apis for public use, giving you unrestricted integration possibilities
+ and reliability.</p>
+ </div>
+ </div>
+ </div>
+</section>
+{{ end }}
+{{ end }}
+
+{{ define "sidebar-footer" }}
+<section class="section section-sm container-fluid">
+ <div class="row">
+ <div class="col-lg-9">
+ {{- .Content -}}
+ </div>
+ </div>
+</section>
+{{ end }} \ No newline at end of file
diff --git a/code/frontpage/layouts/index.redirects b/code/frontpage/layouts/index.redirects
new file mode 100644
index 0000000..12b9350
--- /dev/null
+++ b/code/frontpage/layouts/index.redirects
@@ -0,0 +1,13 @@
+{{- range $p := .Site.Pages -}}
+{{- range .Aliases }}
+{{ . }} {{ $p.RelPermalink }}
+{{- end }}
+{{- end }}
+
+# /docs/1.0/prologue/ /docs/1.0/prologue/introduction/
+# /docs/1.0/help/ /docs/1.0/help/how-to-update/
+# /docs/1.0/ /docs/1.0/prologue/introduction/
+# /docs/ /docs/1.0/prologue/introduction/
+#
+# /docs/0.1/* https://v0-1-0--doks-versioning-poc.netlify.app/docs/0.1/:splat 200
+# /docs/0.2/* https://v0-2-0--doks-versioning-poc.netlify.app/docs/0.2/:splat 200
diff --git a/code/frontpage/layouts/partials/content/card-image.html b/code/frontpage/layouts/partials/content/card-image.html
new file mode 100644
index 0000000..51a5e51
--- /dev/null
+++ b/code/frontpage/layouts/partials/content/card-image.html
@@ -0,0 +1,17 @@
+{{ $fillImage := .Scratch.Get "fillImageCard" }}
+{{ if not $fillImage -}}
+{{ $fillImage = site.Params.fillImage }}
+{{ end -}}
+
+{{ $image := .Resources.GetMatch (printf "**%s" (index .Params.images 0)) }}
+{{ if not $image -}}
+{{ $image = resources.Get (printf "%s%s" "images/" site.Params.defaultImage) }}
+{{ end -}}
+
+{{ $webp := printf "%s%s" $fillImage " webp" }}
+{{/* {{ $image = $image.Resize $webp}} */}}
+
+{{ $lqip := $image.Resize site.Params.lqipWidth -}}
+
+<img class="card-img-top img-fluid lazyload blur-up" src="{{ $lqip.Permalink }}" data-src="{{ $image.Permalink }}"
+ width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}"> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/content/figure.html b/code/frontpage/layouts/partials/content/figure.html
new file mode 100644
index 0000000..fbb0bde
--- /dev/null
+++ b/code/frontpage/layouts/partials/content/figure.html
@@ -0,0 +1,37 @@
+{{ $fillImage := .Scratch.Get "fillImage" }}
+{{ if not $fillImage -}}
+ {{ $fillImage = site.Params.fillImage }}
+{{ end -}}
+
+{{ $image := .Resources.GetMatch (printf "**%s" (index .Params.images 0)) }}
+{{ if not $image -}}
+ {{ $image = resources.Get (printf "%s%s" "images/" site.Params.defaultImage) }}
+{{ end -}}
+
+{{ $image = $image.Fill $fillImage }}
+{{ $lqip := $image.Resize site.Params.lqipWidth -}}
+
+{{ $imgSrc := "" -}}
+{{ $imgSrcSet := slice -}}
+
+{{ $widths := site.Params.landscapePhotoWidths -}}
+{{ if gt $image.Height $image.Width -}}
+ {{ $widths = site.Params.portraitPhotoWidths -}}
+{{ end -}}
+
+{{ range $widths -}}
+ {{ $srcUrl := (printf "%dx" . | $image.Resize).Permalink -}}
+ {{ if eq $imgSrc "" -}}{{ $imgSrc = $srcUrl -}}{{ end -}}
+ {{ $imgSrcSet = $imgSrcSet | append (printf "%s %dw" $srcUrl .) -}}
+{{ end -}}
+{{ $imgSrcSet = (delimit $imgSrcSet ",") -}}
+
+{{ if gt $image.Width site.Params.smallLimit -}}
+ <figure class="figure">
+ <img class="figure-img img-fluid lazyload blur-up" data-sizes="auto" src="{{ $lqip.Permalink }}" data-srcset="{{ $imgSrcSet }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}">
+ <noscript><img class="figure-img img-fluid" sizes="100vw" srcset="{{ $imgSrcSet }}" src="{{ $image.Permalink }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}"></noscript>
+ <!-- {{ with .Title }}<figcaption class="figure-caption">{{ . | safeHTML }}</figcaption>{{ end -}} -->
+ </figure>
+{{ else -}}
+ <img class="img-fluid lazyload blur-up" src="{{ $lqip.Permalink }}" data-src="{{ $image.Permalink }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}">
+{{ end -}}
diff --git a/code/frontpage/layouts/partials/content/image.html b/code/frontpage/layouts/partials/content/image.html
new file mode 100644
index 0000000..2772531
--- /dev/null
+++ b/code/frontpage/layouts/partials/content/image.html
@@ -0,0 +1,32 @@
+{{ $image := .Resources.GetMatch (printf "**%s" (index .Params.images 0)) }}
+{{ if not $image -}}
+ {{ $image = resources.Get (printf "%s%s" "images/" site.Params.defaultImage) }}
+{{ end -}}
+
+{{ $image = $image.Fill site.Params.fillImage }}
+{{ $lqip := $image.Resize site.Params.lqipWidth -}}
+
+{{ $imgSrc := "" -}}
+{{ $imgSrcSet := slice -}}
+
+{{ $widths := site.Params.landscapePhotoWidths -}}
+{{ if gt $image.Height $image.Width -}}
+ {{ $widths = site.Params.portraitPhotoWidths -}}
+{{ end -}}
+
+{{ range $widths -}}
+ {{ $srcUrl := (printf "%dx" . | $image.Resize).Permalink -}}
+ {{ if eq $imgSrc "" -}}{{ $imgSrc = $srcUrl -}}{{ end -}}
+ {{ $imgSrcSet = $imgSrcSet | append (printf "%s %dw" $srcUrl .) -}}
+{{ end -}}
+{{ $imgSrcSet = (delimit $imgSrcSet ",") -}}
+
+{{ if gt $image.Width site.Params.smallLimit -}}
+ <figure class="figure">
+ <img class="figure-img img-fluid lazyload blur-up" data-sizes="auto" src="{{ $lqip.Permalink }}" data-srcset="{{ $imgSrcSet }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}">
+ <noscript><img class="figure-img img-fluid" sizes="100vw" srcset="{{ $imgSrcSet }}" src="{{ $image.Permalink }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}"></noscript>
+ <!-- {{ with .Title }}<figcaption class="figure-caption">{{ . | safeHTML }}</figcaption>{{ end -}} -->
+ </figure>
+{{ else -}}
+ <img class="img-fluid lazyload blur-up" src="{{ $lqip.Permalink }}" data-src="{{ $image.Permalink }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ .Title }}">
+{{ end -}}
diff --git a/code/frontpage/layouts/partials/footer/footer.html b/code/frontpage/layouts/partials/footer/footer.html
new file mode 100644
index 0000000..ceb5fae
--- /dev/null
+++ b/code/frontpage/layouts/partials/footer/footer.html
@@ -0,0 +1,18 @@
+<footer class="footer text-muted">
+ <div class="container-{{ if .Site.Params.options.fullWidth }}fluid{{ else }}xxl{{ end }}">
+ <div class="row">
+ <div class="col-lg-8 order-last order-lg-first">
+ <ul class="list-inline">
+ <li class="list-inline-item">{{ .Site.Params.footer | safeHTML }}</li>
+ </ul>
+ </div>
+ <div class="col-lg-8 order-first order-lg-last text-lg-end">
+ <ul class="list-inline">
+ {{ range .Site.Menus.footer -}}
+ <li class="list-inline-item"><a href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ </ul>
+ </div>
+ </div>
+ </div>
+</footer> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/footer/script-footer.html b/code/frontpage/layouts/partials/footer/script-footer.html
new file mode 100644
index 0000000..8799730
--- /dev/null
+++ b/code/frontpage/layouts/partials/footer/script-footer.html
@@ -0,0 +1,119 @@
+{{ $indexTemplate := resources.Get "js/index.js" -}}
+{{ $index := $indexTemplate | resources.ExecuteAsTemplate "index.js" . -}}
+
+{{ $bs := resources.Get "js/bootstrap.js" -}}
+{{ $bs := $bs | js.Build -}}
+
+{{ $highlight := resources.Get "js/highlight.js" -}}
+{{ $highlight := $highlight | js.Build -}}
+
+{{ $katex := resources.Get "js/vendor/katex/dist/katex.js" -}}
+{{ $katexAutoRender := resources.Get "js/vendor/katex/dist/contrib/auto-render.js" -}}
+
+{{ $mermaid := resources.Get "js/mermaid.js" | js.Build -}}
+
+{{ $app := resources.Get "js/app.js" -}}
+
+{{ $slice := slice $app -}}
+
+{{ if .Site.Params.options.lazySizes -}}
+ {{ $lazySizes := resources.Get "js/lazysizes.js" -}}
+ {{ $lazySizes := $lazySizes | js.Build -}}
+ {{ $slice = $slice | append $lazySizes -}}
+{{ end -}}
+
+{{ if .Site.Params.options.clipBoard -}}
+ {{ $clipBoard := resources.Get "js/clipboard.js" -}}
+ {{ $clipBoard := $clipBoard | js.Build -}}
+ {{ $slice = $slice | append $clipBoard -}}
+{{ end -}}
+
+{{ if .Site.Params.options.instantPage -}}
+ {{ $instantPage := resources.Get "js/instant.page.js" -}}
+ {{ $instantPage := $instantPage | js.Build -}}
+ {{ $slice = $slice | append $instantPage -}}
+{{ end -}}
+
+{{ $showFlexSearch := .Site.Params.options.flexSearch }}
+
+{{ if $showFlexSearch -}}
+ {{ $flexSearch := resources.Get "js/vendor/flexsearch/dist/flexsearch.bundle.js" -}}
+ {{ $slice = $slice | append $flexSearch -}}
+ {{ if and (isset .Site.Params.options "searchsectionsshow") (not (eq .Site.Params.options.searchSectionsShow "ALL")) -}}
+ {{ $showFlexSearch = or (eq (len .Site.Params.options.searchSectionsShow) 0) (in .Site.Params.options.searchSectionsShow .Section) (and .IsHome (in .Site.Params.options.searchSectionsShow "HomePage")) -}}
+ {{ end -}}
+{{ end -}}
+
+{{ if .Site.Params.options.darkMode -}}
+ {{ $darkMode := resources.Get "js/darkmode.js" -}}
+ {{ $darkMode := $darkMode | js.Build -}}
+ {{ $slice = $slice | append $darkMode -}}
+{{ end -}}
+
+{{ if and (.Site.Params.alert) (.Site.Params.alertDismissable) -}}
+ {{ $alert := resources.Get "js/alert.js" -}}
+ {{ $alert := $alert | js.Build -}}
+ {{ $slice = $slice | append $alert -}}
+{{ end -}}
+
+{{ if .Site.Params.options.kaTex -}}
+ {{ $katexConfig := resources.Get "js/katex.js" -}}
+ {{ $katexConfig := $katexConfig | js.Build -}}
+ {{ $slice = $slice | append $katexConfig -}}
+{{ end -}}
+
+{{ $scrollLock := resources.Get "js/scroll-lock.js" | js.Build -}}
+{{ $slice = $slice | append $scrollLock -}}
+
+{{ if .Site.Params.options.toTopButton -}}
+ {{ $toTopButton := resources.Get "js/to-top.js" -}}
+ {{ $toTopButton := $toTopButton | js.Build -}}
+ {{ $slice = $slice | append $toTopButton -}}
+{{ end -}}
+
+{{ $js := $slice | resources.Concat "main.js" -}}
+
+{{ if eq (hugo.Environment) "development" -}}
+ {{ if .Site.Params.options.bootStrapJs -}}
+ <script src="{{ $bs.RelPermalink }}" defer></script>
+ {{ end -}}
+ {{ if .Site.Params.options.highLight -}}
+ <script src="{{ $highlight.RelPermalink }}" defer></script>
+ {{ end -}}
+ {{ if .Site.Params.options.kaTex -}}
+ <script src="{{ $katex.RelPermalink }}" defer></script>
+ <script src="{{ $katexAutoRender.RelPermalink }}" onload="renderMathInElement(document.body);" defer></script>
+ {{ end -}}
+ <script src="{{ $js.RelPermalink }}" defer></script>
+ {{ with .Params.mermaid -}}
+ <script src="{{ $mermaid.RelPermalink }}" defer></script>
+ {{ end -}}
+ {{ if $showFlexSearch -}}
+ <script src="{{ $index.RelPermalink }}" defer></script>
+ {{ end -}}
+{{ else -}}
+ {{ $js := $js | minify | fingerprint "sha512" -}}
+ {{ $index := $index | minify | fingerprint "sha512" -}}
+ {{ $bs := $bs | minify | fingerprint "sha512" -}}
+ {{ $highlight := $highlight | minify | fingerprint "sha512" -}}
+ {{ $katex := $katex | minify | fingerprint "sha512" -}}
+ {{ $katexAutoRender := $katexAutoRender | minify | fingerprint "sha512" -}}
+ {{ $mermaid := $mermaid | minify | fingerprint "sha512" -}}
+ {{ if .Site.Params.options.bootStrapJs -}}
+ <script src="{{ $bs.RelPermalink }}" integrity="{{ $bs.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ end -}}
+ {{ if .Site.Params.options.highLight -}}
+ <script src="{{ $highlight.RelPermalink }}" integrity="{{ $highlight.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ end -}}
+ {{ if .Site.Params.options.kaTex -}}
+ <script src="{{ $katex.RelPermalink }}" integrity="{{ $katex.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ <script src="{{ $katexAutoRender.RelPermalink }}" integrity="{{ $katexAutoRender.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ end -}}
+ <script src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ with .Params.mermaid -}}
+ <script src="{{ $mermaid.RelPermalink }}" integrity="{{ $mermaid.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ end -}}
+ {{ if $showFlexSearch -}}
+ <script src="{{ $index.Permalink }}" integrity="{{ $index.Data.Integrity }}" crossorigin="anonymous" defer></script>
+ {{ end -}}
+{{ end -}}
diff --git a/code/frontpage/layouts/partials/head/custom-head.html b/code/frontpage/layouts/partials/head/custom-head.html
new file mode 100644
index 0000000..0c59d7f
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/custom-head.html
@@ -0,0 +1 @@
+<!-- Custom head -->
diff --git a/code/frontpage/layouts/partials/head/favicons.html b/code/frontpage/layouts/partials/head/favicons.html
new file mode 100644
index 0000000..93c5ed5
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/favicons.html
@@ -0,0 +1,9 @@
+<meta name="theme-color" content="{{ $.Site.Params.themeColor }}">
+<link rel="icon" href="{{ "favicon.ico" | absURL }}" sizes="any">
+{{ if os.FileExists "static/favicon.svg" -}}
+ <link rel="icon" type="image/svg+xml" href="{{ "favicon.svg" | absURL }}">
+{{ end -}}
+<link rel="apple-touch-icon" sizes="180x180" href="{{ "apple-touch-icon.png" | absURL }}">
+<link rel="icon" type="image/png" sizes="32x32" href="{{ "favicon-32x32.png" | absURL }}">
+<link rel="icon" type="image/png" sizes="16x16" href="{{ "favicon-16x16.png" | absURL }}">
+<link rel="manifest" crossorigin="use-credentials" href="{{ "site.webmanifest" | absURL }}">
diff --git a/code/frontpage/layouts/partials/head/head.html b/code/frontpage/layouts/partials/head/head.html
new file mode 100644
index 0000000..12e2a8c
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/head.html
@@ -0,0 +1,11 @@
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ {{ block "head/resource-hints" . }}{{ partial "head/resource-hints.html" . }}{{ end }}
+ {{ block "head/script-header" . }}{{ partial "head/script-header.html" . }}{{ end }}
+ {{ block "head/stylesheet" . }}{{ partial "head/stylesheet.html" . }}{{ end }}
+ {{ block "head/seo" . }}{{ partial "head/seo.html" . }}{{ end }}
+ {{ block "head/favicons" . }}{{ partial "head/favicons.html" . }}{{ end }}
+ {{ block "head/custom-head" . }}{{ partial "head/custom-head.html" . }}{{ end }}
+</head>
diff --git a/code/frontpage/layouts/partials/head/opengraph.html b/code/frontpage/layouts/partials/head/opengraph.html
new file mode 100644
index 0000000..6127e82
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/opengraph.html
@@ -0,0 +1,69 @@
+<meta property="og:locale" content="{{ .Site.Params.ogLocale }}">
+<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
+<meta property="og:title" content="{{ .Title }}">
+<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}">
+{{ if $.Scratch.Get "paginator" -}}
+ {{ $paginator := .Paginate (where .Site.RegularPages.ByDate.Reverse "Section" "blog" ) -}}
+ <meta property="og:url" content="{{ .Paginator.URL | absURL }}">
+{{ else -}}
+ <meta property="og:url" content="{{ .Permalink }}">
+{{ end -}}
+{{ with .Site.Params.title -}}
+ <meta property="og:site_name" content="{{ . }}">
+{{ end -}}
+
+{{ $iso8601 := "2006-01-02T15:04:05-07:00" -}}
+{{ if .IsPage -}}
+ {{ if not .PublishDate.IsZero -}}
+ <meta property="article:published_time" {{ .PublishDate.Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{ else if not .Date.IsZero -}}
+ <meta property="article:published_time" {{ .Date.Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{ end -}}
+ {{ if not .Lastmod.IsZero -}}
+ <meta property="article:modified_time" {{ .Lastmod.Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{ end -}}
+{{ else -}}
+ {{ if not .Date.IsZero -}}
+ <meta property="og:updated_time" {{ .Lastmod.Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
+ {{ end -}}
+{{ end -}}
+
+{{ if eq .Kind "home" -}}
+ {{ .Scratch.Set "title" .Site.Params.titleHome -}}
+{{ else -}}
+ {{ .Scratch.Set "title" .Title -}}
+{{ end -}}
+
+{{ with $.Params.images -}}
+ {{ range first 6 . -}}
+ <meta property="og:image" content="{{ $.Permalink }}{{ . }}">
+ {{ end -}}
+{{ else -}}
+ {{ $images := $.Resources.ByType "image" -}}
+ {{ $featured := $images.GetMatch "*feature*" -}}
+ {{ if not $featured -}}
+ {{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}
+ {{ end -}}
+ {{ with $featured -}}
+ <meta property="og:image" content="{{ $featured.Permalink }}"/>
+ {{ else -}}
+ {{ with $.Site.Params.images -}}
+ <meta property="og:image" content="{{ index . 0 | absURL }}"/>
+ <meta property="og:image:alt" content="{{ $.Site.Params.title }}">
+ {{ end -}}
+ {{ end -}}
+{{ end -}}
+
+{{ with $.Site.Params.images -}}
+ {{ $.Scratch.Set "primaryImage" (index . 0 | absURL) -}}
+{{ end -}}
+
+{{ with .Params.audio -}}
+ <meta property="og:audio" content="{{ . | absURL }}">
+{{ end -}}
+
+{{ with .Params.videos -}}
+ {{ range . -}}
+ <meta property="og:video" content="{{ . | absURL }}">
+ {{ end -}}
+{{ end -}}
diff --git a/code/frontpage/layouts/partials/head/resource-hints.html b/code/frontpage/layouts/partials/head/resource-hints.html
new file mode 100644
index 0000000..6ebcdeb
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/resource-hints.html
@@ -0,0 +1,4 @@
+{{ if .Site.Params.options.kaTex -}}
+<link rel="preload" as="font" href="{{ " fonts/KaTeX_Main-Regular.woff2" | absURL }}" type="font/woff2" crossorigin>
+<link rel="preload" as="font" href="{{ " fonts/KaTeX_Math-Italic.woff2" | absURL }}" type="font/woff2" crossorigin>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/head/script-header.html b/code/frontpage/layouts/partials/head/script-header.html
new file mode 100644
index 0000000..38e5b5b
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/script-header.html
@@ -0,0 +1,8 @@
+{{ if .Site.Params.options.darkMode -}}
+ {{ $darkModeInit := resources.Get "js/darkmode-init.js" | js.Build | minify -}}
+ <script>{{ $darkModeInit.Content | safeJS }}</script>
+{{ end -}}
+{{- if and (.Site.Params.alert) (.Site.Params.alertDismissable) -}}
+ {{ $alertInit := resources.Get "js/alert-init.js" | js.Build | minify -}}
+ <script>{{ $alertInit.Content | safeJS }}</script>
+{{- end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/head/seo.html b/code/frontpage/layouts/partials/head/seo.html
new file mode 100644
index 0000000..ac31d72
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/seo.html
@@ -0,0 +1,48 @@
+{{ if eq .Kind "404" -}}
+ <meta name="robots" content="noindex, follow">
+{{ else -}}
+ {{ with .Params.robots -}}
+ <meta name="robots" content="{{ . }}">
+ {{ else -}}
+ <meta name="robots" content="index, follow">
+ <meta name="googlebot" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
+ <meta name="bingbot" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
+ {{ end -}}
+{{ end -}}
+
+{{ if .IsHome -}}
+ <title>{{ .Site.Params.title }} {{ .Site.Params.titleSeparator }} {{ .Site.Params.titleAddition }}</title>
+{{ else -}}
+ <title>{{ .Title }} {{ .Site.Params.titleSeparator }} {{ .Site.Params.title }}</title>
+{{ end -}}
+
+{{ with .Description -}}
+ <meta name="description" content="{{ . }}">
+{{ else -}}
+ {{ with .Summary | plainify -}}
+ <meta name="description" content="{{ . }}">
+ {{ else -}}
+ <meta name="description" content="{{ .Site.Params.description }}">
+ {{ end -}}
+{{ end -}}
+
+{{ if $.Scratch.Get "paginator" }}
+ <link rel="canonical" href="{{ .Paginator.URL | absURL }}">
+ {{ if .Paginator.HasPrev -}}
+ <link rel="prev" href="{{ .Paginator.Prev.URL | absURL }}">
+ {{ end -}}
+ {{ if .Paginator.HasNext -}}
+ <link rel="next" href="{{ .Paginator.Next.URL | absURL }}">
+ {{ end -}}
+{{ else -}}
+ <link rel="canonical" href="{{ .Permalink }}">
+{{ end -}}
+
+{{ partial "head/opengraph.html" . }}
+{{ partial "head/twitter_cards.html" . }}
+
+{{ range .AlternativeOutputFormats -}}
+ <link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
+{{ end -}}
+
+{{ partial "head/structured-data.html" . }}
diff --git a/code/frontpage/layouts/partials/head/structured-data.html b/code/frontpage/layouts/partials/head/structured-data.html
new file mode 100644
index 0000000..1f153a3
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/structured-data.html
@@ -0,0 +1,210 @@
+{{ $baseURL := "/" | absURL -}}
+
+{{ $dot := . -}}
+{{ $dot.Scratch.Set "path" "" -}}
+{{ $dot.Scratch.Set "breadcrumb" slice -}}
+
+{{ $url := replace .Permalink ( printf "%s" .Site.BaseURL) "" -}}
+{{ $.Scratch.Add "path" .Site.BaseURL -}}
+
+{{ $.Scratch.Add "breadcrumb" (slice (dict "url" .Site.BaseURL "name" "home" "position" 1 )) -}}
+ {{ range $index, $element := split $url "/" -}}
+ {{ $dot.Scratch.Add "path" $element -}}
+ {{ $.Scratch.Add "path" "/" -}}
+ {{ if ne $element "" -}}
+ {{ $.Scratch.Add "breadcrumb" (slice (dict "url" ($.Scratch.Get "path") "name" . "position" (add $index 2))) -}}
+ {{ end -}}
+{{ end -}}
+
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ {{ if eq .Site.Params.schemaType "Organization" -}}
+ "@type": "Organization",
+ "@id": {{ print $baseURL "#/schema/organization/1" }},
+ {{ else -}}
+ "@type": "Person",
+ "@id": {{ print $baseURL "#/schema/person/1" }},
+ {{ end -}}
+ "name": "{{ .Site.Params.schemaName }}",
+ "url": {{ print $baseURL }},
+ "sameAs": [
+ {{ with .Site.Params.schemaTwitter -}}
+ {{ . }}
+ {{ end -}}
+ {{ with .Site.Params.schemaLinkedIn -}}
+ , {{ . }}
+ {{ end -}}
+ {{ with .Site.Params.schemaGitHub -}}
+ , {{ . }}
+ {{ end -}}
+ ],
+ {{ if eq .Site.Params.schemaType "Organization" -}}
+ "logo": {
+ "@type": "ImageObject",
+ "@id": {{ print $baseURL "#/schema/image/1"}},
+ "url": {{ print $baseURL .Site.Params.schemaLogo }},
+ "width": {{ .Site.Params.schemaLogoWidth }},
+ "height": {{ .Site.Params.schemaLogoHeight }},
+ "caption": "{{ .Site.Params.schemaName }}"
+ },
+ "image": {
+ "@id": {{ print $baseURL "#/schema/image/1" }}
+ }
+ {{ else -}}
+ "image": {
+ "@type": "ImageObject",
+ "@id": {{ print $baseURL "#/schema/image/1"}},
+ "url": {{ print $baseURL .Site.Params.schemaImage }},
+ "width": {{ .Site.Params.schemaImageWidth }},
+ "height": {{ .Site.Params.schemaImageHeight }},
+ "caption": "{{ .Site.Params.schemaName }}"
+ }
+ {{ end -}}
+ },
+ {
+ "@type": "WebSite",
+ "@id": {{ print $baseURL "#/schema/website/1" }},
+ "url": {{ print $baseURL }},
+ "name": "{{ .Site.Params.title }}",
+ "description": "{{ .Site.Params.description }}",
+ {{ if eq .Site.Params.schemaType "Organization" -}}
+ "publisher": {
+ "@id": {{ print $baseURL "#/schema/organization/1" }}
+ }
+ {{ else -}}
+ "publisher": {
+ "@id": {{ print $baseURL "#/schema/person/1" }}
+ }
+ {{ end -}}
+ },
+ {
+ {{ if and (ne .Kind "taxonomy") (ne .Kind "term") -}}
+ "@type": "WebPage",
+ {{ else -}}
+ "@type": "CollectionPage",
+ {{ end -}}
+ "@id": {{ .Permalink }},
+ "url": {{ .Permalink }},
+ "name": "{{ .Title }}",
+ "description": "{{ .Description }}",
+ "isPartOf": {
+ "@id": {{ print $baseURL "#/schema/website/1" }}
+ },
+ {{ if eq .Site.Params.schemaType "Organization" -}}
+ "about": {
+ "@id": {{ print $baseURL "#/schema/organization/1" }}
+ },
+ {{ else -}}
+ "about": {
+ "@id": {{ print $baseURL "#/schema/person/1" }}
+ },
+ {{ end -}}
+ "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05CET" }}",
+ "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05CET" }}",
+ "breadcrumb": {
+ "@id": {{ print .Permalink "#/schema/breadcrumb/1" }}
+ },
+ "primaryImageOfPage": {
+ "@id": {{ print .Permalink "#/schema/image/2" }}
+ },
+ "inLanguage": "{{ .Site.Params.schemaLocale }}",
+ "potentialAction": [{
+ "@type": "ReadAction", "target": [{{ .Permalink }}]
+ }]
+ },
+ {
+ "@type": "BreadcrumbList",
+ "@id": {{ print .Permalink "#/schema/breadcrumb/1" }},
+ "name": "Breadcrumbs",
+ "itemListElement": [{{ $list := $.Scratch.Get "breadcrumb" }}{{ $len := (len $list) }}{{ range $index, $element := $list }}{{ if ne .position 1 }},{{ end }}{
+ "@type": "ListItem",
+ "position": {{ .position }},
+ "item": {
+ {{ if ne (add $index 1) $len -}}
+ "@type": "WebPage",
+ "@id": {{ .url }},
+ "url": {{ .url }},
+ "name": "{{ .name | humanize | title }}"
+ {{ else -}}
+ "@id": {{ .url }}
+ {{ end -}}
+ }
+ }{{ end }}]
+ },
+
+ {{ if and (eq .Kind "page") (or (eq .Section "blog") (eq .Section "docs") (eq .Section "tutorial") (eq .Section "showcase")) -}}
+ {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "Article",
+ "@id": {{ print $baseURL "#/schema/article/1" }},
+ "headline": "{{ .Title }}",
+ "description": "{{ .Description }}",
+ "isPartOf": {
+ "@id": {{ .Permalink }}
+ },
+ "mainEntityOfPage": {
+ "@id": {{ .Permalink }}
+ },
+ "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05CET" }}",
+ "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05CET" }}",
+ "author": {
+ "@id": {{ print $baseURL "#/schema/person/2" }}
+ },
+ {{ if eq .Site.Params.schemaType "Organization" -}}
+ "publisher": {
+ "@id": {{ print $baseURL "#/schema/organization/1" }}
+ },
+ {{ else -}}
+ "publisher": {
+ "@id": {{ print $baseURL "#/schema/person/1" }}
+ },
+ {{ end -}}
+ "image": {
+ "@id": {{ print .Permalink "#/schema/image/2" }}
+ }
+ }
+ ]
+ },
+ {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "Person",
+ "@id": {{ print $baseURL "#/schema/person/2" }},
+ "name": {{ .Site.Params.schemaAuthor }},
+ "sameAs": [
+ {{ with .Site.Params.schemaAuthorTwitter -}}
+ {{ . }}
+ {{ end -}}
+ {{ with .Site.Params.schemaAuthorLinkedIn -}}
+ , {{ . }}
+ {{ end -}}
+ {{ with .Site.Params.schemaAuthorGitHub -}}
+ , {{ . }}
+ {{ end -}}
+ ]
+ }
+ ]
+ },
+ {{ end -}}
+ {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "ImageObject",
+ "@id": {{ print .Permalink "#/schema/image/2" }},
+ "url": {{ $.Scratch.Get "primaryImage" }},
+ "contentUrl": {{ $.Scratch.Get "primaryImage" }},
+ "caption": "{{ .Title }}"
+ }
+ ]
+ }
+
+ ]
+}
+</script> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/head/stylesheet.html b/code/frontpage/layouts/partials/head/stylesheet.html
new file mode 100644
index 0000000..4dc25c0
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/stylesheet.html
@@ -0,0 +1,11 @@
+{{ if eq (hugo.Environment) "development" -}}
+ {{ $options := (dict "targetPath" "main.css" "enableSourceMap" true "includePaths" (slice "node_modules")) -}}
+ {{ $css := resources.Get "scss/app.scss" | toCSS $options -}}
+ <link rel="stylesheet" href="{{ $css.Permalink | relURL }}">
+{{ else -}}
+ {{ $options := (dict "targetPath" "main.css" "outputStyle" "compressed" "includePaths" (slice "node_modules")) -}}
+ {{ $css := resources.Get "scss/app.scss" | toCSS $options | postCSS (dict "config" "config/postcss.config.js") -}}
+ {{ $secureCSS := $css | resources.Fingerprint "sha512" -}}
+ <link rel="stylesheet" href="{{ $secureCSS.Permalink }}" integrity="{{ $secureCSS.Data.Integrity }}" crossorigin="anonymous">
+{{ end -}}
+<noscript><style>img.lazyload { display: none; }</style></noscript> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/head/twitter_cards.html b/code/frontpage/layouts/partials/head/twitter_cards.html
new file mode 100644
index 0000000..fdf581c
--- /dev/null
+++ b/code/frontpage/layouts/partials/head/twitter_cards.html
@@ -0,0 +1,24 @@
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:site" content="{{ .Site.Params.twitterSite }}">
+<meta name="twitter:creator" content="{{ .Site.Params.twitterCreator }}">
+<meta name="twitter:title" content="{{ .Title }}">
+<meta name="twitter:description" content="{{ .Description }}">
+{{ with $.Params.images -}}
+ <meta name="twitter:image" content="{{ $.Permalink }}{{ index . 0 }}">
+{{ else -}}
+ {{ $images := $.Resources.ByType "image" -}}
+ {{ $featured := $images.GetMatch "*feature*" -}}
+ {{ if not $featured -}}
+ {{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" -}}
+ {{ end -}}
+ {{ with $featured -}}
+ <meta name="twitter:image" content="{{ $featured.Permalink }}">
+ {{ else -}}
+ {{ with $.Site.Params.images -}}
+ <meta name="twitter:image" content="{{ index . 0 | absURL }}">
+ {{ else -}}
+ <meta name="twitter:card" content="summary">
+ {{ end -}}
+ {{ end -}}
+{{ end -}}
+<meta name="twitter:image:alt" content="{{ .Title }}"> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/header/alert.html b/code/frontpage/layouts/partials/header/alert.html
new file mode 100644
index 0000000..7944ae9
--- /dev/null
+++ b/code/frontpage/layouts/partials/header/alert.html
@@ -0,0 +1,10 @@
+{{ if .Site.Params.alertDismissable -}}
+ <div id="announcement" data-id="global-alert-{{ md5 .Site.Params.alertText }}" class="alert alert-primary alert-dismissible fade show text-lg-center" role="alert">
+ {{ .Site.Params.alertText | safeHTML }}
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+ </div>
+{{ else -}}
+ <div class="alert alert-primary text-lg-center" role="alert">
+ {{ .Site.Params.alertText | safeHTML }}
+ </div>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/header/header.html b/code/frontpage/layouts/partials/header/header.html
new file mode 100644
index 0000000..7869784
--- /dev/null
+++ b/code/frontpage/layouts/partials/header/header.html
@@ -0,0 +1,254 @@
+{{ if .Site.Params.alert -}}
+{{ partial "header/alert.html" . }}
+{{ end -}}
+
+{{ if eq .Site.Params.options.navbarSticky true -}}
+<div class="sticky-top">
+ {{ end -}}
+
+ <header class="navbar navbar-expand-md navbar-light doks-navbar">
+ <nav class="container-{{ if .Site.Params.options.fullWidth }}fluid{{ else }}xxl{{ end }} flex-wrap flex-lg-nowrap"
+ aria-label="Main navigation">
+
+ <a class="navbar-brand order-0" href="/" aria-label="{{ .Site.Params.Title }}">
+ {{ .Site.Params.Title }}
+ </a>
+
+ {{ if (in .Site.Params.sections.sectionNav .Section) -}}
+ <button class="btn btn-link order-0 ms-auto d-md-none" type="button" data-bs-toggle="offcanvas"
+ data-bs-target="#offcanvasExample" aria-controls="offcanvasExample">
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+ class="feather feather-more-horizontal">
+ <circle cx="12" cy="12" r="1"></circle>
+ <circle cx="19" cy="12" r="1"></circle>
+ <circle cx="5" cy="12" r="1"></circle>
+ </svg>
+ </button>
+ <div class="offcanvas offcanvas-start d-md-none" tabindex="-1" id="offcanvasExample"
+ aria-labelledby="offcanvasExampleLabel">
+ <div class="offcanvas-header">
+ <h5 class="offcanvas-title" id="offcanvasExampleLabel">{{ i18n "browse" }} {{ .Section }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+ </div>
+ <div class="offcanvas-body">
+ <aside class="doks-sidebar mt-n3">
+ <nav id="doks-docs-nav" aria-label="Tertiary navigation">
+ {{ partial "sidebar/docs-menu.html" . }}
+ </nav>
+ </aside>
+ </div>
+ </div>
+ {{ end -}}
+
+ <button class="btn btn-menu order-2 d-block d-md-none" type="button" data-bs-toggle="offcanvas"
+ data-bs-target="#offcanvasDoks" aria-controls="offcanvasDoks" aria-label="Open main menu">
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+ class="feather feather-menu">
+ <line x1="3" y1="12" x2="21" y2="12"></line>
+ <line x1="3" y1="6" x2="21" y2="6"></line>
+ <line x1="3" y1="18" x2="21" y2="18"></line>
+ </svg>
+ </button>
+ <div class="offcanvas offcanvas-end border-0 py-md-1" tabindex="-1" id="offcanvasDoks" data-bs-backdrop="true"
+ aria-labelledby="offcanvasDoksLabel">
+ <div class="offcanvas-header d-md-none">
+ <h2 class="h5 offcanvas-title ps-2" id="offcanvasDoksLabel"><a class="text-dark" href="{{ " /" | relLangURL
+ }}">{{ .Site.Params.Title }}</a></h2>
+ <button type="button" class="btn-close text-reset me-2" data-bs-dismiss="offcanvas"
+ aria-label="Close main menu"></button>
+ </div>
+ <div class="offcanvas-body p-4 p-md-0">
+ <ul class="nav flex-column flex-md-row align-items-md-center mt-2 mt-md-0 ms-md-2 me-md-auto">
+ {{- $current := . -}}
+ {{- $section := $current.Section -}}
+ {{ range .Site.Menus.main -}}
+ {{- $active := or ($current.IsMenuCurrent "main" .) ($current.HasMenuCurrent "main" .) -}}
+ {{- $active = or $active (eq .Name $current.Title) -}}
+ {{- $active = or $active (and (eq .Name ($section | humanize)) (eq $current.Section $section)) -}}
+ {{- $active = or $active (and (eq .Name "Blog") (eq $current.Section "blog" "contributors" "categories"
+ "tags")) -}}
+ {{ if .HasChildren }}
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle ps-0 py-1" href="#" id="navbarDropdownMenuLink" role="button"
+ data-bs-toggle="dropdown" aria-expanded="false">
+ {{ .Name }}
+ <span class="dropdown-caret"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ stroke-linejoin="round" class="feather feather-chevron-down">
+ <polyline points="6 9 12 15 18 9"></polyline>
+ </svg></span>
+ </a>
+ <ul class="dropdown-menu dropdown-menu-main shadow rounded border-0"
+ aria-labelledby="navbarDropdownMenuLink">
+ {{ range .Children -}}
+ {{- $active = eq .Name $current.Title -}}
+ <li>
+ <a class="dropdown-item{{ if $active }} active{{ end }}" href="{{ .URL | relLangURL }}" {{ if $active
+ }} aria-current="true" {{ end }}>{{ .Name }}</a>
+ </li>
+ {{ end -}}
+ </ul>
+ </li>
+ {{ else }}
+ <li class="nav-item">
+ <a class="nav-link ps-0 py-1{{ if $active }} active{{ end }}" href="{{ .URL | relLangURL }}">{{ .Name
+ }}</a>
+ </li>
+ {{ end }}
+ {{ end -}}
+ </ul>
+
+ {{- $showFlexSearch := .Site.Params.options.flexSearch }}
+ {{- if $showFlexSearch }}
+ {{- if and (isset .Site.Params.options "searchsectionsshow") (not (eq .Site.Params.options.searchSectionsShow
+ "ALL")) }}
+ {{- $showFlexSearch = or (eq (len .Site.Params.options.searchSectionsShow) 0) (in
+ .Site.Params.options.searchSectionsShow .Section) (and .IsHome (in .Site.Params.options.searchSectionsShow
+ "HomePage")) }}
+ {{- end }}
+ {{- end }}
+
+ {{ if $showFlexSearch -}}
+ <hr class="text-black-50 my-4 d-lg-none">
+ <form class="doks-search position-relative flex-grow-1 ms-lg-auto me-lg-2">
+ <input id="search" class="form-control is-search" type="search" placeholder="{{ i18n " search-text" }}"
+ aria-label="{{ i18n " search-text" }}" autocomplete="off">
+ <div id="suggestions" class="shadow bg-white rounded d-none"></div>
+ </form>
+ {{ end -}}
+
+ <hr class="text-black-50 my-4 d-lg-none">
+ <ul class="nav flex-column flex-lg-row">
+ {{ range .Site.Menus.social -}}
+ <li class="nav-item">
+ <a class="nav-link social-link" href="{{ .URL | relURL }}">{{ .Pre | safeHTML }}<small
+ class="ms-2 d-lg-none">{{ .Name | safeHTML }}</small></a>
+ </li>
+ {{ end -}}
+ </ul>
+
+ {{ if .Site.Params.options.darkMode -}}
+ <hr class="text-black-50 my-4 d-lg-none">
+ <button id="mode" class="btn btn-link" type="button" aria-label="Toggle user interface mode">
+ <span class="toggle-dark"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+ class="feather feather-moon">
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
+ </svg></span>
+ <span class="toggle-light"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ stroke-linejoin="round" class="feather feather-sun">
+ <circle cx="12" cy="12" r="5"></circle>
+ <line x1="12" y1="1" x2="12" y2="3"></line>
+ <line x1="12" y1="21" x2="12" y2="23"></line>
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
+ <line x1="1" y1="12" x2="3" y2="12"></line>
+ <line x1="21" y1="12" x2="23" y2="12"></line>
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
+ </svg></span>
+ </button>
+ {{ end -}}
+
+ {{ if eq .Site.Params.options.multilingualMode true -}}
+ <hr class="text-black-50 my-4 d-lg-none">
+ <div class="dropdown">
+ <button class="btn btn-doks-light dropdown-toggle" id="doks-languages" data-bs-toggle="dropdown"
+ aria-expanded="false" data-bs-display="static">
+ {{ .Site.Params.languageName }}
+ <span class="dropdown-caret"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ stroke-linejoin="round" class="feather feather-chevron-down">
+ <polyline points="6 9 12 15 18 9"></polyline>
+ </svg></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-lg-end me-lg-2 shadow rounded border-0"
+ aria-labelledby="doks-languages">
+
+ <li><a class="dropdown-item current" aria-current="true" href="{{ .RelPermalink }}">{{
+ .Site.Language.LanguageName }}</a></li>
+
+ <li>
+ <hr class="dropdown-divider">
+ </li>
+
+ {{ if .IsTranslated -}}
+ {{ range .Translations }}
+ <li><a class="dropdown-item" rel="alternate" href="{{ .RelPermalink }}" hreflang="{{ .Lang }}"
+ lang="{{ .Lang }}">{{ .Language.LanguageName }}</a></li>
+ {{ end -}}
+ {{ else -}}
+ {{ range .Site.Languages -}}
+ {{ if ne $.Site.Language.Lang .Lang }}
+ <li><a class="dropdown-item" rel="alternate" href="{{ .Lang | relLangURL }}" hreflang="{{ .Lang }}"
+ lang="{{ .Lang }}">{{ .LanguageName }}</a></li>
+ {{ end -}}
+ {{ end -}}
+ {{ end -}}
+ <!--
+ <li><hr class="dropdown-divider"></li>
+ <li><a class="dropdown-item" href="/docs/contributing/how-to-contribute/">Help Translate</a></li>
+ -->
+ </ul>
+ </div>
+ {{ end -}}
+
+ {{ if eq .Site.Params.options.docsVersioning true -}}
+ <hr class="text-black-50 my-4 d-lg-none">
+ <div class="dropdown">
+ <button class="btn btn-doks-light dropdown-toggle" id="doks-versions" data-bs-toggle="dropdown"
+ aria-expanded="false" data-bs-display="static" aria-label="Toggle version menu">
+ <span class="d-none">Doks</span> v{{ .Site.Params.docsVersion }}
+ <span class="dropdown-caret"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ stroke-linejoin="round" class="feather feather-chevron-down">
+ <polyline points="6 9 12 15 18 9"></polyline>
+ </svg></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-lg-end me-lg-2 shadow rounded border-0"
+ aria-labelledby="doks-versions">
+ <li><a class="dropdown-item current" aria-current="true"
+ href="/docs/{{ .Site.Params.docsVersion }}/prologue/introduction/">Latest ({{ .Site.Params.docsVersion
+ }}.x)</a></li>
+ <li>
+ <hr class="dropdown-divider">
+ </li>
+ <li><a class="dropdown-item" href="/docs/0.2/prologue/introduction/">v0.2.x</a></li>
+ <li><a class="dropdown-item" href="/docs/0.1/prologue/introduction/">v0.1.x</a></li>
+ <li>
+ <hr class="dropdown-divider">
+ </li>
+ <li><a class="dropdown-item" href="/docs/versions/">All versions</a></li>
+ </ul>
+ </div>
+ {{ end -}}
+ </div>
+ </div>
+ </nav>
+ </header>
+
+ {{ if eq .Site.Params.options.navbarSticky true }}
+</div>
+{{ end -}}
+
+{{ if eq .Section "docs" -}}
+<div class="container-{{ if .Site.Params.options.fullWidth }}fluid{{ else }}xxl{{ end }}">
+ <aside class="doks-sidebar">
+ <nav id="doks-docs-nav" class="collapse d-lg-none" aria-label="Tertiary navigation">
+ {{ partial "sidebar/docs-menu.html" . }}
+ </nav>
+ </aside>
+</div>
+
+{{ else if ne .CurrentSection .FirstSection -}}
+<div class="container-{{ if .Site.Params.options.fullWidth }}fluid{{ else }}xxl{{ end }}">
+ <aside class="doks-sidebar">
+ <nav id="doks-docs-nav" class="collapse d-lg-none" aria-label="Tertiary navigation">
+ {{ partial "sidebar/docs-menu.html" . }}
+ </nav>
+ </aside>
+</div>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/main/blog-meta.html b/code/frontpage/layouts/partials/main/blog-meta.html
new file mode 100644
index 0000000..7d11e94
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/blog-meta.html
@@ -0,0 +1,2 @@
+{{ $last := sub (len .Params.contributors) 1 }}
+<p><small>Posted{{ if .Params.categories -}}&nbsp;in&nbsp;{{ range $index, $category := .Params.categories -}}{{ if gt $index 0 -}}, {{ end -}}<a class="stretched-link position-relative link-muted" href="{{ "/categories/" | absURL }}{{ . | urlize }}/">{{ . }}</a>{{ end -}}{{ end -}}&nbsp;on&nbsp;{{ .PublishDate.Format "January 2, 2006" }} by {{ if .Params.contributors -}}{{ range $index, $contributor := .Params.contributors }}{{ if gt $index 0 }}{{ if eq $index $last }} and {{ else }}, {{ end }}{{ end }}<a class="stretched-link position-relative" href="{{ "/contributors/" | relURL }}{{ . | urlize }}/">{{ . }}</a>{{ end -}}{{ end -}}&nbsp;&hyphen;&nbsp;<strong>{{ .ReadingTime -}}&nbsp;min read</strong></small><p> \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/main/breadcrumb.html b/code/frontpage/layouts/partials/main/breadcrumb.html
new file mode 100644
index 0000000..1d960d2
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/breadcrumb.html
@@ -0,0 +1,4 @@
+{{ with .Parent -}}
+ {{ partial "main/breadcrumb.html" . -}}
+ <li class="breadcrumb-item"><a href="{{ .RelPermalink }}">{{ if .IsHome }}Home{{ else if eq .CurrentSection .FirstSection }}{{ .Section | humanize }}{{ else }}{{ .Title }}{{ end }}</a></li>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/main/date.html b/code/frontpage/layouts/partials/main/date.html
new file mode 100644
index 0000000..4b41ddb
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/date.html
@@ -0,0 +1,6 @@
+<!--
+ Returns formatted date.
+ Usage: partial "docs/date" (dict "Date" .Date "Format" .Site.Params.BookDateFormat)
+-->
+{{ $format := default "January 2, 2006" .Format -}}
+{{ return (.Date.Format $format) -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/main/docs-navigation.html b/code/frontpage/layouts/partials/main/docs-navigation.html
new file mode 100644
index 0000000..e6a5668
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/docs-navigation.html
@@ -0,0 +1,24 @@
+{{ if or .Prev .Next -}}
+ <div class="docs-navigation d-flex justify-content-between">
+ <!-- https://www.feliciano.tech/blog/custom-sort-hugo-single-pages/ -->
+ {{ $pages := where site.RegularPages "Section" .Section -}}
+ {{ with $pages.Next . -}}
+ <a href="{{ .RelPermalink }}">
+ <div class="card my-1">
+ <div class="card-body py-2">
+ &larr; {{ .Title }}
+ </div>
+ </div>
+ </a>
+ {{ end -}}
+ {{ with $pages.Prev . -}}
+ <a class="ms-auto" href="{{ .RelPermalink }}">
+ <div class="card my-1">
+ <div class="card-body py-2">
+ {{ .Title }} &rarr;
+ </div>
+ </div>
+ </a>
+ {{ end -}}
+ </div>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/main/edit-page.html b/code/frontpage/layouts/partials/main/edit-page.html
new file mode 100644
index 0000000..b69ed36
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/edit-page.html
@@ -0,0 +1,34 @@
+{{ $parts := slice .Site.Params.docsRepo }}
+
+{{ if (eq .Site.Params.repoHost "GitHub") }}
+ {{ $parts = $parts | append "blob" .Site.Params.docsRepoBranch }}
+{{ else if (eq .Site.Params.repoHost "Gitea") }}
+ {{ $parts = $parts | append "_edit" .Site.Params.docsRepoBranch }}
+{{ else if (eq .Site.Params.repoHost "GitLab") }}
+ {{ $parts = $parts | append "-/blob" .Site.Params.docsRepoBranch }}
+{{ else if (eq .Site.Params.repoHost "Bitbucket") }}
+ {{ $parts = $parts | append "src" .Site.Params.docsRepoBranch }}
+{{ else if (eq .Site.Params.repoHost "BitbucketServer") }}
+ {{ $parts = $parts | append "browse" .Site.Params.docsRepoBranch }}
+{{ end }}
+
+{{ if isset .Site.Params "docsreposubpath" }}
+ {{ if not (eq .Site.Params.docsRepoSubPath "") }}
+ {{ $parts = $parts | append .Site.Params.docsRepoSubPath }}
+ {{ end }}
+{{ end }}
+
+{{ $filePath := replace .File.Path "\\" "/" }}
+
+{{ $parts = $parts | append "content" .Lang $filePath }}
+
+{{ $url := delimit $parts "/" }}
+
+<div class="edit-page">
+ <a href="{{ $url }}">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-2">
+ <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
+ </svg>
+ Edit this page on {{ .Site.Params.repoHost }}
+ </a>
+</div>
diff --git a/code/frontpage/layouts/partials/main/last-modified.html b/code/frontpage/layouts/partials/main/last-modified.html
new file mode 100644
index 0000000..edb6c7d
--- /dev/null
+++ b/code/frontpage/layouts/partials/main/last-modified.html
@@ -0,0 +1,10 @@
+{{ if and .GitInfo .Site.Params.docsRepo -}}
+ {{- $date := partial "main/date" (dict "Date" .GitInfo.AuthorDate.Local "Format" .Site.Params.BookDateFormat) -}}
+ {{- $commitPath := default "commit" .Site.Params.BookCommitPath -}}
+ <div class="last-modified">
+ <a href="{{ .Site.Params.docsRepo }}/{{ $commitPath }}/{{ .GitInfo.Hash }}">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
+ Last modified on {{ $date }}
+ </a>
+ </div>
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/sidebar/auto-collapsible-menu.html b/code/frontpage/layouts/partials/sidebar/auto-collapsible-menu.html
new file mode 100644
index 0000000..4600d0a
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/auto-collapsible-menu.html
@@ -0,0 +1,57 @@
+<!-- Auto collapsible section menu -->
+<ul class="list-unstyled collapsible-sidebar">
+ {{ $currentPage := . -}}
+ {{ $section := $currentPage.Section -}}
+ {{ range (where .Site.Sections "Section" "in" $section) }}
+ {{ range .Sections }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li class="mb-1">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ md5 .Title }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Title }}
+ </button>
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ md5 .Title }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Pages }}
+ {{ if .IsNode }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li class="my-1 ms-3">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ md5 .Title }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Title }}
+ </button>
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ md5 .Title }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Pages }}
+ {{ if .IsNode }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li class="my-1 ms-3">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ md5 .Title }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Title }}
+ </button>
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ md5 .Title }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Pages }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Title }}</a></li>
+ {{ end }}
+ </ul>
+ </div>
+ </li>
+ {{ else }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Title }}</a></li>
+ {{ end }}
+ {{ end }}
+ </ul>
+ </div>
+ </li>
+ {{ else }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Title }}</a></li>
+ {{ end }}
+ {{ end }}
+ </ul>
+ </div>
+ </li>
+ {{ end }}
+ {{ end }}
+</ul>
diff --git a/code/frontpage/layouts/partials/sidebar/auto-default-menu.html b/code/frontpage/layouts/partials/sidebar/auto-default-menu.html
new file mode 100644
index 0000000..6e4565b
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/auto-default-menu.html
@@ -0,0 +1,37 @@
+<!-- Auto default section menu -->
+{{ $currentPage := . -}}
+{{ $section := $currentPage.Section -}}
+{{ range (where .Site.Sections "Section" "in" $section) }}
+ {{ range .Sections }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <h3 class="h6 text-uppercase mb-2">{{ .Title }}</h3>
+ <ul class="list-unstyled">
+ {{ range .Pages }}
+ {{ if .IsNode }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <h4 class="h6 text-uppercase ms-3 mt-3 mb-2">{{ .Title }}</h4>
+ <ul class="list-unstyled ms-3">
+ {{ range .Pages }}
+ {{ if .IsNode }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <h5 class="h6 text-uppercase mt-2 mb-2">{{ .Title }}</h5>
+ <ul class="list-unstyled ms-3">
+ {{ range .Pages }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Name }}</a></li>
+ {{ end }}
+ </ul>
+ {{ else }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Name }}</a></li>
+ {{ end }}
+ {{ end }}
+ </ul>
+ {{ else }}
+ {{ $active := in $currentPage.RelPermalink .RelPermalink }}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .Permalink }}">{{ .Name }}</a></li>
+ {{ end }}
+ {{ end }}
+ </ul>
+ {{ end }}
+{{ end }}
diff --git a/code/frontpage/layouts/partials/sidebar/docs-menu.html b/code/frontpage/layouts/partials/sidebar/docs-menu.html
new file mode 100644
index 0000000..b7baebb
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/docs-menu.html
@@ -0,0 +1,9 @@
+{{ if and .Site.Params.menu.section.auto .Site.Params.menu.section.collapsibleSidebar -}}
+ {{ partial "sidebar/auto-collapsible-menu.html" . -}}
+{{ else if and .Site.Params.menu.section.auto (not .Site.Params.menu.section.collapsibleSidebar) -}}
+ {{ partial "sidebar/auto-default-menu.html" . -}}
+{{ else if and (not .Site.Params.menu.section.auto) .Site.Params.menu.section.collapsibleSidebar -}}
+ {{ partial "sidebar/manual-collapsible-menu.html" . -}}
+{{ else if and (not .Site.Params.menu.section.auto) (not .Site.Params.menu.section.collapsibleSidebar) -}}
+ {{ partial "sidebar/manual-default-menu.html" . -}}
+{{ end -}} \ No newline at end of file
diff --git a/code/frontpage/layouts/partials/sidebar/docs-toc.html b/code/frontpage/layouts/partials/sidebar/docs-toc.html
new file mode 100644
index 0000000..c71a7b8
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/docs-toc.html
@@ -0,0 +1,26 @@
+{{ if and (ne .Params.toc false) (ne .TableOfContents "<nav id=\"TableOfContents\"></nav>") -}}
+<div class="d-xl-none">
+ <button class="btn btn-outline-primary btn-sm doks-toc-toggle collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#onThisPage" aria-controls="doks-docs-nav" aria-expanded="false" aria-label="Toggle On this page navigation">
+ <span>{{ i18n "on-this-page" }}</span>
+ <span>
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="doks doks-expand" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><title>Expand</title><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="doks doks-collapse" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><title>Collapse</title><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>
+ </span>
+ </button>
+ <div class="collapse" id="onThisPage">
+ <div class="card card-body mt-3 py-1">
+ <div class="page-links">
+ {{ .TableOfContents }}
+ </div>
+ </div>
+ </div>
+</div>
+<div class="page-links d-none d-xl-block">
+ <h3>{{ i18n "on-this-page" }}</h3>
+ {{ if eq .Site.Params.options.scrollSpy true -}}
+ {{ .TableOfContents | replaceRE "<nav id=\"TableOfContents\">" "<nav id=\"toc\">" | safeHTML }}
+ {{ else -}}
+ {{ .TableOfContents }}
+ {{ end -}}
+</div>
+{{ end -}}
diff --git a/code/frontpage/layouts/partials/sidebar/manual-collapsible-menu.html b/code/frontpage/layouts/partials/sidebar/manual-collapsible-menu.html
new file mode 100644
index 0000000..f83c667
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/manual-collapsible-menu.html
@@ -0,0 +1,63 @@
+<!-- Manual collapsible section menu -->
+<ul class="list-unstyled collapsible-sidebar">
+ {{ $currentPage := . -}}
+ {{ $section := $currentPage.Section -}}
+ {{ range (index .Site.Menus $section) -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li class="mb-1">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ .Identifier }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Name }}
+ </button>
+ {{ if .HasChildren -}}
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ .Identifier }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Children -}}
+ {{ if .HasChildren -}}
+ <li class="my-1 ms-3">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ .Identifier }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Name }}
+ </button>
+ {{ if .HasChildren -}}
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ .Identifier }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Children -}}
+ {{ if .HasChildren -}}
+ <li class="my-1 ms-3">
+ <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#section-{{ .Identifier }}" aria-expanded="{{ if $active }}true{{ else }}false{{ end }}">
+ {{ .Name }}
+ </button>
+ {{ if .HasChildren -}}
+ <div class="collapse{{ if $active }} show{{ end }}" id="section-{{ .Identifier }}">
+ <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
+ {{ range .Children -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ </ul>
+ </div>
+ {{ end -}}
+ </li>
+ {{ else -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ {{ end -}}
+ </ul>
+ </div>
+ {{ end -}}
+ </li>
+ {{ else -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ {{ end -}}
+ </ul>
+ </div>
+ {{ end -}}
+ </li>
+ {{ end -}}
+</ul>
diff --git a/code/frontpage/layouts/partials/sidebar/manual-default-menu.html b/code/frontpage/layouts/partials/sidebar/manual-default-menu.html
new file mode 100644
index 0000000..32c4cd4
--- /dev/null
+++ b/code/frontpage/layouts/partials/sidebar/manual-default-menu.html
@@ -0,0 +1,41 @@
+<!-- Manual default section menu -->
+{{ $currentPage := . -}}
+{{ $section := $currentPage.Section -}}
+{{ range (index .Site.Menus $section) -}}
+ <h3 class="h6 text-uppercase mb-2">{{ .Name }}</h3>
+ {{ if .HasChildren -}}
+ <ul class="list-unstyled">
+ {{ range .Children -}}
+ {{ if .HasChildren -}}
+ <h4 class="h6 text-uppercase ms-3 mt-3 mb-2">{{ .Name }}</h4>
+ {{ if .HasChildren -}}
+ <ul class="list-unstyled ms-3">
+ {{ range .Children -}}
+ {{ if .HasChildren -}}
+ <h5 class="h6 text-uppercase mt-2 mb-2">{{ .Name }}</h5>
+ {{ if .HasChildren -}}
+ <ul class="list-unstyled ms-3">
+ {{ range .Children -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ </ul>
+ {{ end -}}
+ {{ else -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ {{ end -}}
+ </ul>
+ {{ end -}}
+ {{ else -}}
+ {{- $active := or ($currentPage.IsMenuCurrent $section .) ($currentPage.HasMenuCurrent $section .) -}}
+ {{- $active = or $active (eq $currentPage.Section .Identifier) -}}
+ <li><a class="docs-link{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}">{{ .Name }}</a></li>
+ {{ end -}}
+ {{ end -}}
+ </ul>
+ {{ end -}}
+{{ end -}}
diff --git a/code/frontpage/layouts/robots.txt b/code/frontpage/layouts/robots.txt
new file mode 100644
index 0000000..3107a65
--- /dev/null
+++ b/code/frontpage/layouts/robots.txt
@@ -0,0 +1,7 @@
+User-agent: *
+{{ if eq (hugo.Environment) "production" -}}
+Allow: /
+{{ else -}}
+Disallow: /
+{{ end }}
+Sitemap: {{ "sitemap.xml" | absURL -}}
diff --git a/code/frontpage/layouts/rss.xml b/code/frontpage/layouts/rss.xml
new file mode 100644
index 0000000..3a5bb13
--- /dev/null
+++ b/code/frontpage/layouts/rss.xml
@@ -0,0 +1,27 @@
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
+ <link>{{ .Permalink }}</link>
+ <description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
+ <generator>Hugo -- gohugo.io</generator>{{ with .Site.Params.languageTag | default "en-US" }}
+ <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
+ <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
+ <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Params.copyRight }}
+ <copyright>{{ . | safeHTML }}</copyright>{{end}}{{ if not .Date.IsZero }}
+ <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
+ {{ with .OutputFormats.Get "RSS" }}
+ {{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
+ {{ end }}
+ {{ range .Pages }}{{ if ne .Params.feed_exclude true }}
+ <item>
+ <title>{{ .Title }}</title>
+ <link>{{ .Permalink }}</link>
+ <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
+ {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
+ <guid>{{ .Permalink }}</guid>
+ <description>{{ .Summary | html }}</description>
+ </item>
+ {{ end }}{{ end }}
+ </channel>
+</rss>
diff --git a/code/frontpage/layouts/shortcodes/alert.html b/code/frontpage/layouts/shortcodes/alert.html
new file mode 100644
index 0000000..e2abe8e
--- /dev/null
+++ b/code/frontpage/layouts/shortcodes/alert.html
@@ -0,0 +1,12 @@
+<div class="alert alert-{{ with .Get "context" }}{{.}}{{ else }}doks{{ end }} d-flex" role="alert">
+ <div class="flex-shrink-1 alert-icon">{{ with .Get "icon" }}{{.}} {{ end }}</div>
+ {{ with .Get "text"}}
+ <div class="w-100">{{ . | safeHTML }} </div>
+ {{ else }}
+ {{ with .Inner}}
+ <div class="w-100"> {{ . | markdownify}}</div>
+ {{ else }}
+ {{ errorf "No valid text variable or Inner content given"}}
+ {{ end }}
+ {{ end}}
+</div>
diff --git a/code/frontpage/layouts/shortcodes/details.html b/code/frontpage/layouts/shortcodes/details.html
new file mode 100644
index 0000000..61a7183
--- /dev/null
+++ b/code/frontpage/layouts/shortcodes/details.html
@@ -0,0 +1,4 @@
+<details{{ with .Get 1 }} {{ . | safeHTML }}{{ end -}}>
+ <summary>{{ with .Get 0 -}}{{ . | safeHTML }}{{ else -}}{{ errorf "No summary provided"}}{{ end -}}</summary>
+ {{ with .Inner -}}{{ . | markdownify}}{{ else -}}{{ errorf "No details provided"}}{{ end -}}
+</details> \ No newline at end of file
diff --git a/code/frontpage/layouts/shortcodes/email.html b/code/frontpage/layouts/shortcodes/email.html
new file mode 100644
index 0000000..88674fc
--- /dev/null
+++ b/code/frontpage/layouts/shortcodes/email.html
@@ -0,0 +1 @@
+<script type="text/javascript" nonce="dXNlcj0iaGVsbG8iLGRvbWFpbj0iaGVua3ZlcmxpbmRlLmNvbSIsZG9jdW1lbnQud3JpdGUodXNlcisiQCIrZG9tYWluKTs=">user="{{ with .Get "user" }}{{.}}{{ end }}",domain="{{ with .Get "domain" }}{{.}}{{ end }}",document.write(user+"@"+domain);</script><noscript>{{ with .Get "user" }}{{.}}{{ end }} at {{ with .Get "domain" }}{{.}}{{ end }}</noscript> \ No newline at end of file
diff --git a/code/frontpage/layouts/shortcodes/mermaid.html b/code/frontpage/layouts/shortcodes/mermaid.html
new file mode 100644
index 0000000..aeecad5
--- /dev/null
+++ b/code/frontpage/layouts/shortcodes/mermaid.html
@@ -0,0 +1,8 @@
+{{ if .Page.Params.mermaid -}}
+ <div class="mermaid{{ with .Get "class" }} {{ . }}{{ end }}">
+ {{ $data := replaceRE "(^\\s+```)" "" .Inner -}}
+ {{ replaceRE "(```\\s+$)" "" $data -}}
+ </div>
+{{ else -}}
+ {{ errorf "Failed to process mermaid shortcode: %s. Set mermaid to true in page front matter." .Position }}
+{{ end -}}
diff --git a/code/frontpage/layouts/shortcodes/video.html b/code/frontpage/layouts/shortcodes/video.html
new file mode 100644
index 0000000..784c3a7
--- /dev/null
+++ b/code/frontpage/layouts/shortcodes/video.html
@@ -0,0 +1,11 @@
+<div class="ratio ratio-{{ with .Get "ratio" }}{{.}}{{ end }}">
+ <video{{ with .Get "attributes" }} {{ . | safeHTMLAttr }}{{ end }}>
+ {{ with .Get "webm-src" -}}
+ <source src="{{ . | relURL }}" type="video/webm">
+ {{ end -}}
+ {{ with .Get "mp4-src" -}}
+ <source src="{{ . | relURL }}" type="video/mp4">
+ {{ end -}}
+ Sorry, your browser doesn't support embedded videos.
+ </video>
+</div>
diff --git a/code/frontpage/layouts/sitemap.xml b/code/frontpage/layouts/sitemap.xml
new file mode 100644
index 0000000..a3fcf7a
--- /dev/null
+++ b/code/frontpage/layouts/sitemap.xml
@@ -0,0 +1,22 @@
+{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml">
+ {{ range .Data.Pages }}{{ if ne .Params.sitemap_exclude true }}
+ <url>
+ <loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
+ <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>{{ end }}{{ with .Sitemap.ChangeFreq }}
+ <changefreq>{{ . }}</changefreq>{{ end }}{{ if ge .Sitemap.Priority 0.0 }}
+ <priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ <xhtml:link
+ rel="alternate"
+ hreflang="{{ .Lang }}"
+ href="{{ .Permalink }}"
+ />{{ end }}
+ </url>
+ {{ end }}{{ end }}
+</urlset>
diff --git a/code/frontpage/package.json b/code/frontpage/package.json
new file mode 100644
index 0000000..e5f638b
--- /dev/null
+++ b/code/frontpage/package.json
@@ -0,0 +1,74 @@
+{
+ "name": "@hyas/doks",
+ "description": "Doks theme",
+ "version": "0.5.0",
+ "engines": {
+ "node": ">=16.16.0"
+ },
+ "browserslist": [
+ "defaults"
+ ],
+ "repository": "https://github.com/h-enk/doks",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "init": "shx rm -rf .git && git init -b main",
+ "create": "exec-bin node_modules/.bin/hugo/hugo new",
+ "prestart": "npm run clean",
+ "start": "exec-bin node_modules/.bin/hugo/hugo server --bind=0.0.0.0 --disableFastRender",
+ "prebuild": "npm run clean",
+ "build": "exec-bin node_modules/.bin/hugo/hugo --gc --minify",
+ "build:preview": "npm run build -D -F",
+ "clean": "shx rm -rf public resources",
+ "clean:install": "shx rm -rf package-lock.json node_modules ",
+ "lint": "npm run -s lint:scripts && npm run -s lint:styles && npm run -s lint:markdown",
+ "lint:scripts": "eslint assets/js config functions",
+ "lint:styles": "stylelint \"assets/scss/**/*.{css,sass,scss,sss,less}\"",
+ "lint:markdown": "markdownlint-cli2 \"*.md\" \"content/**/*.md\"",
+ "lint:markdown-fix": "markdownlint-cli2-fix \"*.md\" \"content/**/*.md\"",
+ "server": "exec-bin node_modules/.bin/hugo/hugo server",
+ "test": "npm run -s lint",
+ "env": "env",
+ "precheck": "npm version",
+ "check": "exec-bin node_modules/.bin/hugo/hugo version",
+ "copy:katex-fonts": "shx cp ./node_modules/katex/dist/fonts/* ./static/fonts/",
+ "postinstall": "hugo-installer --version otherDependencies.hugo --extended --destination node_modules/.bin/hugo",
+ "version": "auto-changelog -p && git add CHANGELOG.md"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.19.3",
+ "@babel/core": "^7.20.5",
+ "@babel/preset-env": "^7.20.2",
+ "@fullhuman/postcss-purgecss": "^5.0.0",
+ "@hyas/images": "^0.3.2",
+ "auto-changelog": "^2.4.0",
+ "autoprefixer": "^10.4.13",
+ "bootstrap": "^5.2.3",
+ "clipboard": "^2.0.11",
+ "eslint": "^8.29.0",
+ "exec-bin": "^1.0.0",
+ "flexsearch": "^0.7.31",
+ "highlight.js": "^11.7.0",
+ "hugo-installer": "^4.0.1",
+ "instant.page": "^5.1.1",
+ "katex": "^0.16.4",
+ "lazysizes": "^5.3.2",
+ "markdownlint-cli2": "^0.5.1",
+ "netlify-plugin-submit-sitemap": "^0.4.0",
+ "node-fetch": "^3.3.0",
+ "postcss": "^8.4.19",
+ "postcss-cli": "^10.1.0",
+ "purgecss-whitelister": "^2.4.0",
+ "shx": "^0.3.4",
+ "stylelint": "^14.16.0",
+ "stylelint-config-standard-scss": "^6.1.0"
+ },
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.6"
+ },
+ "otherDependencies": {
+ "hugo": "0.107.0"
+ }
+}
diff --git a/code/frontpage/pnpm-lock.yaml b/code/frontpage/pnpm-lock.yaml
new file mode 100644
index 0000000..5b6fd83
--- /dev/null
+++ b/code/frontpage/pnpm-lock.yaml
@@ -0,0 +1,4292 @@
+lockfileVersion: 5.4
+
+specifiers:
+ '@babel/cli': ^7.19.3
+ '@babel/core': ^7.20.5
+ '@babel/preset-env': ^7.20.2
+ '@fullhuman/postcss-purgecss': ^5.0.0
+ '@hyas/images': ^0.3.2
+ auto-changelog: ^2.4.0
+ autoprefixer: ^10.4.13
+ bootstrap: ^5.2.3
+ clipboard: ^2.0.11
+ eslint: ^8.29.0
+ exec-bin: ^1.0.0
+ flexsearch: ^0.7.31
+ highlight.js: ^11.7.0
+ hugo-installer: ^4.0.1
+ instant.page: ^5.1.1
+ katex: ^0.16.4
+ lazysizes: ^5.3.2
+ markdownlint-cli2: ^0.5.1
+ netlify-plugin-submit-sitemap: ^0.4.0
+ node-fetch: ^3.3.0
+ postcss: ^8.4.19
+ postcss-cli: ^10.1.0
+ purgecss-whitelister: ^2.4.0
+ shx: ^0.3.4
+ stylelint: ^14.16.0
+ stylelint-config-standard-scss: ^6.1.0
+
+devDependencies:
+ '@babel/cli': 7.19.3_@babel+core@7.20.5
+ '@babel/core': 7.20.5
+ '@babel/preset-env': 7.20.2_@babel+core@7.20.5
+ '@fullhuman/postcss-purgecss': 5.0.0_postcss@8.4.19
+ '@hyas/images': 0.3.2
+ auto-changelog: 2.4.0
+ autoprefixer: 10.4.13_postcss@8.4.19
+ bootstrap: 5.2.3
+ clipboard: 2.0.11
+ eslint: 8.29.0
+ exec-bin: 1.0.0
+ flexsearch: 0.7.31
+ highlight.js: 11.7.0
+ hugo-installer: 4.0.1
+ instant.page: 5.1.1
+ katex: 0.16.4
+ lazysizes: 5.3.2
+ markdownlint-cli2: 0.5.1
+ netlify-plugin-submit-sitemap: 0.4.0
+ node-fetch: 3.3.0
+ postcss: 8.4.19
+ postcss-cli: 10.1.0_postcss@8.4.19
+ purgecss-whitelister: 2.4.0
+ shx: 0.3.4
+ stylelint: 14.16.0
+ stylelint-config-standard-scss: 6.1.0_u4cmdib575x7lmfjhgvokchuwe
+
+packages:
+
+ /@ampproject/remapping/2.2.0:
+ resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/gen-mapping': 0.1.1
+ '@jridgewell/trace-mapping': 0.3.17
+ dev: true
+
+ /@babel/cli/7.19.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-643/TybmaCAe101m2tSVHi9UKpETXP9c/Ff4mD2tAwkdP6esKIfaauZFc67vGEM6r9fekbEGid+sZhbEnSe3dg==}
+ engines: {node: '>=6.9.0'}
+ hasBin: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@jridgewell/trace-mapping': 0.3.17
+ commander: 4.1.1
+ convert-source-map: 1.9.0
+ fs-readdir-recursive: 1.1.0
+ glob: 7.2.3
+ make-dir: 2.1.0
+ slash: 2.0.0
+ optionalDependencies:
+ '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3
+ chokidar: 3.5.3
+ dev: true
+
+ /@babel/code-frame/7.18.6:
+ resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/highlight': 7.18.6
+ dev: true
+
+ /@babel/compat-data/7.20.5:
+ resolution: {integrity: sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/core/7.20.5:
+ resolution: {integrity: sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@ampproject/remapping': 2.2.0
+ '@babel/code-frame': 7.18.6
+ '@babel/generator': 7.20.5
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-module-transforms': 7.20.2
+ '@babel/helpers': 7.20.6
+ '@babel/parser': 7.20.5
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.20.5
+ '@babel/types': 7.20.5
+ convert-source-map: 1.9.0
+ debug: 4.3.4
+ gensync: 1.0.0-beta.2
+ json5: 2.2.1
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/generator/7.20.5:
+ resolution: {integrity: sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ '@jridgewell/gen-mapping': 0.3.2
+ jsesc: 2.5.2
+ dev: true
+
+ /@babel/helper-annotate-as-pure/7.18.6:
+ resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9:
+ resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-explode-assignable-expression': 7.18.6
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-compilation-targets/7.20.0_@babel+core@7.20.5:
+ resolution: {integrity: sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/compat-data': 7.20.5
+ '@babel/core': 7.20.5
+ '@babel/helper-validator-option': 7.18.6
+ browserslist: 4.21.4
+ semver: 6.3.0
+ dev: true
+
+ /@babel/helper-create-class-features-plugin/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-member-expression-to-functions': 7.18.9
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/helper-replace-supers': 7.19.1
+ '@babel/helper-split-export-declaration': 7.18.6
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-create-regexp-features-plugin/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-annotate-as-pure': 7.18.6
+ regexpu-core: 5.2.2
+ dev: true
+
+ /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==}
+ peerDependencies:
+ '@babel/core': ^7.4.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ debug: 4.3.4
+ lodash.debounce: 4.0.8
+ resolve: 1.22.1
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-environment-visitor/7.18.9:
+ resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-explode-assignable-expression/7.18.6:
+ resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-function-name/7.19.0:
+ resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.18.10
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-hoist-variables/7.18.6:
+ resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-member-expression-to-functions/7.18.9:
+ resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-module-imports/7.18.6:
+ resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-module-transforms/7.20.2:
+ resolution: {integrity: sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/helper-simple-access': 7.20.2
+ '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/helper-validator-identifier': 7.19.1
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.20.5
+ '@babel/types': 7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-optimise-call-expression/7.18.6:
+ resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-plugin-utils/7.20.2:
+ resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-wrap-function': 7.20.5
+ '@babel/types': 7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-replace-supers/7.19.1:
+ resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-member-expression-to-functions': 7.18.9
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/traverse': 7.20.5
+ '@babel/types': 7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helper-simple-access/7.20.2:
+ resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-skip-transparent-expression-wrappers/7.20.0:
+ resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-split-export-declaration/7.18.6:
+ resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/helper-string-parser/7.19.4:
+ resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-validator-identifier/7.19.1:
+ resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-validator-option/7.18.6:
+ resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /@babel/helper-wrap-function/7.20.5:
+ resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-function-name': 7.19.0
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.20.5
+ '@babel/types': 7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/helpers/7.20.6:
+ resolution: {integrity: sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/template': 7.18.10
+ '@babel/traverse': 7.20.5
+ '@babel/types': 7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/highlight/7.18.6:
+ resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-validator-identifier': 7.19.1
+ chalk: 2.4.2
+ js-tokens: 4.0.0
+ dev: true
+
+ /@babel/parser/7.20.5:
+ resolution: {integrity: sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dependencies:
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-async-generator-functions/7.20.1_@babel+core@7.20.5:
+ resolution: {integrity: sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-object-rest-spread/7.20.2_@babel+core@7.20.5:
+ resolution: {integrity: sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.5
+ '@babel/core': 7.20.5
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-transform-parameters': 7.20.5_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-optional-chaining/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.5
+ dev: true
+
+ /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-private-property-in-object/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-create-class-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.20.5:
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.5:
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-import-assertions/7.20.0_@babel+core@7.20.5:
+ resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.5:
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.20.5:
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-block-scoping/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-classes/7.20.2_@babel+core@7.20.5:
+ resolution: {integrity: sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-optimise-call-expression': 7.18.6
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-replace-supers': 7.19.1
+ '@babel/helper-split-export-declaration': 7.18.6
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-destructuring/7.20.2_@babel+core@7.20.5:
+ resolution: {integrity: sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.20.5:
+ resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-literals/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-modules-amd/7.19.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-module-transforms': 7.20.2
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-commonjs/7.19.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-module-transforms': 7.20.2
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-simple-access': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-systemjs/7.19.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-hoist-variables': 7.18.6
+ '@babel/helper-module-transforms': 7.20.2
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-validator-identifier': 7.19.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-module-transforms': 7.20.2
+ '@babel/helper-plugin-utils': 7.20.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-named-capturing-groups-regex/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-replace-supers': 7.19.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/plugin-transform-parameters/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-regenerator/7.20.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ regenerator-transform: 0.15.1
+ dev: true
+
+ /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-spread/7.19.0_@babel+core@7.20.5:
+ resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-skip-transparent-expression-wrappers': 7.20.0
+ dev: true
+
+ /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.20.5:
+ resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.20.5:
+ resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.20.5:
+ resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ dev: true
+
+ /@babel/preset-env/7.20.2_@babel+core@7.20.5:
+ resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.5
+ '@babel/core': 7.20.5
+ '@babel/helper-compilation-targets': 7.20.0_@babel+core@7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/helper-validator-option': 7.18.6
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-proposal-async-generator-functions': 7.20.1_@babel+core@7.20.5
+ '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-object-rest-spread': 7.20.2_@babel+core@7.20.5
+ '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-proposal-private-property-in-object': 7.20.5_@babel+core@7.20.5
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.5
+ '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.5
+ '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.5
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-import-assertions': 7.20.0_@babel+core@7.20.5
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.5
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.5
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.5
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.5
+ '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.5
+ '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-block-scoping': 7.20.5_@babel+core@7.20.5
+ '@babel/plugin-transform-classes': 7.20.2_@babel+core@7.20.5
+ '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-destructuring': 7.20.2_@babel+core@7.20.5
+ '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.20.5
+ '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-modules-amd': 7.19.6_@babel+core@7.20.5
+ '@babel/plugin-transform-modules-commonjs': 7.19.6_@babel+core@7.20.5
+ '@babel/plugin-transform-modules-systemjs': 7.19.6_@babel+core@7.20.5
+ '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5_@babel+core@7.20.5
+ '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-parameters': 7.20.5_@babel+core@7.20.5
+ '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-regenerator': 7.20.5_@babel+core@7.20.5
+ '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.20.5
+ '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.20.5
+ '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.5
+ '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/preset-modules': 0.1.5_@babel+core@7.20.5
+ '@babel/types': 7.20.5
+ babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.5
+ babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.5
+ babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.5
+ core-js-compat: 3.26.1
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/preset-modules/0.1.5_@babel+core@7.20.5:
+ resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-plugin-utils': 7.20.2
+ '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.5
+ '@babel/types': 7.20.5
+ esutils: 2.0.3
+ dev: true
+
+ /@babel/runtime/7.20.6:
+ resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.13.11
+ dev: true
+
+ /@babel/template/7.18.10:
+ resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ '@babel/parser': 7.20.5
+ '@babel/types': 7.20.5
+ dev: true
+
+ /@babel/traverse/7.20.5:
+ resolution: {integrity: sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ '@babel/generator': 7.20.5
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-hoist-variables': 7.18.6
+ '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/parser': 7.20.5
+ '@babel/types': 7.20.5
+ debug: 4.3.4
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@babel/types/7.20.5:
+ resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-string-parser': 7.19.4
+ '@babel/helper-validator-identifier': 7.19.1
+ to-fast-properties: 2.0.0
+ dev: true
+
+ /@csstools/selector-specificity/2.0.2_tbwh2mpcdwdeb2slx6bobindua:
+ resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==}
+ engines: {node: ^12 || ^14 || >=16}
+ peerDependencies:
+ postcss: ^8.2
+ postcss-selector-parser: ^6.0.10
+ dependencies:
+ postcss: 8.4.19
+ postcss-selector-parser: 6.0.11
+ dev: true
+
+ /@eslint/eslintrc/1.3.3:
+ resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.4
+ espree: 9.4.1
+ globals: 13.18.0
+ ignore: 5.2.1
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@fullhuman/postcss-purgecss/5.0.0_postcss@8.4.19:
+ resolution: {integrity: sha512-onDS/b/2pMRzqSoj4qOs2tYFmOpaspjTAgvACIHMPiicu1ptajiBruTrjBzTKdxWdX0ldaBb7wj8nEaTLyFkJw==}
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ postcss: 8.4.19
+ purgecss: 5.0.0
+ dev: true
+
+ /@humanwhocodes/config-array/0.11.7:
+ resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==}
+ engines: {node: '>=10.10.0'}
+ dependencies:
+ '@humanwhocodes/object-schema': 1.2.1
+ debug: 4.3.4
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@humanwhocodes/module-importer/1.0.1:
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+ dev: true
+
+ /@humanwhocodes/object-schema/1.2.1:
+ resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
+ dev: true
+
+ /@hyas/images/0.3.2:
+ resolution: {integrity: sha512-3khtRISGu4euI2JMViqsb25QtoqlIGAcHyKgSLSj9d3JP4aD//M+PfrzNnIi/NzGu4nTiBfOBtDmR+RijBNK7g==}
+ dev: true
+
+ /@jridgewell/gen-mapping/0.1.1:
+ resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.1.2
+ '@jridgewell/sourcemap-codec': 1.4.14
+ dev: true
+
+ /@jridgewell/gen-mapping/0.3.2:
+ resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ '@jridgewell/set-array': 1.1.2
+ '@jridgewell/sourcemap-codec': 1.4.14
+ '@jridgewell/trace-mapping': 0.3.17
+ dev: true
+
+ /@jridgewell/resolve-uri/3.1.0:
+ resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/set-array/1.1.2:
+ resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
+ /@jridgewell/sourcemap-codec/1.4.14:
+ resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
+ dev: true
+
+ /@jridgewell/trace-mapping/0.3.17:
+ resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.0
+ '@jridgewell/sourcemap-codec': 1.4.14
+ dev: true
+
+ /@nicolo-ribaudo/chokidar-2/2.1.8-no-fsevents.3:
+ resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==}
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@nodelib/fs.scandir/2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat/2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk/1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.14.0
+ dev: true
+
+ /@sindresorhus/is/5.3.0:
+ resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /@szmarczak/http-timer/5.0.1:
+ resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ defer-to-connect: 2.0.1
+ dev: true
+
+ /@types/cacheable-request/6.0.3:
+ resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
+ dependencies:
+ '@types/http-cache-semantics': 4.0.1
+ '@types/keyv': 3.1.4
+ '@types/node': 18.11.13
+ '@types/responselike': 1.0.0
+ dev: true
+
+ /@types/http-cache-semantics/4.0.1:
+ resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
+ dev: true
+
+ /@types/keyv/3.1.4:
+ resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
+ dependencies:
+ '@types/node': 18.11.13
+ dev: true
+
+ /@types/minimist/1.2.2:
+ resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
+ dev: true
+
+ /@types/node/18.11.13:
+ resolution: {integrity: sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w==}
+ dev: true
+
+ /@types/normalize-package-data/2.4.1:
+ resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
+ dev: true
+
+ /@types/parse-json/4.0.0:
+ resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
+ dev: true
+
+ /@types/responselike/1.0.0:
+ resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
+ dependencies:
+ '@types/node': 18.11.13
+ dev: true
+
+ /acorn-jsx/5.3.2_acorn@8.8.1:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ acorn: 8.8.1
+ dev: true
+
+ /acorn/8.8.1:
+ resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+ dev: true
+
+ /aggregate-error/4.0.1:
+ resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==}
+ engines: {node: '>=12'}
+ dependencies:
+ clean-stack: 4.2.0
+ indent-string: 5.0.0
+ dev: true
+
+ /ajv/6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+ dev: true
+
+ /ajv/8.11.2:
+ resolution: {integrity: sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: true
+
+ /ansi-regex/5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ansi-styles/3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+ dependencies:
+ color-convert: 1.9.3
+ dev: true
+
+ /ansi-styles/4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: true
+
+ /anymatch/3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: true
+
+ /argparse/2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ dev: true
+
+ /array-union/2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /arrify/1.0.1:
+ resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /astral-regex/2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /auto-changelog/2.4.0:
+ resolution: {integrity: sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==}
+ engines: {node: '>=8.3'}
+ hasBin: true
+ dependencies:
+ commander: 7.2.0
+ handlebars: 4.7.7
+ node-fetch: 2.6.7
+ parse-github-url: 1.0.2
+ semver: 7.3.8
+ transitivePeerDependencies:
+ - encoding
+ dev: true
+
+ /autoprefixer/10.4.13_postcss@8.4.19:
+ resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ browserslist: 4.21.4
+ caniuse-lite: 1.0.30001439
+ fraction.js: 4.2.0
+ normalize-range: 0.1.2
+ picocolors: 1.0.0
+ postcss: 8.4.19
+ postcss-value-parser: 4.2.0
+ dev: true
+
+ /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.20.5:
+ resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.20.5
+ '@babel/core': 7.20.5
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.20.5:
+ resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5
+ core-js-compat: 3.26.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.20.5:
+ resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.20.5
+ '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /balanced-match/1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: true
+
+ /balanced-match/2.0.0:
+ resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
+ dev: true
+
+ /base64-js/1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ dev: true
+
+ /binary-extensions/2.2.0:
+ resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /bl/1.2.3:
+ resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==}
+ dependencies:
+ readable-stream: 2.3.7
+ safe-buffer: 5.2.1
+ dev: true
+
+ /bootstrap/5.2.3:
+ resolution: {integrity: sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==}
+ peerDependencies:
+ '@popperjs/core': ^2.11.6
+ dev: true
+
+ /brace-expansion/1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: true
+
+ /brace-expansion/2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+ dependencies:
+ balanced-match: 1.0.2
+ dev: true
+
+ /braces/3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /browserslist/4.21.4:
+ resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+ dependencies:
+ caniuse-lite: 1.0.30001439
+ electron-to-chromium: 1.4.284
+ node-releases: 2.0.6
+ update-browserslist-db: 1.0.10_browserslist@4.21.4
+ dev: true
+
+ /buffer-alloc-unsafe/1.1.0:
+ resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
+ dev: true
+
+ /buffer-alloc/1.2.0:
+ resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
+ dependencies:
+ buffer-alloc-unsafe: 1.1.0
+ buffer-fill: 1.0.0
+ dev: true
+
+ /buffer-crc32/0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ dev: true
+
+ /buffer-fill/1.0.0:
+ resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
+ dev: true
+
+ /buffer/5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+ dev: true
+
+ /cacheable-lookup/6.1.0:
+ resolution: {integrity: sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==}
+ engines: {node: '>=10.6.0'}
+ dev: true
+
+ /cacheable-request/7.0.2:
+ resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
+ engines: {node: '>=8'}
+ dependencies:
+ clone-response: 1.0.3
+ get-stream: 5.2.0
+ http-cache-semantics: 4.1.0
+ keyv: 4.5.2
+ lowercase-keys: 2.0.0
+ normalize-url: 6.1.0
+ responselike: 2.0.1
+ dev: true
+
+ /callsites/3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /camelcase-keys/6.2.2:
+ resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
+ engines: {node: '>=8'}
+ dependencies:
+ camelcase: 5.3.1
+ map-obj: 4.3.0
+ quick-lru: 4.0.1
+ dev: true
+
+ /camelcase/5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /caniuse-lite/1.0.30001439:
+ resolution: {integrity: sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==}
+ dev: true
+
+ /chalk/2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ dev: true
+
+ /chalk/4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+ dev: true
+
+ /chokidar/3.5.3:
+ resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.2
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /clean-stack/4.2.0:
+ resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==}
+ engines: {node: '>=12'}
+ dependencies:
+ escape-string-regexp: 5.0.0
+ dev: true
+
+ /clipboard/2.0.11:
+ resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+ dependencies:
+ good-listener: 1.2.2
+ select: 1.1.2
+ tiny-emitter: 2.1.0
+ dev: true
+
+ /cliui/6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+ dev: true
+
+ /cliui/7.0.4:
+ resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: true
+
+ /cliui/8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: true
+
+ /clone-response/1.0.3:
+ resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
+ dependencies:
+ mimic-response: 1.0.1
+ dev: true
+
+ /color-convert/1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+ dependencies:
+ color-name: 1.1.3
+ dev: true
+
+ /color-convert/2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
+ /color-name/1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+ dev: true
+
+ /color-name/1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: true
+
+ /colord/2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+ dev: true
+
+ /commander/2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+ dev: true
+
+ /commander/4.1.1:
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /commander/7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+ dev: true
+
+ /commander/8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+ dev: true
+
+ /commander/9.4.1:
+ resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==}
+ engines: {node: ^12.20.0 || >=14}
+ dev: true
+
+ /concat-map/0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: true
+
+ /convert-source-map/1.9.0:
+ resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+ dev: true
+
+ /core-js-compat/3.26.1:
+ resolution: {integrity: sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==}
+ dependencies:
+ browserslist: 4.21.4
+ dev: true
+
+ /core-util-is/1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ dev: true
+
+ /cosmiconfig/7.1.0:
+ resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
+ engines: {node: '>=10'}
+ dependencies:
+ '@types/parse-json': 4.0.0
+ import-fresh: 3.3.0
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ yaml: 1.10.2
+ dev: true
+
+ /cross-spawn/7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ dev: true
+
+ /css-functions-list/3.1.0:
+ resolution: {integrity: sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==}
+ engines: {node: '>=12.22'}
+ dev: true
+
+ /cssesc/3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /data-uri-to-buffer/4.0.0:
+ resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==}
+ engines: {node: '>= 12'}
+ dev: true
+
+ /debug/4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+ dev: true
+
+ /decamelize-keys/1.1.1:
+ resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ decamelize: 1.2.0
+ map-obj: 1.0.1
+ dev: true
+
+ /decamelize/1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /decompress-response/6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ mimic-response: 3.1.0
+ dev: true
+
+ /decompress-tar/4.1.1:
+ resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ file-type: 5.2.0
+ is-stream: 1.1.0
+ tar-stream: 1.6.2
+ dev: true
+
+ /decompress-tarbz2/4.1.1:
+ resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==}
+ engines: {node: '>=4'}
+ dependencies:
+ decompress-tar: 4.1.1
+ file-type: 6.2.0
+ is-stream: 1.1.0
+ seek-bzip: 1.0.6
+ unbzip2-stream: 1.4.3
+ dev: true
+
+ /decompress-targz/4.1.1:
+ resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==}
+ engines: {node: '>=4'}
+ dependencies:
+ decompress-tar: 4.1.1
+ file-type: 5.2.0
+ is-stream: 1.1.0
+ dev: true
+
+ /decompress-unzip/4.0.1:
+ resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==}
+ engines: {node: '>=4'}
+ dependencies:
+ file-type: 3.9.0
+ get-stream: 2.3.1
+ pify: 2.3.0
+ yauzl: 2.10.0
+ dev: true
+
+ /decompress/4.2.1:
+ resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ decompress-tar: 4.1.1
+ decompress-tarbz2: 4.1.1
+ decompress-targz: 4.1.1
+ decompress-unzip: 4.0.1
+ graceful-fs: 4.2.10
+ make-dir: 1.3.0
+ pify: 2.3.0
+ strip-dirs: 2.1.0
+ dev: true
+
+ /deep-is/0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ dev: true
+
+ /defer-to-connect/2.0.1:
+ resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /del/7.0.0:
+ resolution: {integrity: sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ globby: 13.1.2
+ graceful-fs: 4.2.10
+ is-glob: 4.0.3
+ is-path-cwd: 3.0.0
+ is-path-inside: 4.0.0
+ p-map: 5.5.0
+ rimraf: 3.0.2
+ slash: 4.0.0
+ dev: true
+
+ /delegate/3.2.0:
+ resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
+ dev: true
+
+ /dependency-graph/0.11.0:
+ resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==}
+ engines: {node: '>= 0.6.0'}
+ dev: true
+
+ /dir-glob/3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-type: 4.0.0
+ dev: true
+
+ /doctrine/3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ esutils: 2.0.3
+ dev: true
+
+ /electron-to-chromium/1.4.284:
+ resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
+ dev: true
+
+ /emoji-regex/8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: true
+
+ /end-of-stream/1.4.4:
+ resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ dependencies:
+ once: 1.4.0
+ dev: true
+
+ /entities/3.0.1:
+ resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
+ engines: {node: '>=0.12'}
+ dev: true
+
+ /error-ex/1.3.2:
+ resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+ dependencies:
+ is-arrayish: 0.2.1
+ dev: true
+
+ /escalade/3.1.1:
+ resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /escape-string-regexp/1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+ dev: true
+
+ /escape-string-regexp/4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /escape-string-regexp/5.0.0:
+ resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /eslint-scope/7.1.1:
+ resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+ dev: true
+
+ /eslint-utils/3.0.0_eslint@8.29.0:
+ resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
+ engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
+ peerDependencies:
+ eslint: '>=5'
+ dependencies:
+ eslint: 8.29.0
+ eslint-visitor-keys: 2.1.0
+ dev: true
+
+ /eslint-visitor-keys/2.1.0:
+ resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /eslint-visitor-keys/3.3.0:
+ resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dev: true
+
+ /eslint/8.29.0:
+ resolution: {integrity: sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ '@eslint/eslintrc': 1.3.3
+ '@humanwhocodes/config-array': 0.11.7
+ '@humanwhocodes/module-importer': 1.0.1
+ '@nodelib/fs.walk': 1.2.8
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.3
+ debug: 4.3.4
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.1.1
+ eslint-utils: 3.0.0_eslint@8.29.0
+ eslint-visitor-keys: 3.3.0
+ espree: 9.4.1
+ esquery: 1.4.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ globals: 13.18.0
+ grapheme-splitter: 1.0.4
+ ignore: 5.2.1
+ import-fresh: 3.3.0
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-sdsl: 4.2.0
+ js-yaml: 4.1.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.1
+ regexpp: 3.2.0
+ strip-ansi: 6.0.1
+ strip-json-comments: 3.1.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /espree/9.4.1:
+ resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ dependencies:
+ acorn: 8.8.1
+ acorn-jsx: 5.3.2_acorn@8.8.1
+ eslint-visitor-keys: 3.3.0
+ dev: true
+
+ /esquery/1.4.0:
+ resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
+ engines: {node: '>=0.10'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /esrecurse/4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+ dependencies:
+ estraverse: 5.3.0
+ dev: true
+
+ /estraverse/5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+ dev: true
+
+ /esutils/2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /exec-bin/1.0.0:
+ resolution: {integrity: sha512-p8f8h8b6op2nR7U5rsd+zACUMfsfB+jW8HNIBD2njOQ/gF2WvBfQRo/OU6Q6f/b34WLAyePZcwMJyrDdEjB/fw==}
+ hasBin: true
+ dev: true
+
+ /fast-deep-equal/3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: true
+
+ /fast-glob/3.2.12:
+ resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
+ /fast-json-stable-stringify/2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+ dev: true
+
+ /fast-levenshtein/2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ dev: true
+
+ /fastest-levenshtein/1.0.16:
+ resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
+ engines: {node: '>= 4.9.1'}
+ dev: true
+
+ /fastq/1.14.0:
+ resolution: {integrity: sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
+ /fd-slicer/1.1.0:
+ resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
+ dependencies:
+ pend: 1.2.0
+ dev: true
+
+ /fetch-blob/3.2.0:
+ resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+ engines: {node: ^12.20 || >= 14.13}
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 3.2.1
+ dev: true
+
+ /file-entry-cache/6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+ dependencies:
+ flat-cache: 3.0.4
+ dev: true
+
+ /file-type/3.9.0:
+ resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /file-type/5.2.0:
+ resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /file-type/6.2.0:
+ resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /fill-range/7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /find-up/4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /find-up/5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+ dev: true
+
+ /flat-cache/3.0.4:
+ resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+ dependencies:
+ flatted: 3.2.7
+ rimraf: 3.0.2
+ dev: true
+
+ /flatted/3.2.7:
+ resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
+ dev: true
+
+ /flexsearch/0.7.31:
+ resolution: {integrity: sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==}
+ dev: true
+
+ /form-data-encoder/2.1.4:
+ resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
+ engines: {node: '>= 14.17'}
+ dev: true
+
+ /formdata-polyfill/4.0.10:
+ resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+ engines: {node: '>=12.20.0'}
+ dependencies:
+ fetch-blob: 3.2.0
+ dev: true
+
+ /fraction.js/4.2.0:
+ resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
+ dev: true
+
+ /fs-constants/1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ dev: true
+
+ /fs-extra/11.1.0:
+ resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
+ engines: {node: '>=14.14'}
+ dependencies:
+ graceful-fs: 4.2.10
+ jsonfile: 6.1.0
+ universalify: 2.0.0
+ dev: true
+
+ /fs-readdir-recursive/1.1.0:
+ resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==}
+ dev: true
+
+ /fs.realpath/1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ dev: true
+
+ /fsevents/2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /function-bind/1.1.1:
+ resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+ dev: true
+
+ /gensync/1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
+ /get-caller-file/2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+ dev: true
+
+ /get-stdin/9.0.0:
+ resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /get-stream/2.3.1:
+ resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ object-assign: 4.1.1
+ pinkie-promise: 2.0.1
+ dev: true
+
+ /get-stream/5.2.0:
+ resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
+ engines: {node: '>=8'}
+ dependencies:
+ pump: 3.0.0
+ dev: true
+
+ /get-stream/6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /glob-all/3.3.1:
+ resolution: {integrity: sha512-Y+ESjdI7ZgMwfzanHZYQ87C59jOO0i+Hd+QYtVt9PhLi6d8wlOpzQnfBxWUlaTuAoR3TkybLqqbIoWveU4Ji7Q==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ yargs: 15.4.1
+ dev: true
+
+ /glob-parent/5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob-parent/6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob/7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+ dev: true
+
+ /glob/8.0.3:
+ resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 5.1.1
+ once: 1.4.0
+ dev: true
+
+ /global-modules/2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+ dependencies:
+ global-prefix: 3.0.0
+ dev: true
+
+ /global-prefix/3.0.0:
+ resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
+ engines: {node: '>=6'}
+ dependencies:
+ ini: 1.3.8
+ kind-of: 6.0.3
+ which: 1.3.1
+ dev: true
+
+ /globals/11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /globals/13.18.0:
+ resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==}
+ engines: {node: '>=8'}
+ dependencies:
+ type-fest: 0.20.2
+ dev: true
+
+ /globby/11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.2.12
+ ignore: 5.2.1
+ merge2: 1.4.1
+ slash: 3.0.0
+ dev: true
+
+ /globby/13.1.2:
+ resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ dir-glob: 3.0.1
+ fast-glob: 3.2.12
+ ignore: 5.2.1
+ merge2: 1.4.1
+ slash: 4.0.0
+ dev: true
+
+ /globjoin/0.1.4:
+ resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
+ dev: true
+
+ /gonzales-pe/4.3.0:
+ resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==}
+ engines: {node: '>=0.6.0'}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.7
+ dev: true
+
+ /good-listener/1.2.2:
+ resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
+ dependencies:
+ delegate: 3.2.0
+ dev: true
+
+ /got/12.4.1:
+ resolution: {integrity: sha512-Sz1ojLt4zGNkcftIyJKnulZT/yEDvifhUjccHA8QzOuTgPs/+njXYNMFE3jR4/2OODQSSbH8SdnoLCkbh41ieA==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ '@sindresorhus/is': 5.3.0
+ '@szmarczak/http-timer': 5.0.1
+ '@types/cacheable-request': 6.0.3
+ cacheable-lookup: 6.1.0
+ cacheable-request: 7.0.2
+ decompress-response: 6.0.0
+ form-data-encoder: 2.1.4
+ get-stream: 6.0.1
+ http2-wrapper: 2.2.0
+ lowercase-keys: 3.0.0
+ p-cancelable: 3.0.0
+ responselike: 3.0.0
+ dev: true
+
+ /graceful-fs/4.2.10:
+ resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
+ dev: true
+
+ /grapheme-splitter/1.0.4:
+ resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
+ dev: true
+
+ /handlebars/4.7.7:
+ resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
+ engines: {node: '>=0.4.7'}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.7
+ neo-async: 2.6.2
+ source-map: 0.6.1
+ wordwrap: 1.0.0
+ optionalDependencies:
+ uglify-js: 3.17.4
+ dev: true
+
+ /hard-rejection/2.1.0:
+ resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /has-flag/3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /has-flag/4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /has/1.0.3:
+ resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ function-bind: 1.1.1
+ dev: true
+
+ /highlight.js/11.7.0:
+ resolution: {integrity: sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==}
+ engines: {node: '>=12.0.0'}
+ dev: true
+
+ /hosted-git-info/2.8.9:
+ resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+ dev: true
+
+ /hosted-git-info/4.1.0:
+ resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
+ engines: {node: '>=10'}
+ dependencies:
+ lru-cache: 6.0.0
+ dev: true
+
+ /hpagent/1.0.0:
+ resolution: {integrity: sha512-SCleE2Uc1bM752ymxg8QXYGW0TWtAV4ZW3TqH1aOnyi6T6YW2xadCcclm5qeVjvMvfQ2RKNtZxO7uVb9CTPt1A==}
+ engines: {node: '>=14'}
+ dev: true
+
+ /html-tags/3.2.0:
+ resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /http-cache-semantics/4.1.0:
+ resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
+ dev: true
+
+ /http2-wrapper/2.2.0:
+ resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==}
+ engines: {node: '>=10.19.0'}
+ dependencies:
+ quick-lru: 5.1.1
+ resolve-alpn: 1.2.1
+ dev: true
+
+ /hugo-installer/4.0.1:
+ resolution: {integrity: sha512-pkp1RO7+ekQ0vw1aqgBMK+dD2dqioIWVbwWKsJsKLOpzfFc78gK68Cweoi/g+CftoiMFO7cyGx/2MgkHCMqaLQ==}
+ hasBin: true
+ dependencies:
+ decompress: 4.2.1
+ del: 7.0.0
+ got: 12.4.1
+ hpagent: 1.0.0
+ object-path: 0.11.8
+ semver: 7.3.8
+ yargs: 17.5.1
+ dev: true
+
+ /ieee754/1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ dev: true
+
+ /ignore/5.2.1:
+ resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==}
+ engines: {node: '>= 4'}
+ dev: true
+
+ /import-fresh/3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ dev: true
+
+ /import-lazy/4.0.0:
+ resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /imurmurhash/0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+ dev: true
+
+ /indent-string/4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /indent-string/5.0.0:
+ resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /inflight/1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+ dev: true
+
+ /inherits/2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ dev: true
+
+ /ini/1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ dev: true
+
+ /instant.page/5.1.1:
+ resolution: {integrity: sha512-7FhcXQ+FSUjN8pqBFVDbwJwcXsV6mPLmQosdb1FYbZGaL9TWUPg08yHaK2RdmgnkJ6lPzEp4T3Opx/HcFDUbRQ==}
+ dev: true
+
+ /interpret/1.4.0:
+ resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
+ engines: {node: '>= 0.10'}
+ dev: true
+
+ /invariant/2.2.2:
+ resolution: {integrity: sha512-FUiAFCOgp7bBzHfa/fK+Uc/vqywvdN9Wg3CiTprLcE630mrhxjDS5MlBkHzeI6+bC/6bq9VX/hxBt05fPAT5WA==}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: true
+
+ /is-arrayish/0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+ dev: true
+
+ /is-binary-path/2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.2.0
+ dev: true
+
+ /is-core-module/2.11.0:
+ resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==}
+ dependencies:
+ has: 1.0.3
+ dev: true
+
+ /is-extglob/2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-fullwidth-code-point/3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-glob/4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-natural-number/4.0.1:
+ resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==}
+ dev: true
+
+ /is-number/7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-path-cwd/3.0.0:
+ resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /is-path-inside/3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-path-inside/4.0.0:
+ resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /is-plain-obj/1.1.0:
+ resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-plain-object/5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-stream/1.1.0:
+ resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /isarray/1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+ dev: true
+
+ /isexe/2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
+ /js-sdsl/4.2.0:
+ resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
+ dev: true
+
+ /js-tokens/4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: true
+
+ /js-yaml/4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+ dev: true
+
+ /jsesc/0.5.0:
+ resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
+ hasBin: true
+ dev: true
+
+ /jsesc/2.5.2:
+ resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: true
+
+ /json-buffer/3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ dev: true
+
+ /json-parse-even-better-errors/2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+ dev: true
+
+ /json-schema-traverse/0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ dev: true
+
+ /json-schema-traverse/1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: true
+
+ /json-stable-stringify-without-jsonify/1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ dev: true
+
+ /json5/2.2.1:
+ resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==}
+ engines: {node: '>=6'}
+ hasBin: true
+ dev: true
+
+ /jsonfile/6.1.0:
+ resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+ dependencies:
+ universalify: 2.0.0
+ optionalDependencies:
+ graceful-fs: 4.2.10
+ dev: true
+
+ /katex/0.16.4:
+ resolution: {integrity: sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==}
+ hasBin: true
+ dependencies:
+ commander: 8.3.0
+ dev: true
+
+ /keyv/4.5.2:
+ resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
+ dependencies:
+ json-buffer: 3.0.1
+ dev: true
+
+ /kind-of/6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /known-css-properties/0.26.0:
+ resolution: {integrity: sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==}
+ dev: true
+
+ /lazysizes/5.3.2:
+ resolution: {integrity: sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==}
+ dev: true
+
+ /levn/0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ dev: true
+
+ /lilconfig/2.0.6:
+ resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /lines-and-columns/1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ dev: true
+
+ /linkify-it/4.0.1:
+ resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
+ dependencies:
+ uc.micro: 1.0.6
+ dev: true
+
+ /locate-path/5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-locate: 4.1.0
+ dev: true
+
+ /locate-path/6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-locate: 5.0.0
+ dev: true
+
+ /lodash.debounce/4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+ dev: true
+
+ /lodash.merge/4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ dev: true
+
+ /lodash.truncate/4.4.2:
+ resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+ dev: true
+
+ /lodash/4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ dev: true
+
+ /loose-envify/1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+ dependencies:
+ js-tokens: 4.0.0
+ dev: true
+
+ /lowercase-keys/2.0.0:
+ resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /lowercase-keys/3.0.0:
+ resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /lru-cache/6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+ dependencies:
+ yallist: 4.0.0
+ dev: true
+
+ /make-dir/1.3.0:
+ resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ pify: 3.0.0
+ dev: true
+
+ /make-dir/2.1.0:
+ resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
+ engines: {node: '>=6'}
+ dependencies:
+ pify: 4.0.1
+ semver: 5.7.1
+ dev: true
+
+ /map-obj/1.0.1:
+ resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /map-obj/4.3.0:
+ resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /markdown-it/13.0.1:
+ resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+ entities: 3.0.1
+ linkify-it: 4.0.1
+ mdurl: 1.0.1
+ uc.micro: 1.0.6
+ dev: true
+
+ /markdownlint-cli2-formatter-default/0.0.3_markdownlint-cli2@0.5.1:
+ resolution: {integrity: sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==}
+ peerDependencies:
+ markdownlint-cli2: '>=0.0.4'
+ dependencies:
+ markdownlint-cli2: 0.5.1
+ dev: true
+
+ /markdownlint-cli2/0.5.1:
+ resolution: {integrity: sha512-f3Nb1GF/c8YSrV/FntsCWzpa5mLFJRlO+wzEgv+lkNQjU6MZflUwc2FbyEDPTo6oVhP2VyUOkK0GkFgfuktl1w==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ globby: 13.1.2
+ markdownlint: 0.26.2
+ markdownlint-cli2-formatter-default: 0.0.3_markdownlint-cli2@0.5.1
+ micromatch: 4.0.5
+ strip-json-comments: 5.0.0
+ yaml: 2.1.1
+ dev: true
+
+ /markdownlint/0.26.2:
+ resolution: {integrity: sha512-2Am42YX2Ex5SQhRq35HxYWDfz1NLEOZWWN25nqd2h3AHRKsGRE+Qg1gt1++exW792eXTrR4jCNHfShfWk9Nz8w==}
+ engines: {node: '>=14'}
+ dependencies:
+ markdown-it: 13.0.1
+ dev: true
+
+ /mathml-tag-names/2.1.3:
+ resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
+ dev: true
+
+ /mdurl/1.0.1:
+ resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
+ dev: true
+
+ /meow/9.0.0:
+ resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ '@types/minimist': 1.2.2
+ camelcase-keys: 6.2.2
+ decamelize: 1.2.0
+ decamelize-keys: 1.1.1
+ hard-rejection: 2.1.0
+ minimist-options: 4.1.0
+ normalize-package-data: 3.0.3
+ read-pkg-up: 7.0.1
+ redent: 3.0.0
+ trim-newlines: 3.0.1
+ type-fest: 0.18.1
+ yargs-parser: 20.2.9
+ dev: true
+
+ /merge2/1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /micromatch/4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /mimic-response/1.0.1:
+ resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /mimic-response/3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /min-indent/1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /minimatch/3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+
+ /minimatch/5.1.1:
+ resolution: {integrity: sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==}
+ engines: {node: '>=10'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: true
+
+ /minimist-options/4.1.0:
+ resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
+ engines: {node: '>= 6'}
+ dependencies:
+ arrify: 1.0.1
+ is-plain-obj: 1.1.0
+ kind-of: 6.0.3
+ dev: true
+
+ /minimist/1.2.7:
+ resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
+ dev: true
+
+ /ms/2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ dev: true
+
+ /nanoid/3.3.4:
+ resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /natural-compare/1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ dev: true
+
+ /neo-async/2.6.2:
+ resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ dev: true
+
+ /netlify-plugin-submit-sitemap/0.4.0:
+ resolution: {integrity: sha512-5ntDtSKZRHaCDrDXh4sH4V7lNEEsoi01lsmSUuqJ/ikPHf0XEErjsKba8TsM3iaZRYEHI9bQse3BWgguwuwIIQ==}
+ dependencies:
+ node-fetch: 3.3.0
+ dev: true
+
+ /node-domexception/1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ dev: true
+
+ /node-fetch/2.6.7:
+ resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: true
+
+ /node-fetch/3.3.0:
+ resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ data-uri-to-buffer: 4.0.0
+ fetch-blob: 3.2.0
+ formdata-polyfill: 4.0.10
+ dev: true
+
+ /node-releases/2.0.6:
+ resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
+ dev: true
+
+ /normalize-package-data/2.5.0:
+ resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
+ dependencies:
+ hosted-git-info: 2.8.9
+ resolve: 1.22.1
+ semver: 5.7.1
+ validate-npm-package-license: 3.0.4
+ dev: true
+
+ /normalize-package-data/3.0.3:
+ resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
+ engines: {node: '>=10'}
+ dependencies:
+ hosted-git-info: 4.1.0
+ is-core-module: 2.11.0
+ semver: 7.3.8
+ validate-npm-package-license: 3.0.4
+ dev: true
+
+ /normalize-path/3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /normalize-range/0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /normalize-url/6.1.0:
+ resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /object-assign/4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /object-path/0.11.8:
+ resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==}
+ engines: {node: '>= 10.12.0'}
+ dev: true
+
+ /once/1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+ dev: true
+
+ /optionator/0.9.1:
+ resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.3
+ dev: true
+
+ /p-cancelable/3.0.0:
+ resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
+ engines: {node: '>=12.20'}
+ dev: true
+
+ /p-limit/2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+ dependencies:
+ p-try: 2.2.0
+ dev: true
+
+ /p-limit/3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ yocto-queue: 0.1.0
+ dev: true
+
+ /p-locate/4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-limit: 2.3.0
+ dev: true
+
+ /p-locate/5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+ dependencies:
+ p-limit: 3.1.0
+ dev: true
+
+ /p-map/5.5.0:
+ resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==}
+ engines: {node: '>=12'}
+ dependencies:
+ aggregate-error: 4.0.1
+ dev: true
+
+ /p-try/2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /parent-module/1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+ dependencies:
+ callsites: 3.1.0
+ dev: true
+
+ /parse-github-url/1.0.2:
+ resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+ dev: true
+
+ /parse-json/5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ error-ex: 1.3.2
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+ dev: true
+
+ /path-exists/4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-is-absolute/1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /path-key/3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-parse/1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ dev: true
+
+ /path-type/4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /pend/1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+ dev: true
+
+ /picocolors/1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+ dev: true
+
+ /picomatch/2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /pify/2.3.0:
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /pify/3.0.0:
+ resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /pify/4.0.1:
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /pinkie-promise/2.0.1:
+ resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ pinkie: 2.0.4
+ dev: true
+
+ /pinkie/2.0.4:
+ resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /postcss-cli/10.1.0_postcss@8.4.19:
+ resolution: {integrity: sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==}
+ engines: {node: '>=14'}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ chokidar: 3.5.3
+ dependency-graph: 0.11.0
+ fs-extra: 11.1.0
+ get-stdin: 9.0.0
+ globby: 13.1.2
+ picocolors: 1.0.0
+ postcss: 8.4.19
+ postcss-load-config: 4.0.1_postcss@8.4.19
+ postcss-reporter: 7.0.5_postcss@8.4.19
+ pretty-hrtime: 1.0.3
+ read-cache: 1.0.0
+ slash: 5.0.0
+ yargs: 17.6.2
+ transitivePeerDependencies:
+ - ts-node
+ dev: true
+
+ /postcss-load-config/4.0.1_postcss@8.4.19:
+ resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ postcss: '>=8.0.9'
+ ts-node: '>=9.0.0'
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ ts-node:
+ optional: true
+ dependencies:
+ lilconfig: 2.0.6
+ postcss: 8.4.19
+ yaml: 2.1.3
+ dev: true
+
+ /postcss-media-query-parser/0.2.3:
+ resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
+ dev: true
+
+ /postcss-reporter/7.0.5_postcss@8.4.19:
+ resolution: {integrity: sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ picocolors: 1.0.0
+ postcss: 8.4.19
+ thenby: 1.3.4
+ dev: true
+
+ /postcss-resolve-nested-selector/0.1.1:
+ resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==}
+ dev: true
+
+ /postcss-safe-parser/6.0.0_postcss@8.4.19:
+ resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.3.3
+ dependencies:
+ postcss: 8.4.19
+ dev: true
+
+ /postcss-scss/4.0.6_postcss@8.4.19:
+ resolution: {integrity: sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.4.19
+ dependencies:
+ postcss: 8.4.19
+ dev: true
+
+ /postcss-selector-parser/6.0.11:
+ resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==}
+ engines: {node: '>=4'}
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+ dev: true
+
+ /postcss-value-parser/4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+ dev: true
+
+ /postcss/8.4.19:
+ resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.4
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /prelude-ls/1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+ dev: true
+
+ /pretty-hrtime/1.0.3:
+ resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /process-nextick-args/2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+ dev: true
+
+ /pump/3.0.0:
+ resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: true
+
+ /punycode/2.1.1:
+ resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /purgecss-whitelister/2.4.0:
+ resolution: {integrity: sha512-O0jBUDtY9dU9tUT0vA1FvwFdkKDerxzteYaBV49JCbm+QJLFKMlIsf5Kp5cdbLatHQNjJtV8VB8eXtISoZL2Dg==}
+ dependencies:
+ glob-all: 3.3.1
+ gonzales-pe: 4.3.0
+ scss-parser: 1.0.3
+ dev: true
+
+ /purgecss/5.0.0:
+ resolution: {integrity: sha512-RAnuxrGuVyLLTr8uMbKaxDRGWMgK5CCYDfRyUNNcaz5P3kGgD2b7ymQGYEyo2ST7Tl/ScwFgf5l3slKMxHSbrw==}
+ hasBin: true
+ dependencies:
+ commander: 9.4.1
+ glob: 8.0.3
+ postcss: 8.4.19
+ postcss-selector-parser: 6.0.11
+ dev: true
+
+ /queue-microtask/1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
+ /quick-lru/4.0.1:
+ resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /quick-lru/5.1.1:
+ resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /read-cache/1.0.0:
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+ dependencies:
+ pify: 2.3.0
+ dev: true
+
+ /read-pkg-up/7.0.1:
+ resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
+ engines: {node: '>=8'}
+ dependencies:
+ find-up: 4.1.0
+ read-pkg: 5.2.0
+ type-fest: 0.8.1
+ dev: true
+
+ /read-pkg/5.2.0:
+ resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
+ engines: {node: '>=8'}
+ dependencies:
+ '@types/normalize-package-data': 2.4.1
+ normalize-package-data: 2.5.0
+ parse-json: 5.2.0
+ type-fest: 0.6.0
+ dev: true
+
+ /readable-stream/2.3.7:
+ resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==}
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+ dev: true
+
+ /readdirp/3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: true
+
+ /rechoir/0.6.2:
+ resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
+ engines: {node: '>= 0.10'}
+ dependencies:
+ resolve: 1.22.1
+ dev: true
+
+ /redent/3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+ dev: true
+
+ /regenerate-unicode-properties/10.1.0:
+ resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ regenerate: 1.4.2
+ dev: true
+
+ /regenerate/1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+ dev: true
+
+ /regenerator-runtime/0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+ dev: true
+
+ /regenerator-transform/0.15.1:
+ resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
+ dependencies:
+ '@babel/runtime': 7.20.6
+ dev: true
+
+ /regexpp/3.2.0:
+ resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /regexpu-core/5.2.2:
+ resolution: {integrity: sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==}
+ engines: {node: '>=4'}
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.1.0
+ regjsgen: 0.7.1
+ regjsparser: 0.9.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.1.0
+ dev: true
+
+ /regjsgen/0.7.1:
+ resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==}
+ dev: true
+
+ /regjsparser/0.9.1:
+ resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==}
+ hasBin: true
+ dependencies:
+ jsesc: 0.5.0
+ dev: true
+
+ /require-directory/2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /require-from-string/2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /require-main-filename/2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+ dev: true
+
+ /resolve-alpn/1.2.1:
+ resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
+ dev: true
+
+ /resolve-from/4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /resolve-from/5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /resolve/1.22.1:
+ resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.11.0
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+ dev: true
+
+ /responselike/2.0.1:
+ resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
+ dependencies:
+ lowercase-keys: 2.0.0
+ dev: true
+
+ /responselike/3.0.0:
+ resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ lowercase-keys: 3.0.0
+ dev: true
+
+ /reusify/1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /rimraf/3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /run-parallel/1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
+ /safe-buffer/5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+ dev: true
+
+ /safe-buffer/5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: true
+
+ /scss-parser/1.0.3:
+ resolution: {integrity: sha512-XQKCfOJERmhn1yoNRUyxv9wgkf4DIv29Jk0m4FiZforeiCmGxrby8K3not7tQ8GK1yvtd9N0OnNimNetJ8V+zQ==}
+ engines: {node: '>=6.0.0'}
+ dependencies:
+ invariant: 2.2.2
+ lodash: 4.17.21
+ dev: true
+
+ /seek-bzip/1.0.6:
+ resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==}
+ hasBin: true
+ dependencies:
+ commander: 2.20.3
+ dev: true
+
+ /select/1.1.2:
+ resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
+ dev: true
+
+ /semver/5.7.1:
+ resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ hasBin: true
+ dev: true
+
+ /semver/6.3.0:
+ resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
+ hasBin: true
+ dev: true
+
+ /semver/7.3.8:
+ resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ lru-cache: 6.0.0
+ dev: true
+
+ /set-blocking/2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+ dev: true
+
+ /shebang-command/2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+ dev: true
+
+ /shebang-regex/3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /shelljs/0.8.5:
+ resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ interpret: 1.4.0
+ rechoir: 0.6.2
+ dev: true
+
+ /shx/0.3.4:
+ resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==}
+ engines: {node: '>=6'}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.7
+ shelljs: 0.8.5
+ dev: true
+
+ /signal-exit/3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: true
+
+ /slash/2.0.0:
+ resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /slash/3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /slash/4.0.0:
+ resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /slash/5.0.0:
+ resolution: {integrity: sha512-n6KkmvKS0623igEVj3FF0OZs1gYYJ0o0Hj939yc1fyxl2xt+xYpLnzJB6xBSqOfV9ZFLEWodBBN/heZJahuIJQ==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /slice-ansi/4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+ dev: true
+
+ /source-map-js/1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /source-map/0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /spdx-correct/3.1.1:
+ resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
+ dependencies:
+ spdx-expression-parse: 3.0.1
+ spdx-license-ids: 3.0.12
+ dev: true
+
+ /spdx-exceptions/2.3.0:
+ resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
+ dev: true
+
+ /spdx-expression-parse/3.0.1:
+ resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
+ dependencies:
+ spdx-exceptions: 2.3.0
+ spdx-license-ids: 3.0.12
+ dev: true
+
+ /spdx-license-ids/3.0.12:
+ resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==}
+ dev: true
+
+ /string-width/4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: true
+
+ /string_decoder/1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+ dependencies:
+ safe-buffer: 5.1.2
+ dev: true
+
+ /strip-ansi/6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+ dev: true
+
+ /strip-dirs/2.1.0:
+ resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==}
+ dependencies:
+ is-natural-number: 4.0.1
+ dev: true
+
+ /strip-indent/3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ min-indent: 1.0.1
+ dev: true
+
+ /strip-json-comments/3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /strip-json-comments/5.0.0:
+ resolution: {integrity: sha512-V1LGY4UUo0jgwC+ELQ2BNWfPa17TIuwBLg+j1AA/9RPzKINl1lhxVEu2r+ZTTO8aetIsUzE5Qj6LMSBkoGYKKw==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /style-search/0.1.0:
+ resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==}
+ dev: true
+
+ /stylelint-config-recommended-scss/8.0.0_u4cmdib575x7lmfjhgvokchuwe:
+ resolution: {integrity: sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ==}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^14.10.0
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ dependencies:
+ postcss: 8.4.19
+ postcss-scss: 4.0.6_postcss@8.4.19
+ stylelint: 14.16.0
+ stylelint-config-recommended: 9.0.0_stylelint@14.16.0
+ stylelint-scss: 4.3.0_stylelint@14.16.0
+ dev: true
+
+ /stylelint-config-recommended/9.0.0_stylelint@14.16.0:
+ resolution: {integrity: sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==}
+ peerDependencies:
+ stylelint: ^14.10.0
+ dependencies:
+ stylelint: 14.16.0
+ dev: true
+
+ /stylelint-config-standard-scss/6.1.0_u4cmdib575x7lmfjhgvokchuwe:
+ resolution: {integrity: sha512-iZ2B5kQT2G3rUzx+437cEpdcnFOQkwnwqXuY8Z0QUwIHQVE8mnYChGAquyKFUKZRZ0pRnrciARlPaR1RBtPb0Q==}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^14.14.0
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+ dependencies:
+ postcss: 8.4.19
+ stylelint: 14.16.0
+ stylelint-config-recommended-scss: 8.0.0_u4cmdib575x7lmfjhgvokchuwe
+ stylelint-config-standard: 29.0.0_stylelint@14.16.0
+ dev: true
+
+ /stylelint-config-standard/29.0.0_stylelint@14.16.0:
+ resolution: {integrity: sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg==}
+ peerDependencies:
+ stylelint: ^14.14.0
+ dependencies:
+ stylelint: 14.16.0
+ stylelint-config-recommended: 9.0.0_stylelint@14.16.0
+ dev: true
+
+ /stylelint-scss/4.3.0_stylelint@14.16.0:
+ resolution: {integrity: sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ==}
+ peerDependencies:
+ stylelint: ^14.5.1
+ dependencies:
+ lodash: 4.17.21
+ postcss-media-query-parser: 0.2.3
+ postcss-resolve-nested-selector: 0.1.1
+ postcss-selector-parser: 6.0.11
+ postcss-value-parser: 4.2.0
+ stylelint: 14.16.0
+ dev: true
+
+ /stylelint/14.16.0:
+ resolution: {integrity: sha512-X6uTi9DcxjzLV8ZUAjit1vsRtSwcls0nl07c9rqOPzvpA8IvTX/xWEkBRowS0ffevRrqkHa/ThDEu86u73FQDg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ '@csstools/selector-specificity': 2.0.2_tbwh2mpcdwdeb2slx6bobindua
+ balanced-match: 2.0.0
+ colord: 2.9.3
+ cosmiconfig: 7.1.0
+ css-functions-list: 3.1.0
+ debug: 4.3.4
+ fast-glob: 3.2.12
+ fastest-levenshtein: 1.0.16
+ file-entry-cache: 6.0.1
+ global-modules: 2.0.0
+ globby: 11.1.0
+ globjoin: 0.1.4
+ html-tags: 3.2.0
+ ignore: 5.2.1
+ import-lazy: 4.0.0
+ imurmurhash: 0.1.4
+ is-plain-object: 5.0.0
+ known-css-properties: 0.26.0
+ mathml-tag-names: 2.1.3
+ meow: 9.0.0
+ micromatch: 4.0.5
+ normalize-path: 3.0.0
+ picocolors: 1.0.0
+ postcss: 8.4.19
+ postcss-media-query-parser: 0.2.3
+ postcss-resolve-nested-selector: 0.1.1
+ postcss-safe-parser: 6.0.0_postcss@8.4.19
+ postcss-selector-parser: 6.0.11
+ postcss-value-parser: 4.2.0
+ resolve-from: 5.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ style-search: 0.1.0
+ supports-hyperlinks: 2.3.0
+ svg-tags: 1.0.0
+ table: 6.8.1
+ v8-compile-cache: 2.3.0
+ write-file-atomic: 4.0.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /supports-color/5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+ dependencies:
+ has-flag: 3.0.0
+ dev: true
+
+ /supports-color/7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+ dev: true
+
+ /supports-hyperlinks/2.3.0:
+ resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.2.0
+ dev: true
+
+ /supports-preserve-symlinks-flag/1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /svg-tags/1.0.0:
+ resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
+ dev: true
+
+ /table/6.8.1:
+ resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ ajv: 8.11.2
+ lodash.truncate: 4.4.2
+ slice-ansi: 4.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
+ /tar-stream/1.6.2:
+ resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ bl: 1.2.3
+ buffer-alloc: 1.2.0
+ end-of-stream: 1.4.4
+ fs-constants: 1.0.0
+ readable-stream: 2.3.7
+ to-buffer: 1.1.1
+ xtend: 4.0.2
+ dev: true
+
+ /text-table/0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+ dev: true
+
+ /thenby/1.3.4:
+ resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==}
+ dev: true
+
+ /through/2.3.8:
+ resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+ dev: true
+
+ /tiny-emitter/2.1.0:
+ resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
+ dev: true
+
+ /to-buffer/1.1.1:
+ resolution: {integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==}
+ dev: true
+
+ /to-fast-properties/2.0.0:
+ resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /to-regex-range/5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /tr46/0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ dev: true
+
+ /trim-newlines/3.0.1:
+ resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /type-check/0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ prelude-ls: 1.2.1
+ dev: true
+
+ /type-fest/0.18.1:
+ resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /type-fest/0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /type-fest/0.6.0:
+ resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /type-fest/0.8.1:
+ resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /uc.micro/1.0.6:
+ resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
+ dev: true
+
+ /uglify-js/3.17.4:
+ resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
+ engines: {node: '>=0.8.0'}
+ hasBin: true
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /unbzip2-stream/1.4.3:
+ resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
+ dependencies:
+ buffer: 5.7.1
+ through: 2.3.8
+ dev: true
+
+ /unicode-canonical-property-names-ecmascript/2.0.0:
+ resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /unicode-match-property-ecmascript/2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.0
+ unicode-property-aliases-ecmascript: 2.1.0
+ dev: true
+
+ /unicode-match-property-value-ecmascript/2.1.0:
+ resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /unicode-property-aliases-ecmascript/2.1.0:
+ resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /universalify/2.0.0:
+ resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
+ engines: {node: '>= 10.0.0'}
+ dev: true
+
+ /update-browserslist-db/1.0.10_browserslist@4.21.4:
+ resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+ dependencies:
+ browserslist: 4.21.4
+ escalade: 3.1.1
+ picocolors: 1.0.0
+ dev: true
+
+ /uri-js/4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.1.1
+ dev: true
+
+ /util-deprecate/1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ dev: true
+
+ /v8-compile-cache/2.3.0:
+ resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
+ dev: true
+
+ /validate-npm-package-license/3.0.4:
+ resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+ dependencies:
+ spdx-correct: 3.1.1
+ spdx-expression-parse: 3.0.1
+ dev: true
+
+ /web-streams-polyfill/3.2.1:
+ resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /webidl-conversions/3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ dev: true
+
+ /whatwg-url/5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+ dev: true
+
+ /which-module/2.0.0:
+ resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==}
+ dev: true
+
+ /which/1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /which/2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /word-wrap/1.2.3:
+ resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /wordwrap/1.0.0:
+ resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
+ dev: true
+
+ /wrap-ansi/6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
+ /wrap-ansi/7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
+ /wrappy/1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ dev: true
+
+ /write-file-atomic/4.0.2:
+ resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 3.0.7
+ dev: true
+
+ /xtend/4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+ dev: true
+
+ /y18n/4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+ dev: true
+
+ /y18n/5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /yallist/4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+ dev: true
+
+ /yaml/1.10.2:
+ resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
+ engines: {node: '>= 6'}
+ dev: true
+
+ /yaml/2.1.1:
+ resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==}
+ engines: {node: '>= 14'}
+ dev: true
+
+ /yaml/2.1.3:
+ resolution: {integrity: sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==}
+ engines: {node: '>= 14'}
+ dev: true
+
+ /yargs-parser/18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+
+ /yargs-parser/20.2.9:
+ resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /yargs-parser/21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /yargs/15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.0
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+ dev: true
+
+ /yargs/17.5.1:
+ resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==}
+ engines: {node: '>=12'}
+ dependencies:
+ cliui: 7.0.4
+ escalade: 3.1.1
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+ dev: true
+
+ /yargs/17.6.2:
+ resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
+ engines: {node: '>=12'}
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.1.1
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+ dev: true
+
+ /yauzl/2.10.0:
+ resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+ dev: true
+
+ /yocto-queue/0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+ dev: true
diff --git a/code/frontpage/static/android-chrome-192x192.png b/code/frontpage/static/android-chrome-192x192.png
new file mode 100644
index 0000000..03a0e70
--- /dev/null
+++ b/code/frontpage/static/android-chrome-192x192.png
Binary files differ
diff --git a/code/frontpage/static/android-chrome-512x512.png b/code/frontpage/static/android-chrome-512x512.png
new file mode 100644
index 0000000..74bdfb9
--- /dev/null
+++ b/code/frontpage/static/android-chrome-512x512.png
Binary files differ
diff --git a/code/frontpage/static/apple-touch-icon.png b/code/frontpage/static/apple-touch-icon.png
new file mode 100644
index 0000000..225acef
--- /dev/null
+++ b/code/frontpage/static/apple-touch-icon.png
Binary files differ
diff --git a/code/frontpage/static/css/vendor/.gitkeep b/code/frontpage/static/css/vendor/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/static/css/vendor/.gitkeep
diff --git a/code/frontpage/static/doks.png b/code/frontpage/static/doks.png
new file mode 100644
index 0000000..70d8c78
--- /dev/null
+++ b/code/frontpage/static/doks.png
Binary files differ
diff --git a/code/frontpage/static/doks.svg b/code/frontpage/static/doks.svg
new file mode 100644
index 0000000..4631bc5
--- /dev/null
+++ b/code/frontpage/static/doks.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E1E8ED" d="M32.415 9.586l-9-9C23.054.225 22.553 0 22 0c-1.104 0-1.999.896-2 2 0 .552.224 1.053.586 1.415l-3.859 3.859 9 9 3.859-3.859c.362.361.862.585 1.414.585 1.104 0 2.001-.896 2-2 0-.552-.224-1.052-.585-1.414z"/><path fill="#CCD6DD" d="M22 0H7C4.791 0 3 1.791 3 4v28c0 2.209 1.791 4 4 4h22c2.209 0 4-1.791 4-4V11h-9c-1 0-2-1-2-2V0z"/><path fill="#99AAB5" d="M22 0h-2v9c0 2.209 1.791 4 4 4h9v-2h-9c-1 0-2-1-2-2V0zm-5 8c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm0 4c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm12 4c0 .552-.447 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h20c.553 0 1 .448 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1z"/></svg> \ No newline at end of file
diff --git a/code/frontpage/static/favicon-16x16.png b/code/frontpage/static/favicon-16x16.png
new file mode 100644
index 0000000..8fcd422
--- /dev/null
+++ b/code/frontpage/static/favicon-16x16.png
Binary files differ
diff --git a/code/frontpage/static/favicon-32x32.png b/code/frontpage/static/favicon-32x32.png
new file mode 100644
index 0000000..21378a4
--- /dev/null
+++ b/code/frontpage/static/favicon-32x32.png
Binary files differ
diff --git a/code/frontpage/static/favicon.ico b/code/frontpage/static/favicon.ico
new file mode 100644
index 0000000..9e2fc8d
--- /dev/null
+++ b/code/frontpage/static/favicon.ico
Binary files differ
diff --git a/code/frontpage/static/favicon.svg b/code/frontpage/static/favicon.svg
new file mode 100644
index 0000000..4631bc5
--- /dev/null
+++ b/code/frontpage/static/favicon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E1E8ED" d="M32.415 9.586l-9-9C23.054.225 22.553 0 22 0c-1.104 0-1.999.896-2 2 0 .552.224 1.053.586 1.415l-3.859 3.859 9 9 3.859-3.859c.362.361.862.585 1.414.585 1.104 0 2.001-.896 2-2 0-.552-.224-1.052-.585-1.414z"/><path fill="#CCD6DD" d="M22 0H7C4.791 0 3 1.791 3 4v28c0 2.209 1.791 4 4 4h22c2.209 0 4-1.791 4-4V11h-9c-1 0-2-1-2-2V0z"/><path fill="#99AAB5" d="M22 0h-2v9c0 2.209 1.791 4 4 4h9v-2h-9c-1 0-2-1-2-2V0zm-5 8c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm0 4c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm12 4c0 .552-.447 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h20c.553 0 1 .448 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1z"/></svg> \ No newline at end of file
diff --git a/code/frontpage/static/images/vendor/.gitkeep b/code/frontpage/static/images/vendor/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/static/images/vendor/.gitkeep
diff --git a/code/frontpage/static/js/vendor/.gitkeep b/code/frontpage/static/js/vendor/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/frontpage/static/js/vendor/.gitkeep
diff --git a/code/frontpage/static/logo-doks.png b/code/frontpage/static/logo-doks.png
new file mode 100644
index 0000000..74bdfb9
--- /dev/null
+++ b/code/frontpage/static/logo-doks.png
Binary files differ
diff --git a/code/frontpage/static/logo-doks.svg b/code/frontpage/static/logo-doks.svg
new file mode 100644
index 0000000..4631bc5
--- /dev/null
+++ b/code/frontpage/static/logo-doks.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E1E8ED" d="M32.415 9.586l-9-9C23.054.225 22.553 0 22 0c-1.104 0-1.999.896-2 2 0 .552.224 1.053.586 1.415l-3.859 3.859 9 9 3.859-3.859c.362.361.862.585 1.414.585 1.104 0 2.001-.896 2-2 0-.552-.224-1.052-.585-1.414z"/><path fill="#CCD6DD" d="M22 0H7C4.791 0 3 1.791 3 4v28c0 2.209 1.791 4 4 4h22c2.209 0 4-1.791 4-4V11h-9c-1 0-2-1-2-2V0z"/><path fill="#99AAB5" d="M22 0h-2v9c0 2.209 1.791 4 4 4h9v-2h-9c-1 0-2-1-2-2V0zm-5 8c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm0 4c0 .552-.448 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h8c.552 0 1 .448 1 1zm12 4c0 .552-.447 1-1 1H8c-.552 0-1-.448-1-1s.448-1 1-1h20c.553 0 1 .448 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H8c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1z"/></svg> \ No newline at end of file
diff --git a/code/frontpage/static/site.webmanifest b/code/frontpage/static/site.webmanifest
new file mode 100644
index 0000000..d641eb7
--- /dev/null
+++ b/code/frontpage/static/site.webmanifest
@@ -0,0 +1 @@
+{"name":"Doks Theme","short_name":"Doks","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#fff","background_color":"#fff","display":"standalone"} \ No newline at end of file
diff --git a/code/frontpage/theme.toml b/code/frontpage/theme.toml
new file mode 100644
index 0000000..7ffb682
--- /dev/null
+++ b/code/frontpage/theme.toml
@@ -0,0 +1,14 @@
+name = "Doks"
+license = "MIT"
+licenselink = "https://github.com/h-enk/doks/blob/master/LICENSE"
+description = "Hugo theme helping you build modern documentation websites that are secure, fast, and SEO-ready — by default."
+
+homepage = "https://github.com/h-enk/doks"
+demosite = "https://doks.netlify.app"
+
+tags = ["landing page", "documentation", "blog", "minimal", "modern", "customizable", "search", "dark mode", "bootstrap"]
+features = ["security aware", "fast by default", "seo-ready", "development tools", "bootstrap framework", "netlify-ready", "full text search", "page layouts", "dark mode"]
+
+[author]
+ name = "Henk Verlinde"
+ homepage = "https://henkverlinde.com"