From b7e39b59fd0fc7b5610ebff29035bf622079e0d8 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Wed, 5 Oct 2022 20:45:21 +0800 Subject: refactor: Change file structure --- code/api/.build.yaml | 23 + code/api/.dockerignore | 10 + code/api/.version | 1 + code/api/.version-dev | 1 + code/api/CHANGELOG.md | 123 ++ code/api/Dockerfile | 16 + code/api/build_and_push.sh | 93 + code/api/cliff.toml | 62 + code/api/src/Data/AppDbContext.cs | 51 + code/api/src/Data/Database/ApiAccessToken.cs | 31 + code/api/src/Data/Database/Base.cs | 15 + code/api/src/Data/Database/BaseWithOwner.cs | 19 + code/api/src/Data/Database/Customer.cs | 6 + code/api/src/Data/Database/CustomerContact.cs | 12 + code/api/src/Data/Database/CustomerEvent.cs | 7 + .../api/src/Data/Database/ForgotPasswordRequest.cs | 23 + code/api/src/Data/Database/Project.cs | 7 + code/api/src/Data/Database/Tenant.cs | 11 + code/api/src/Data/Database/TimeCategory.cs | 31 + code/api/src/Data/Database/TimeEntry.cs | 45 + code/api/src/Data/Database/TimeLabel.cs | 31 + code/api/src/Data/Database/Todo.cs | 13 + code/api/src/Data/Database/TodoComment.cs | 7 + code/api/src/Data/Database/TodoLabel.cs | 8 + code/api/src/Data/Database/TodoProject.cs | 16 + .../src/Data/Database/TodoProjectAccessControl.cs | 11 + code/api/src/Data/Database/TodoStatus.cs | 45 + code/api/src/Data/Database/User.cs | 37 + code/api/src/Data/Dtos/TimeQueryDto.cs | 34 + code/api/src/Data/Dtos/UserArchiveDto.cs | 131 ++ code/api/src/Data/Enums/TimeEntryQueryDuration.cs | 37 + .../ForgotPasswordRequestNotFoundException.cs | 21 + .../src/Data/Exceptions/UserNotFoundException.cs | 19 + code/api/src/Data/Models/ApiSpecDocument.cs | 9 + code/api/src/Data/Models/AppPath.cs | 23 + code/api/src/Data/Models/LoggedInUserModel.cs | 7 + code/api/src/Data/Results/ErrorResult.cs | 12 + code/api/src/Data/Static/AppClaims.cs | 8 + code/api/src/Data/Static/AppConfiguration.cs | 58 + code/api/src/Data/Static/AppConstants.cs | 12 + code/api/src/Data/Static/AppDateTime.cs | 16 + .../api/src/Data/Static/AppEnvironmentVariables.cs | 21 + code/api/src/Data/Static/AppHeaders.cs | 7 + code/api/src/Data/Static/AppPaths.cs | 17 + code/api/src/Data/Static/JsonSettings.cs | 11 + .../Internal/Account/CreateAccountPayload.cs | 17 + .../Internal/Account/CreateAccountRoute.cs | 44 + .../Internal/Account/CreateInitialAccountRoute.cs | 34 + .../Internal/Account/DeleteAccountRoute.cs | 49 + .../Endpoints/Internal/Account/GetArchiveRoute.cs | 62 + .../api/src/Endpoints/Internal/Account/GetRoute.cs | 31 + .../src/Endpoints/Internal/Account/LoginPayload.cs | 22 + .../src/Endpoints/Internal/Account/LoginRoute.cs | 37 + .../src/Endpoints/Internal/Account/LogoutRoute.cs | 22 + .../Internal/Account/UpdateAccountPayload.cs | 17 + .../Internal/Account/UpdateAccountRoute.cs | 51 + code/api/src/Endpoints/Internal/BaseRoute.cs | 16 + .../CreateResetRequestRoute.cs | 59 + .../FulfillResetRequestPayload.cs | 14 + .../FulfillResetRequestRoute.cs | 34 + .../IsResetRequestValidRoute.cs | 29 + .../Internal/Root/GetApplicationVersionRoute.cs | 21 + code/api/src/Endpoints/Internal/Root/LogRoute.cs | 16 + .../Internal/Root/ReadConfigurationRoute.cs | 17 + .../Internal/Root/RefreshConfigurationRoute.cs | 15 + .../Endpoints/Internal/Root/ValidSessionRoute.cs | 10 + code/api/src/Endpoints/Internal/RouteBaseAsync.cs | 73 + code/api/src/Endpoints/Internal/RouteBaseSync.cs | 53 + code/api/src/Endpoints/V1/ApiSpecV1.cs | 18 + .../src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs | 57 + .../src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs | 33 + .../src/Endpoints/V1/ApiTokens/GetTokensRoute.cs | 22 + code/api/src/Endpoints/V1/BaseRoute.cs | 39 + .../Endpoints/V1/Categories/CreateCategoryRoute.cs | 43 + .../Endpoints/V1/Categories/DeleteCategoryRoute.cs | 38 + .../Endpoints/V1/Categories/GetCategoriesRoute.cs | 35 + .../Endpoints/V1/Categories/UpdateCategoryRoute.cs | 39 + .../src/Endpoints/V1/Entries/CreateEntryRoute.cs | 65 + .../src/Endpoints/V1/Entries/DeleteEntryRoute.cs | 35 + .../src/Endpoints/V1/Entries/EntryQueryPayload.cs | 60 + .../src/Endpoints/V1/Entries/EntryQueryResponse.cs | 37 + .../src/Endpoints/V1/Entries/EntryQueryRoute.cs | 186 ++ code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs | 34 + .../src/Endpoints/V1/Entries/UpdateEntryRoute.cs | 66 + .../src/Endpoints/V1/Labels/CreateLabelRoute.cs | 46 + .../src/Endpoints/V1/Labels/DeleteLabelRoute.cs | 35 + code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs | 34 + .../src/Endpoints/V1/Labels/UpdateLabelRoute.cs | 38 + code/api/src/Endpoints/V1/RouteBaseAsync.cs | 73 + code/api/src/Endpoints/V1/RouteBaseSync.cs | 53 + code/api/src/IOL.GreatOffice.Api.csproj | 56 + code/api/src/Jobs/JobRegister.cs | 18 + code/api/src/Jobs/TokenCleanupJob.cs | 22 + code/api/src/Jobs/VaultTokenRenewalJob.cs | 15 + .../20210517202115_InitialMigration.Designer.cs | 238 ++ .../Migrations/20210517202115_InitialMigration.cs | 162 ++ ...10522165932_RenameNoteToDescription.Designer.cs | 229 ++ .../20210522165932_RenameNoteToDescription.cs | 34 + .../20211002113037_V6Migration.Designer.cs | 233 ++ .../src/Migrations/20211002113037_V6Migration.cs | 130 ++ .../20220225143559_GithubUserMappings.Designer.cs | 270 +++ .../20220225143559_GithubUserMappings.cs | 43 + .../20220319135910_RenameCreated.Designer.cs | 270 +++ .../src/Migrations/20220319135910_RenameCreated.cs | 65 + .../20220319144958_ModifiedAt.Designer.cs | 290 +++ .../src/Migrations/20220319144958_ModifiedAt.cs | 66 + .../Migrations/20220319203018_UserBase.Designer.cs | 322 +++ code/api/src/Migrations/20220319203018_UserBase.cs | 140 ++ .../Migrations/20220320115601_Update1.Designer.cs | 342 +++ code/api/src/Migrations/20220320115601_Update1.cs | 139 ++ ...32220_UpdatedForgotPasswordRequests.Designer.cs | 344 +++ ...20220320132220_UpdatedForgotPasswordRequests.cs | 57 + .../20220529190359_ApiAccessTokens.Designer.cs | 401 ++++ .../Migrations/20220529190359_ApiAccessTokens.cs | 48 + .../Migrations/20220530174741_Tenants.Designer.cs | 710 ++++++ code/api/src/Migrations/20220530174741_Tenants.cs | 481 ++++ .../20220530175322_RemoveUnusedNavs.Designer.cs | 686 ++++++ .../Migrations/20220530175322_RemoveUnusedNavs.cs | 78 + ...02214238_NullableOptionalBaseFields.Designer.cs | 656 ++++++ .../20220602214238_NullableOptionalBaseFields.cs | 649 ++++++ .../20220606232346_FleshOutNewModules.Designer.cs | 510 +++++ .../20220606232346_FleshOutNewModules.cs | 630 ++++++ .../20220616170311_DataProtectionKeys.Designer.cs | 533 +++++ .../20220616170311_DataProtectionKeys.cs | 33 + .../20220819203816_RemoveGithubUsers.Designer.cs | 496 +++++ .../Migrations/20220819203816_RemoveGithubUsers.cs | 43 + .../src/Migrations/AppDbContextModelSnapshot.cs | 494 +++++ code/api/src/Program.cs | 236 ++ code/api/src/Properties/launchSettings.json | 14 + code/api/src/Services/MailService.cs | 49 + code/api/src/Services/PasswordResetService.cs | 115 + code/api/src/Services/UserService.cs | 50 + code/api/src/Services/VaultService.cs | 158 ++ .../src/Utilities/BasicAuthenticationAttribute.cs | 39 + .../src/Utilities/BasicAuthenticationHandler.cs | 79 + code/api/src/Utilities/ConfigurationExtensions.cs | 88 + .../src/Utilities/GithubAuthenticationHelpers.cs | 84 + code/api/src/Utilities/QuartzJsonSerializer.cs | 16 + code/api/src/Utilities/SwaggerDefaultValues.cs | 58 + .../src/Utilities/SwaggerGenOptionsExtensions.cs | 43 + code/api/src/appsettings.json | 22 + code/api/src/wwwroot/version.txt | 1 + code/app/.gitignore | 8 + code/app/.npmrc | 1 + code/app/.typesafe-i18n.json | 5 + code/app/package.json | 46 + code/app/playwright.config.ts | 10 + code/app/pnpm-lock.yaml | 2299 ++++++++++++++++++++ code/app/postcss.config.cjs | 13 + code/app/src/actions/pwKey.js | 12 + code/app/src/actions/pwKey.js.map | 1 + code/app/src/actions/pwKey.ts | 10 + code/app/src/app.d.ts | 9 + code/app/src/app.html | 14 + code/app/src/app.pcss | 34 + code/app/src/global.d.ts | 11 + code/app/src/lib/api/internal-fetch.ts | 170 ++ code/app/src/lib/api/root.ts | 6 + code/app/src/lib/api/time-entry.ts | 83 + code/app/src/lib/api/user.ts | 47 + code/app/src/lib/colors.ts | 47 + code/app/src/lib/components/alert.svelte | 268 +++ code/app/src/lib/components/button.svelte | 103 + code/app/src/lib/components/checkbox.svelte | 24 + .../src/lib/components/icons/adjustments.svelte | 14 + .../lib/components/icons/bars-3-center-left.svelte | 15 + code/app/src/lib/components/icons/calendar.svelte | 14 + .../src/lib/components/icons/check-circle.svelte | 13 + .../lib/components/icons/chevron-up-down.svelte | 13 + code/app/src/lib/components/icons/database.svelte | 14 + .../lib/components/icons/exclamation-circle.svelte | 13 + .../components/icons/exclamation-triangle.svelte | 13 + .../src/lib/components/icons/folder-open.svelte | 14 + code/app/src/lib/components/icons/home.svelte | 14 + code/app/src/lib/components/icons/index.ts | 41 + .../lib/components/icons/information-circle.svelte | 13 + .../lib/components/icons/magnifying-glass.svelte | 13 + code/app/src/lib/components/icons/megaphone.svelte | 14 + code/app/src/lib/components/icons/menu.svelte | 14 + .../app/src/lib/components/icons/queue-list.svelte | 14 + code/app/src/lib/components/icons/spinner.svelte | 20 + code/app/src/lib/components/icons/x-circle.svelte | 13 + code/app/src/lib/components/icons/x-mark.svelte | 11 + code/app/src/lib/components/icons/x.svelte | 14 + code/app/src/lib/components/index.ts | 15 + code/app/src/lib/components/input.svelte | 103 + code/app/src/lib/components/locale-switcher.svelte | 55 + code/app/src/lib/components/switch.svelte | 143 ++ code/app/src/lib/configuration.ts | 60 + code/app/src/lib/helpers.ts | 497 +++++ code/app/src/lib/i18n/en/app/index.ts | 5 + code/app/src/lib/i18n/en/index.ts | 50 + code/app/src/lib/i18n/formatters.ts | 13 + code/app/src/lib/i18n/i18n-svelte.ts | 12 + code/app/src/lib/i18n/i18n-types.ts | 359 +++ code/app/src/lib/i18n/i18n-util.async.ts | 42 + code/app/src/lib/i18n/i18n-util.sync.ts | 35 + code/app/src/lib/i18n/i18n-util.ts | 39 + code/app/src/lib/i18n/nb/app/index.ts | 8 + code/app/src/lib/i18n/nb/index.ts | 50 + code/app/src/lib/logger.ts | 86 + code/app/src/lib/models/CreateAccountPayload.ts | 4 + code/app/src/lib/models/ErrorResult.ts | 4 + code/app/src/lib/models/IInternalFetchRequest.ts | 6 + code/app/src/lib/models/IInternalFetchResponse.ts | 6 + code/app/src/lib/models/ISession.ts | 8 + code/app/src/lib/models/IValidationResult.ts | 31 + code/app/src/lib/models/LoginPayload.ts | 5 + code/app/src/lib/models/TimeCategoryDto.ts | 9 + code/app/src/lib/models/TimeEntryDto.ts | 13 + code/app/src/lib/models/TimeEntryQuery.ts | 27 + code/app/src/lib/models/TimeLabelDto.ts | 8 + code/app/src/lib/models/TimeQueryDto.ts | 29 + code/app/src/lib/models/UnwrappedEntryDateTime.ts | 9 + code/app/src/lib/models/UpdateProfilePayload.ts | 4 + code/app/src/lib/persistent-store.ts | 102 + code/app/src/lib/session.ts | 69 + code/app/src/routes/(main)/(app)/+layout.svelte | 297 +++ code/app/src/routes/(main)/(app)/home/+page.svelte | 1 + code/app/src/routes/(main)/(app)/org/+page.svelte | 4 + .../src/routes/(main)/(app)/profile/+page.svelte | 4 + .../src/routes/(main)/(app)/projects/+page.svelte | 5 + .../src/routes/(main)/(app)/settings/+page.svelte | 4 + .../src/routes/(main)/(app)/tickets/+page.svelte | 4 + code/app/src/routes/(main)/(app)/todo/+page.svelte | 4 + code/app/src/routes/(main)/(app)/wiki/+page.svelte | 4 + code/app/src/routes/(main)/(public)/+layout.svelte | 18 + .../(main)/(public)/reset-password/+page.svelte | 82 + .../(public)/reset-password/[id]/+page.server.js | 11 + .../reset-password/[id]/+page.server.js.map | 1 + .../(public)/reset-password/[id]/+page.server.ts | 11 + .../(public)/reset-password/[id]/+page.svelte | 132 ++ .../routes/(main)/(public)/sign-in/+page.svelte | 133 ++ .../src/routes/(main)/(public)/sign-in/index.ts | 19 + .../(main)/(public)/sign-in/tests/index.spec.ts | 12 + .../routes/(main)/(public)/sign-up/+page.svelte | 82 + code/app/src/routes/(main)/+layout.server.ts | 34 + code/app/src/routes/(main)/+layout.svelte | 29 + code/app/src/routes/(main)/+layout.ts | 15 + code/app/src/routes/(main)/+page.svelte | 1 + code/app/src/routes/book/+layout.svelte | 64 + code/app/src/routes/book/+page.svelte | 1 + code/app/src/routes/book/alerts/+page.svelte | 70 + code/app/src/routes/book/buttons/+page.svelte | 23 + code/app/src/routes/book/inputs/+page.svelte | 48 + code/app/src/routes/book/toggles/+page.svelte | 27 + code/app/static/favicon.ico | Bin 0 -> 1406 bytes code/app/svelte.config.js | 24 + code/app/tailwind.config.cjs | 135 ++ code/app/tsconfig.json | 11 + code/app/vite.config.js | 14 + .../ApplicationTests/LoginPageTests.cs | 23 + .../Helpers/Element.cs | 6 + .../Helpers/WebServerFixture.cs | 48 + .../IOL.GreatOffice.IntegrationTests.csproj | 21 + 255 files changed, 20865 insertions(+) create mode 100644 code/api/.build.yaml create mode 100644 code/api/.dockerignore create mode 100644 code/api/.version create mode 100644 code/api/.version-dev create mode 100644 code/api/CHANGELOG.md create mode 100644 code/api/Dockerfile create mode 100755 code/api/build_and_push.sh create mode 100644 code/api/cliff.toml create mode 100644 code/api/src/Data/AppDbContext.cs create mode 100644 code/api/src/Data/Database/ApiAccessToken.cs create mode 100644 code/api/src/Data/Database/Base.cs create mode 100644 code/api/src/Data/Database/BaseWithOwner.cs create mode 100644 code/api/src/Data/Database/Customer.cs create mode 100644 code/api/src/Data/Database/CustomerContact.cs create mode 100644 code/api/src/Data/Database/CustomerEvent.cs create mode 100644 code/api/src/Data/Database/ForgotPasswordRequest.cs create mode 100644 code/api/src/Data/Database/Project.cs create mode 100644 code/api/src/Data/Database/Tenant.cs create mode 100644 code/api/src/Data/Database/TimeCategory.cs create mode 100644 code/api/src/Data/Database/TimeEntry.cs create mode 100644 code/api/src/Data/Database/TimeLabel.cs create mode 100644 code/api/src/Data/Database/Todo.cs create mode 100644 code/api/src/Data/Database/TodoComment.cs create mode 100644 code/api/src/Data/Database/TodoLabel.cs create mode 100644 code/api/src/Data/Database/TodoProject.cs create mode 100644 code/api/src/Data/Database/TodoProjectAccessControl.cs create mode 100644 code/api/src/Data/Database/TodoStatus.cs create mode 100644 code/api/src/Data/Database/User.cs create mode 100644 code/api/src/Data/Dtos/TimeQueryDto.cs create mode 100644 code/api/src/Data/Dtos/UserArchiveDto.cs create mode 100644 code/api/src/Data/Enums/TimeEntryQueryDuration.cs create mode 100644 code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs create mode 100644 code/api/src/Data/Exceptions/UserNotFoundException.cs create mode 100644 code/api/src/Data/Models/ApiSpecDocument.cs create mode 100644 code/api/src/Data/Models/AppPath.cs create mode 100644 code/api/src/Data/Models/LoggedInUserModel.cs create mode 100644 code/api/src/Data/Results/ErrorResult.cs create mode 100644 code/api/src/Data/Static/AppClaims.cs create mode 100644 code/api/src/Data/Static/AppConfiguration.cs create mode 100644 code/api/src/Data/Static/AppConstants.cs create mode 100644 code/api/src/Data/Static/AppDateTime.cs create mode 100644 code/api/src/Data/Static/AppEnvironmentVariables.cs create mode 100644 code/api/src/Data/Static/AppHeaders.cs create mode 100644 code/api/src/Data/Static/AppPaths.cs create mode 100644 code/api/src/Data/Static/JsonSettings.cs create mode 100644 code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs create mode 100644 code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/GetRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/LoginPayload.cs create mode 100644 code/api/src/Endpoints/Internal/Account/LoginRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/LogoutRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/UpdateAccountPayload.cs create mode 100644 code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/BaseRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/LogRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/ValidSessionRoute.cs create mode 100644 code/api/src/Endpoints/Internal/RouteBaseAsync.cs create mode 100644 code/api/src/Endpoints/Internal/RouteBaseSync.cs create mode 100644 code/api/src/Endpoints/V1/ApiSpecV1.cs create mode 100644 code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs create mode 100644 code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs create mode 100644 code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs create mode 100644 code/api/src/Endpoints/V1/BaseRoute.cs create mode 100644 code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs create mode 100644 code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Entries/EntryQueryPayload.cs create mode 100644 code/api/src/Endpoints/V1/Entries/EntryQueryResponse.cs create mode 100644 code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs create mode 100644 code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs create mode 100644 code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs create mode 100644 code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs create mode 100644 code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs create mode 100644 code/api/src/Endpoints/V1/RouteBaseAsync.cs create mode 100644 code/api/src/Endpoints/V1/RouteBaseSync.cs create mode 100644 code/api/src/IOL.GreatOffice.Api.csproj create mode 100644 code/api/src/Jobs/JobRegister.cs create mode 100644 code/api/src/Jobs/TokenCleanupJob.cs create mode 100644 code/api/src/Jobs/VaultTokenRenewalJob.cs create mode 100644 code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs create mode 100644 code/api/src/Migrations/20210517202115_InitialMigration.cs create mode 100644 code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs create mode 100644 code/api/src/Migrations/20210522165932_RenameNoteToDescription.cs create mode 100644 code/api/src/Migrations/20211002113037_V6Migration.Designer.cs create mode 100644 code/api/src/Migrations/20211002113037_V6Migration.cs create mode 100644 code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs create mode 100644 code/api/src/Migrations/20220225143559_GithubUserMappings.cs create mode 100644 code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs create mode 100644 code/api/src/Migrations/20220319135910_RenameCreated.cs create mode 100644 code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs create mode 100644 code/api/src/Migrations/20220319144958_ModifiedAt.cs create mode 100644 code/api/src/Migrations/20220319203018_UserBase.Designer.cs create mode 100644 code/api/src/Migrations/20220319203018_UserBase.cs create mode 100644 code/api/src/Migrations/20220320115601_Update1.Designer.cs create mode 100644 code/api/src/Migrations/20220320115601_Update1.cs create mode 100644 code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs create mode 100644 code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs create mode 100644 code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs create mode 100644 code/api/src/Migrations/20220529190359_ApiAccessTokens.cs create mode 100644 code/api/src/Migrations/20220530174741_Tenants.Designer.cs create mode 100644 code/api/src/Migrations/20220530174741_Tenants.cs create mode 100644 code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs create mode 100644 code/api/src/Migrations/20220530175322_RemoveUnusedNavs.cs create mode 100644 code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs create mode 100644 code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.cs create mode 100644 code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs create mode 100644 code/api/src/Migrations/20220606232346_FleshOutNewModules.cs create mode 100644 code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs create mode 100644 code/api/src/Migrations/20220616170311_DataProtectionKeys.cs create mode 100644 code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs create mode 100644 code/api/src/Migrations/20220819203816_RemoveGithubUsers.cs create mode 100644 code/api/src/Migrations/AppDbContextModelSnapshot.cs create mode 100644 code/api/src/Program.cs create mode 100644 code/api/src/Properties/launchSettings.json create mode 100644 code/api/src/Services/MailService.cs create mode 100644 code/api/src/Services/PasswordResetService.cs create mode 100644 code/api/src/Services/UserService.cs create mode 100644 code/api/src/Services/VaultService.cs create mode 100644 code/api/src/Utilities/BasicAuthenticationAttribute.cs create mode 100644 code/api/src/Utilities/BasicAuthenticationHandler.cs create mode 100644 code/api/src/Utilities/ConfigurationExtensions.cs create mode 100644 code/api/src/Utilities/GithubAuthenticationHelpers.cs create mode 100644 code/api/src/Utilities/QuartzJsonSerializer.cs create mode 100644 code/api/src/Utilities/SwaggerDefaultValues.cs create mode 100644 code/api/src/Utilities/SwaggerGenOptionsExtensions.cs create mode 100644 code/api/src/appsettings.json create mode 100644 code/api/src/wwwroot/version.txt create mode 100644 code/app/.gitignore create mode 100644 code/app/.npmrc create mode 100644 code/app/.typesafe-i18n.json create mode 100644 code/app/package.json create mode 100644 code/app/playwright.config.ts create mode 100644 code/app/pnpm-lock.yaml create mode 100644 code/app/postcss.config.cjs create mode 100644 code/app/src/actions/pwKey.js create mode 100644 code/app/src/actions/pwKey.js.map create mode 100644 code/app/src/actions/pwKey.ts create mode 100644 code/app/src/app.d.ts create mode 100644 code/app/src/app.html create mode 100644 code/app/src/app.pcss create mode 100644 code/app/src/global.d.ts create mode 100644 code/app/src/lib/api/internal-fetch.ts create mode 100644 code/app/src/lib/api/root.ts create mode 100644 code/app/src/lib/api/time-entry.ts create mode 100644 code/app/src/lib/api/user.ts create mode 100644 code/app/src/lib/colors.ts create mode 100644 code/app/src/lib/components/alert.svelte create mode 100644 code/app/src/lib/components/button.svelte create mode 100644 code/app/src/lib/components/checkbox.svelte create mode 100644 code/app/src/lib/components/icons/adjustments.svelte create mode 100644 code/app/src/lib/components/icons/bars-3-center-left.svelte create mode 100644 code/app/src/lib/components/icons/calendar.svelte create mode 100644 code/app/src/lib/components/icons/check-circle.svelte create mode 100644 code/app/src/lib/components/icons/chevron-up-down.svelte create mode 100644 code/app/src/lib/components/icons/database.svelte create mode 100644 code/app/src/lib/components/icons/exclamation-circle.svelte create mode 100644 code/app/src/lib/components/icons/exclamation-triangle.svelte create mode 100644 code/app/src/lib/components/icons/folder-open.svelte create mode 100644 code/app/src/lib/components/icons/home.svelte create mode 100644 code/app/src/lib/components/icons/index.ts create mode 100644 code/app/src/lib/components/icons/information-circle.svelte create mode 100644 code/app/src/lib/components/icons/magnifying-glass.svelte create mode 100644 code/app/src/lib/components/icons/megaphone.svelte create mode 100644 code/app/src/lib/components/icons/menu.svelte create mode 100644 code/app/src/lib/components/icons/queue-list.svelte create mode 100644 code/app/src/lib/components/icons/spinner.svelte create mode 100644 code/app/src/lib/components/icons/x-circle.svelte create mode 100644 code/app/src/lib/components/icons/x-mark.svelte create mode 100644 code/app/src/lib/components/icons/x.svelte create mode 100644 code/app/src/lib/components/index.ts create mode 100644 code/app/src/lib/components/input.svelte create mode 100644 code/app/src/lib/components/locale-switcher.svelte create mode 100644 code/app/src/lib/components/switch.svelte create mode 100644 code/app/src/lib/configuration.ts create mode 100644 code/app/src/lib/helpers.ts create mode 100644 code/app/src/lib/i18n/en/app/index.ts create mode 100644 code/app/src/lib/i18n/en/index.ts create mode 100644 code/app/src/lib/i18n/formatters.ts create mode 100644 code/app/src/lib/i18n/i18n-svelte.ts create mode 100644 code/app/src/lib/i18n/i18n-types.ts create mode 100644 code/app/src/lib/i18n/i18n-util.async.ts create mode 100644 code/app/src/lib/i18n/i18n-util.sync.ts create mode 100644 code/app/src/lib/i18n/i18n-util.ts create mode 100644 code/app/src/lib/i18n/nb/app/index.ts create mode 100644 code/app/src/lib/i18n/nb/index.ts create mode 100644 code/app/src/lib/logger.ts create mode 100644 code/app/src/lib/models/CreateAccountPayload.ts create mode 100644 code/app/src/lib/models/ErrorResult.ts create mode 100644 code/app/src/lib/models/IInternalFetchRequest.ts create mode 100644 code/app/src/lib/models/IInternalFetchResponse.ts create mode 100644 code/app/src/lib/models/ISession.ts create mode 100644 code/app/src/lib/models/IValidationResult.ts create mode 100644 code/app/src/lib/models/LoginPayload.ts create mode 100644 code/app/src/lib/models/TimeCategoryDto.ts create mode 100644 code/app/src/lib/models/TimeEntryDto.ts create mode 100644 code/app/src/lib/models/TimeEntryQuery.ts create mode 100644 code/app/src/lib/models/TimeLabelDto.ts create mode 100644 code/app/src/lib/models/TimeQueryDto.ts create mode 100644 code/app/src/lib/models/UnwrappedEntryDateTime.ts create mode 100644 code/app/src/lib/models/UpdateProfilePayload.ts create mode 100644 code/app/src/lib/persistent-store.ts create mode 100644 code/app/src/lib/session.ts create mode 100644 code/app/src/routes/(main)/(app)/+layout.svelte create mode 100644 code/app/src/routes/(main)/(app)/home/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/org/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/profile/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/projects/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/settings/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/tickets/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/todo/+page.svelte create mode 100644 code/app/src/routes/(main)/(app)/wiki/+page.svelte create mode 100644 code/app/src/routes/(main)/(public)/+layout.svelte create mode 100644 code/app/src/routes/(main)/(public)/reset-password/+page.svelte create mode 100644 code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js create mode 100644 code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js.map create mode 100644 code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts create mode 100644 code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte create mode 100644 code/app/src/routes/(main)/(public)/sign-in/+page.svelte create mode 100644 code/app/src/routes/(main)/(public)/sign-in/index.ts create mode 100644 code/app/src/routes/(main)/(public)/sign-in/tests/index.spec.ts create mode 100644 code/app/src/routes/(main)/(public)/sign-up/+page.svelte create mode 100644 code/app/src/routes/(main)/+layout.server.ts create mode 100644 code/app/src/routes/(main)/+layout.svelte create mode 100644 code/app/src/routes/(main)/+layout.ts create mode 100644 code/app/src/routes/(main)/+page.svelte create mode 100644 code/app/src/routes/book/+layout.svelte create mode 100644 code/app/src/routes/book/+page.svelte create mode 100644 code/app/src/routes/book/alerts/+page.svelte create mode 100644 code/app/src/routes/book/buttons/+page.svelte create mode 100644 code/app/src/routes/book/inputs/+page.svelte create mode 100644 code/app/src/routes/book/toggles/+page.svelte create mode 100644 code/app/static/favicon.ico create mode 100644 code/app/svelte.config.js create mode 100644 code/app/tailwind.config.cjs create mode 100644 code/app/tsconfig.json create mode 100644 code/app/vite.config.js create mode 100644 code/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs create mode 100644 code/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs create mode 100644 code/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs create mode 100644 code/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj (limited to 'code') 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..adc4be3 --- /dev/null +++ b/code/api/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.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:6.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..dd88916 --- /dev/null +++ b/code/api/build_and_push.sh @@ -0,0 +1,93 @@ +#!/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))-server-dev" + OLD_VERSION=$CURRENT_DEV_VERSION +else + NEW_VERSION="v$((CURRENT_VERSION_INT+1))-server" + 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 buildx build --platform linux/amd64 -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 = """ + +""" + +[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/src/Data/AppDbContext.cs b/code/api/src/Data/AppDbContext.cs new file mode 100644 index 0000000..c970429 --- /dev/null +++ b/code/api/src/Data/AppDbContext.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; + +namespace IOL.GreatOffice.Api.Data; + +public class AppDbContext : DbContext, IDataProtectionKeyContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + public DbSet Users { get; set; } + public DbSet ForgotPasswordRequests { get; set; } + public DbSet TimeLabels { get; set; } + public DbSet TimeEntries { get; set; } + public DbSet TimeCategories { get; set; } + public DbSet AccessTokens { get; set; } + public DbSet Tenants { get; set; } + public DbSet DataProtectionKeys { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity(e => { + e.ToTable("users"); + }); + + modelBuilder.Entity(e => { + e.HasOne(c => c.User); + e.ToTable("forgot_password_requests"); + }); + + modelBuilder.Entity(e => { + e.ToTable("time_categories"); + }); + + modelBuilder.Entity(e => { + e.ToTable("time_labels"); + }); + + modelBuilder.Entity(e => { + e.HasOne(c => c.Category); + e.HasMany(c => c.Labels); + e.ToTable("time_entries"); + }); + + modelBuilder.Entity(e => { + e.ToTable("api_access_tokens"); + }); + + modelBuilder.Entity(e => { + e.ToTable("tenants"); + }); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/code/api/src/Data/Database/ApiAccessToken.cs b/code/api/src/Data/Database/ApiAccessToken.cs new file mode 100644 index 0000000..9582869 --- /dev/null +++ b/code/api/src/Data/Database/ApiAccessToken.cs @@ -0,0 +1,31 @@ +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; + public ApiAccessTokenDto AsDto => new(this); + + public class ApiAccessTokenDto + { + public ApiAccessTokenDto(ApiAccessToken source) { + ExpiryDate = source.ExpiryDate; + AllowRead = source.AllowRead; + AllowCreate = source.AllowCreate; + AllowUpdate = source.AllowUpdate; + AllowDelete = source.AllowDelete; + } + + 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/Data/Database/Base.cs b/code/api/src/Data/Database/Base.cs new file mode 100644 index 0000000..ae9efa2 --- /dev/null +++ b/code/api/src/Data/Database/Base.cs @@ -0,0 +1,15 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public 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 bool Deleted { get; set; } + public void Modified() => ModifiedAt = AppDateTime.UtcNow; +} \ No newline at end of file diff --git a/code/api/src/Data/Database/BaseWithOwner.cs b/code/api/src/Data/Database/BaseWithOwner.cs new file mode 100644 index 0000000..1eb99f4 --- /dev/null +++ b/code/api/src/Data/Database/BaseWithOwner.cs @@ -0,0 +1,19 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +/// +/// Base class for all entities. +/// +public class BaseWithOwner : Base +{ + protected BaseWithOwner() { } + + protected BaseWithOwner(Guid userId) { + UserId = userId; + } + + public Guid? UserId { get; set; } + public Guid? TenantId { get; init; } + public Guid? ModifiedById { get; init; } + public Guid? CreatedById { get; init; } + public Guid? DeletedById { get; init; } +} \ No newline at end of file diff --git a/code/api/src/Data/Database/Customer.cs b/code/api/src/Data/Database/Customer.cs new file mode 100644 index 0000000..c6b06a4 --- /dev/null +++ b/code/api/src/Data/Database/Customer.cs @@ -0,0 +1,6 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class Customer : BaseWithOwner +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/code/api/src/Data/Database/CustomerContact.cs b/code/api/src/Data/Database/CustomerContact.cs new file mode 100644 index 0000000..f5a951d --- /dev/null +++ b/code/api/src/Data/Database/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/Data/Database/CustomerEvent.cs b/code/api/src/Data/Database/CustomerEvent.cs new file mode 100644 index 0000000..da3e3ed --- /dev/null +++ b/code/api/src/Data/Database/CustomerEvent.cs @@ -0,0 +1,7 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class CustomerEvent : BaseWithOwner +{ + public Customer Customer { get; set; } + public string Name { get; set; } +} diff --git a/code/api/src/Data/Database/ForgotPasswordRequest.cs b/code/api/src/Data/Database/ForgotPasswordRequest.cs new file mode 100644 index 0000000..1510a35 --- /dev/null +++ b/code/api/src/Data/Database/ForgotPasswordRequest.cs @@ -0,0 +1,23 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class ForgotPasswordRequest +{ + public ForgotPasswordRequest() { } + + public ForgotPasswordRequest(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/Data/Database/Project.cs b/code/api/src/Data/Database/Project.cs new file mode 100644 index 0000000..7e694ee --- /dev/null +++ b/code/api/src/Data/Database/Project.cs @@ -0,0 +1,7 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class Project : BaseWithOwner +{ + public string Name { get; set; } + public Guid? CustomerId { get; set; } +} diff --git a/code/api/src/Data/Database/Tenant.cs b/code/api/src/Data/Database/Tenant.cs new file mode 100644 index 0000000..b185c7a --- /dev/null +++ b/code/api/src/Data/Database/Tenant.cs @@ -0,0 +1,11 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class Tenant : BaseWithOwner +{ + 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 Users { get; set; } +} diff --git a/code/api/src/Data/Database/TimeCategory.cs b/code/api/src/Data/Database/TimeCategory.cs new file mode 100644 index 0000000..69c6957 --- /dev/null +++ b/code/api/src/Data/Database/TimeCategory.cs @@ -0,0 +1,31 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TimeCategory : BaseWithOwner +{ + public TimeCategory() { } + public TimeCategory(Guid userId) : base(userId) { } + public string Name { get; set; } + public string Color { get; set; } + public TimeCategoryDto AsDto => new(this); + + public class TimeCategoryDto + { + public TimeCategoryDto() { } + + public TimeCategoryDto(TimeCategory sourceEntry = default) { + if (sourceEntry == default) { + return; + } + + Id = sourceEntry.Id; + ModifiedAt = sourceEntry.ModifiedAt; + Name = sourceEntry.Name; + Color = sourceEntry.Color; + } + + public Guid Id { get; set; } + public DateTime? ModifiedAt { get; set; } + public string Name { get; set; } + public string Color { get; set; } + } +} diff --git a/code/api/src/Data/Database/TimeEntry.cs b/code/api/src/Data/Database/TimeEntry.cs new file mode 100644 index 0000000..46c62e1 --- /dev/null +++ b/code/api/src/Data/Database/TimeEntry.cs @@ -0,0 +1,45 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TimeEntry : BaseWithOwner +{ + public TimeEntry() { } + public TimeEntry(Guid userId) : base(userId) { } + public DateTime Start { get; set; } + public DateTime Stop { get; set; } + public string Description { get; set; } + public ICollection Labels { get; set; } + public TimeCategory Category { get; set; } + public TimeEntryDto AsDto => new(this); + + public class TimeEntryDto + { + public TimeEntryDto() { } + + public TimeEntryDto(TimeEntry sourceEntry = default) { + if (sourceEntry == default) { + return; + } + + Id = sourceEntry.Id; + ModifiedAt = sourceEntry.ModifiedAt; + Stop = sourceEntry.Stop; + Start = sourceEntry.Start; + Description = sourceEntry.Description; + if (sourceEntry.Labels != default) { + Labels = sourceEntry.Labels + .Select(t => t.AsDto) + .ToList(); + } + + Category = sourceEntry.Category.AsDto; + } + + public Guid? Id { get; set; } + public DateTime? ModifiedAt { get; set; } + public DateTime Start { get; set; } + public DateTime Stop { get; set; } + public string Description { get; set; } + public List Labels { get; set; } + public TimeCategory.TimeCategoryDto Category { get; set; } + } +} diff --git a/code/api/src/Data/Database/TimeLabel.cs b/code/api/src/Data/Database/TimeLabel.cs new file mode 100644 index 0000000..55e20b0 --- /dev/null +++ b/code/api/src/Data/Database/TimeLabel.cs @@ -0,0 +1,31 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TimeLabel : BaseWithOwner +{ + public TimeLabel() { } + public TimeLabel(Guid userId) : base(userId) { } + public string Name { get; set; } + public string Color { get; set; } + + [NotMapped] + public TimeLabelDto AsDto => new(this); + + public class TimeLabelDto + { + public TimeLabelDto() { } + + public TimeLabelDto(TimeLabel sourceEntry) { + Id = sourceEntry.Id; + CreatedAt = sourceEntry.CreatedAt; + ModifiedAt = sourceEntry.ModifiedAt; + Name = sourceEntry.Name; + Color = sourceEntry.Color; + } + + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? ModifiedAt { get; set; } + public string Name { get; set; } + public string Color { get; set; } + } +} diff --git a/code/api/src/Data/Database/Todo.cs b/code/api/src/Data/Database/Todo.cs new file mode 100644 index 0000000..5fe3c9a --- /dev/null +++ b/code/api/src/Data/Database/Todo.cs @@ -0,0 +1,13 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class Todo : BaseWithOwner +{ + public int PublicId { get; set; } + public TodoStatus Status { get; set; } + public TodoProject Project { get; set; } + public Guid? AssignedUserId { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public ICollection Labels { get; set; } + public ICollection Comments { get; set; } +} diff --git a/code/api/src/Data/Database/TodoComment.cs b/code/api/src/Data/Database/TodoComment.cs new file mode 100644 index 0000000..44dcbed --- /dev/null +++ b/code/api/src/Data/Database/TodoComment.cs @@ -0,0 +1,7 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TodoComment : BaseWithOwner +{ + public string Value { get; set; } + public Todo Todo { get; set; } +} diff --git a/code/api/src/Data/Database/TodoLabel.cs b/code/api/src/Data/Database/TodoLabel.cs new file mode 100644 index 0000000..7753ade --- /dev/null +++ b/code/api/src/Data/Database/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/Data/Database/TodoProject.cs b/code/api/src/Data/Database/TodoProject.cs new file mode 100644 index 0000000..0a4a7be --- /dev/null +++ b/code/api/src/Data/Database/TodoProject.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TodoProject : BaseWithOwner +{ + public string Name { get; set; } + public TodoVisibility Visibility { get; set; } + public Guid? ProjectId { get; set; } +} + +public enum TodoVisibility +{ + PRIVATE = 0, + UNLISTED = 1, + TENANT_WIDE = 2, + PUBLIC = 3, +} diff --git a/code/api/src/Data/Database/TodoProjectAccessControl.cs b/code/api/src/Data/Database/TodoProjectAccessControl.cs new file mode 100644 index 0000000..964f831 --- /dev/null +++ b/code/api/src/Data/Database/TodoProjectAccessControl.cs @@ -0,0 +1,11 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TodoProjectAccessControl +{ + public TodoProject Project { get; set; } + public Guid? UserId { get; set; } + public bool Browse { get; set; } + public bool Submit { get; set; } + public bool Comment { get; set; } + public bool Edit { get; set; } +} diff --git a/code/api/src/Data/Database/TodoStatus.cs b/code/api/src/Data/Database/TodoStatus.cs new file mode 100644 index 0000000..416212d --- /dev/null +++ b/code/api/src/Data/Database/TodoStatus.cs @@ -0,0 +1,45 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class TodoStatus : BaseWithOwner +{ + public string Name { get; set; } + public string Color { get; set; } + public Todo Todo { get; set; } + + public static List GetDefaultStatusSetForTenant(Guid tenantId) { + return new List() { + new() { + Name = "Reported", + TenantId = tenantId + }, + new() { + Name = "Resolved", + TenantId = tenantId + }, + new() { + Name = "Fixed", + TenantId = tenantId + }, + new() { + Name = "Implemented", + TenantId = tenantId + }, + new() { + Name = "Won't fix", + TenantId = tenantId + }, + new() { + Name = "By design", + TenantId = tenantId + }, + new() { + Name = "Invalid", + TenantId = tenantId + }, + new() { + Name = "Duplicate", + TenantId = tenantId + } + }; + } +} diff --git a/code/api/src/Data/Database/User.cs b/code/api/src/Data/Database/User.cs new file mode 100644 index 0000000..9db5d35 --- /dev/null +++ b/code/api/src/Data/Database/User.cs @@ -0,0 +1,37 @@ +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 ICollection Tenants { get; set; } + + public string DisplayName() { + if (FirstName.HasValue() && LastName.HasValue()) return FirstName + " " + LastName; + return FirstName.HasValue() ? FirstName : Email; + } + + public void HashAndSetPassword(string password) { + Password = PasswordHelper.HashPassword(password); + } + + public bool VerifyPassword(string password) { + return PasswordHelper.Verify(password, Password); + } + + public IEnumerable 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/Data/Dtos/TimeQueryDto.cs b/code/api/src/Data/Dtos/TimeQueryDto.cs new file mode 100644 index 0000000..f734cb1 --- /dev/null +++ b/code/api/src/Data/Dtos/TimeQueryDto.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Data.Dtos; + +public class TimeQueryDto +{ + public TimeQueryDto() { + Results = new List(); + } + + /// + /// List of entries. + /// + public List Results { get; set; } + + /// + /// Curren page. + /// + public int Page { get; set; } + + /// + /// Maximum count of entries in a page. + /// + public int PageSize { get; set; } + + /// + /// Total count of entries. + /// + public int TotalSize { get; set; } + + /// + /// Total count of pages. + /// + public int TotalPageCount { get; set; } +} diff --git a/code/api/src/Data/Dtos/UserArchiveDto.cs b/code/api/src/Data/Dtos/UserArchiveDto.cs new file mode 100644 index 0000000..42e0600 --- /dev/null +++ b/code/api/src/Data/Dtos/UserArchiveDto.cs @@ -0,0 +1,131 @@ + +namespace IOL.GreatOffice.Api.Data.Dtos; + +/// +/// Represents a user archive as it is provided to users. +/// +public class UserArchiveDto +{ + /// + public UserArchiveDto(User user) { + Meta = new MetaDto { + GeneratedAt = AppDateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + }; + User = new UserDto(user); + Entries = new List(); + } + + /// + /// Metadata for the user archive. + /// + public MetaDto Meta { get; } + + /// + /// Relevant user data for the archive. + /// + public UserDto User { get; } + + /// + /// List of entries that the user has created. + /// + public List Entries { get; } + + public void CountEntries() { + Meta.EntryCount = Entries.Count; + } + + /// + /// Represents a time entry in the data archive. + /// + public class EntryDto + { + public string CreatedAt { get; init; } + + [JsonIgnore] + public DateTime StartDateTime { get; init; } + + /// + /// ISO 8601 string of the UTC date the time entry started. + /// + public string Start => StartDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"); + + [JsonIgnore] + public DateTime StopDateTime { get; init; } + + /// + /// ISO 8601 string of the UTC date the time entry stopped. + /// + public string Stop => StopDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"); + + /// + /// Total amount of minutes elapsed from start to stop on this time entry. + /// + public double Minutes => StopDateTime.Subtract(StartDateTime).TotalMinutes; + + public string Description { get; init; } + + /// + /// Archive spesific category for this time entry. + /// + public CategoryDto Category { get; init; } + + /// + /// Archive spesific list of labels for this time entry. + /// + public List Labels { get; init; } + } + + /// + /// Time entry category as it is written to the user archive. + /// + public class CategoryDto + { + public string Name { get; init; } + public string Color { get; init; } + } + + /// + /// Time entry label as it is written to the user archive. + /// + public class LabelDto + { + public string Name { get; init; } + public string Color { get; init; } + } + + + /// + /// Represents the user who this archive's data is based on. + /// + public class UserDto + { + /// + public UserDto(User user) { + Username = user.Username; + CreatedAt = user.CreatedAt; + } + + /// + /// UTC date this user was created. + /// + public DateTime CreatedAt { get; } + + public string Username { get; } + } + + /// + /// Represents the meta object which contains metdata for this archive. + /// + public class MetaDto + { + /// + /// ISO 8601 UTC date string for when this archive was created. + /// + public string GeneratedAt { get; init; } + + /// + /// Amount of entries in the archive. + /// + public int EntryCount { get; set; } + } +} diff --git a/code/api/src/Data/Enums/TimeEntryQueryDuration.cs b/code/api/src/Data/Enums/TimeEntryQueryDuration.cs new file mode 100644 index 0000000..af70ca6 --- /dev/null +++ b/code/api/src/Data/Enums/TimeEntryQueryDuration.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Data.Enums; + +/// +/// Specify a duration filter for time entry queries. +/// +public enum TimeEntryQueryDuration +{ + /// + /// Only query entries created today. + /// + TODAY = 0, + + /// + /// Only query entries created this week. + /// + THIS_WEEK = 1, + + /// + /// Only query entries created this month. + /// + THIS_MONTH = 2, + + /// + /// Only query entries created this year. + /// + THIS_YEAR = 3, + + /// + /// Only query entries created at a spesific date. + /// + SPECIFIC_DATE = 4, + + /// + /// Only query entries created between two dates. + /// + DATE_RANGE = 5, +} diff --git a/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs b/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs new file mode 100644 index 0000000..02474b4 --- /dev/null +++ b/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs @@ -0,0 +1,21 @@ +namespace IOL.GreatOffice.Api.Data.Exceptions; + +[Serializable] +public class ForgotPasswordRequestNotFoundException : Exception +{ + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public ForgotPasswordRequestNotFoundException() { } + public ForgotPasswordRequestNotFoundException(string message) : base(message) { } + public ForgotPasswordRequestNotFoundException(string message, Exception inner) : base(message, inner) { } + + protected ForgotPasswordRequestNotFoundException( + SerializationInfo info, + StreamingContext context + ) : base(info, context) { } +} diff --git a/code/api/src/Data/Exceptions/UserNotFoundException.cs b/code/api/src/Data/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..06b57a9 --- /dev/null +++ b/code/api/src/Data/Exceptions/UserNotFoundException.cs @@ -0,0 +1,19 @@ +namespace IOL.GreatOffice.Api.Data.Exceptions; + +[Serializable] +public class UserNotFoundException : Exception +{ + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + + public UserNotFoundException() { } + public UserNotFoundException(string message) : base(message) { } + public UserNotFoundException(string message, Exception inner) : base(message, inner) { } + + protected UserNotFoundException( + SerializationInfo info, + StreamingContext context + ) : base(info, context) { } +} diff --git a/code/api/src/Data/Models/ApiSpecDocument.cs b/code/api/src/Data/Models/ApiSpecDocument.cs new file mode 100644 index 0000000..1c7d936 --- /dev/null +++ b/code/api/src/Data/Models/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/Data/Models/AppPath.cs b/code/api/src/Data/Models/AppPath.cs new file mode 100644 index 0000000..e47e48c --- /dev/null +++ b/code/api/src/Data/Models/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/Data/Models/LoggedInUserModel.cs b/code/api/src/Data/Models/LoggedInUserModel.cs new file mode 100644 index 0000000..d802b77 --- /dev/null +++ b/code/api/src/Data/Models/LoggedInUserModel.cs @@ -0,0 +1,7 @@ +namespace IOL.GreatOffice.Api.Data.Models; + +public class LoggedInUserModel +{ + public Guid Id { get; set; } + public string Username { get; set; } +} diff --git a/code/api/src/Data/Results/ErrorResult.cs b/code/api/src/Data/Results/ErrorResult.cs new file mode 100644 index 0000000..fd2fd6a --- /dev/null +++ b/code/api/src/Data/Results/ErrorResult.cs @@ -0,0 +1,12 @@ +namespace IOL.GreatOffice.Api.Data.Results; + +public class ErrorResult +{ + public ErrorResult(string title = default, string text = default) { + Title = title; + Text = text; + } + + public string Title { get; set; } + public string Text { get; set; } +} diff --git a/code/api/src/Data/Static/AppClaims.cs b/code/api/src/Data/Static/AppClaims.cs new file mode 100644 index 0000000..8b6d3a8 --- /dev/null +++ b/code/api/src/Data/Static/AppClaims.cs @@ -0,0 +1,8 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class AppClaims +{ + public const string USER_ID = "user_id"; + public const string NAME = "name"; + public const string GITHUB_ACCESS_TOKEN = ""; +} diff --git a/code/api/src/Data/Static/AppConfiguration.cs b/code/api/src/Data/Static/AppConfiguration.cs new file mode 100644 index 0000000..4ee7a8e --- /dev/null +++ b/code/api/src/Data/Static/AppConfiguration.cs @@ -0,0 +1,58 @@ +using System.Security.Cryptography.X509Certificates; + +namespace IOL.GreatOffice.Api.Data.Static; + +public class AppConfiguration +{ + public string DB_HOST { get; set; } + public string DB_PORT { get; set; } + public string DB_USER { get; set; } + public string DB_PASSWORD { get; set; } + public string DB_NAME { get; set; } + public string QUARTZ_DB_HOST { get; set; } + public string QUARTZ_DB_PORT { get; set; } + public string QUARTZ_DB_USER { get; set; } + public string QUARTZ_DB_PASSWORD { get; set; } + public string QUARTZ_DB_NAME { get; set; } + public string SEQ_API_KEY { get; set; } + public string SEQ_API_URL { get; set; } + public string SMTP_HOST { get; set; } + public string SMTP_PORT { get; set; } + public string SMTP_USER { get; set; } + public string SMTP_PASSWORD { get; set; } + public string EMAIL_FROM_ADDRESS { get; set; } + public string EMAIL_FROM_DISPLAY_NAME { get; set; } + public string PORTAL_URL { get; set; } + public string GITHUB_CLIENT_ID { get; set; } + public string GITHUB_CLIENT_SECRET { get; set; } + public string APP_AES_KEY { get; set; } + 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, + SMTP_HOST, + SMTP_PORT, + SMTP_USER = SMTP_USER.Obfuscate() ?? "", + SMTP_PASSWORD = SMTP_PASSWORD.Obfuscate() ?? "", + EMAIL_FROM_ADDRESS, + EMAIL_FROM_DISPLAY_NAME, + PORTAL_URL, + GITHUB_CLIENT_ID = GITHUB_CLIENT_ID.Obfuscate() ?? "", + GITHUB_CLIENT_SECRET = GITHUB_CLIENT_SECRET.Obfuscate() ?? "", + APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "", + CERT1 = CERT1().PublicKey.Oid.FriendlyName + }; + } +} diff --git a/code/api/src/Data/Static/AppConstants.cs b/code/api/src/Data/Static/AppConstants.cs new file mode 100644 index 0000000..461317b --- /dev/null +++ b/code/api/src/Data/Static/AppConstants.cs @@ -0,0 +1,12 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class AppConstants +{ + public const string API_NAME = "Great Office 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/Data/Static/AppDateTime.cs b/code/api/src/Data/Static/AppDateTime.cs new file mode 100644 index 0000000..880d2a8 --- /dev/null +++ b/code/api/src/Data/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/Data/Static/AppEnvironmentVariables.cs b/code/api/src/Data/Static/AppEnvironmentVariables.cs new file mode 100644 index 0000000..c3f821d --- /dev/null +++ b/code/api/src/Data/Static/AppEnvironmentVariables.cs @@ -0,0 +1,21 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class AppEnvironmentVariables +{ + /// + /// An access token that can be used to access the Hashicorp Vault instance that is available at VAULT_URL + /// + public const string VAULT_TOKEN = "VAULT_TOKEN"; + /// + /// An url pointing to the Hashicorp Vault instance the app should use + /// + public const string VAULT_URL = "VAULT_URL"; + /// + /// The duration of which to keep a local cached version of the configuration + /// + public const string VAULT_CACHE_TTL = "VAULT_CACHE_TTL"; + /// + /// The vault key name for the main configuration json object, described by + /// + public const string MAIN_CONFIG_SHEET = "MAIN_CONFIG_SHEET"; +} diff --git a/code/api/src/Data/Static/AppHeaders.cs b/code/api/src/Data/Static/AppHeaders.cs new file mode 100644 index 0000000..7912418 --- /dev/null +++ b/code/api/src/Data/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 VAULT_TOKEN = "X-Vault-Token"; +} diff --git a/code/api/src/Data/Static/AppPaths.cs b/code/api/src/Data/Static/AppPaths.cs new file mode 100644 index 0000000..a24f5af --- /dev/null +++ b/code/api/src/Data/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/Data/Static/JsonSettings.cs b/code/api/src/Data/Static/JsonSettings.cs new file mode 100644 index 0000000..a163c11 --- /dev/null +++ b/code/api/src/Data/Static/JsonSettings.cs @@ -0,0 +1,11 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class JsonSettings +{ + public static Action 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/Endpoints/Internal/Account/CreateAccountPayload.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs new file mode 100644 index 0000000..dc73e68 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for creating new user accounts. +/// +public class CreateAccountPayload +{ + /// + /// Username for the new account. + /// + public string Username { get; set; } + + /// + /// Password for the new account. + /// + public string Password { get; set; } +} 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..954fbf5 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -0,0 +1,44 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +public class CreateAccountRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public CreateAccountRoute(UserService userService, AppDbContext context) { + _userService = userService; + _context = context; + } + + /// + /// Create a new user account. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/account/create")] + public override async Task HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) { + if (request.Username.IsValidEmailAddress() == false) { + return BadRequest(new ErrorResult("Invalid form", request.Username + " does not look like a valid email")); + } + + if (request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid form", "The password requires 6 or more characters.")); + } + + var username = request.Username.Trim(); + if (_context.Users.Any(c => c.Username == username)) { + return BadRequest(new ErrorResult("Username is not available", "There is already a user registered with email: " + username)); + } + + var user = new User(username); + user.HashAndSetPassword(request.Password); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Ok(); + } +} 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..13fbdf4 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -0,0 +1,34 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public CreateInitialAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Create an initial user account. + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/account/create-initial")] + public override async Task HandleAsync(CancellationToken cancellationToken = default) { + if (_context.Users.Any()) { + return NotFound(); + } + + var user = new User("admin@ivarlovlie.no"); + user.HashAndSetPassword("ivar123"); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Redirect("/"); + } +} 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..2149e15 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs @@ -0,0 +1,49 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public DeleteAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Delete the logged on user's account. + /// + /// + /// + [HttpDelete("~/_/account/delete")] + public override async Task HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await _userService.LogOutUser(HttpContext); + return Unauthorized(); + } + + if (user.Username == "demo@demo.demo") { + await _userService.LogOutUser(HttpContext); + return Ok(); + } + + var githubMappings = _context.TimeCategories.Where(c => c.UserId == user.Id); + var passwordResets = _context.ForgotPasswordRequests.Where(c => c.UserId == user.Id); + var entries = _context.TimeEntries.Where(c => c.UserId == user.Id); + var labels = _context.TimeLabels.Where(c => c.UserId == user.Id); + var categories = _context.TimeCategories.Where(c => c.UserId == user.Id); + + _context.TimeCategories.RemoveRange(githubMappings); + _context.ForgotPasswordRequests.RemoveRange(passwordResets); + _context.TimeEntries.RemoveRange(entries); + _context.TimeLabels.RemoveRange(labels); + _context.TimeCategories.RemoveRange(categories); + _context.Users.Remove(user); + + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogOutUser(HttpContext); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs new file mode 100644 index 0000000..f1b70f3 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs @@ -0,0 +1,62 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountArchiveRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public GetAccountArchiveRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get a data archive with the currently logged on user's data. + /// + /// + /// + [HttpGet("~/_/account/archive")] + public override async Task> HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await HttpContext.SignOutAsync(); + return Unauthorized(); + } + + var entries = _context.TimeEntries + .AsNoTracking() + .Include(c => c.Labels) + .Include(c => c.Category) + .Where(c => c.UserId == user.Id) + .ToList(); + + var jsonOptions = new JsonSerializerOptions { + WriteIndented = true + }; + + var dto = new UserArchiveDto(user); + dto.Entries.AddRange(entries.Select(entry => new UserArchiveDto.EntryDto { + CreatedAt = entry.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"), + StartDateTime = entry.Start, + StopDateTime = entry.Stop, + Description = entry.Description, + Labels = entry.Labels + .Select(c => new UserArchiveDto.LabelDto { + Name = c.Name, + Color = c.Color + }) + .ToList(), + Category = new UserArchiveDto.CategoryDto { + Name = entry.Category.Name, + Color = entry.Category.Color + }, + })); + + dto.CountEntries(); + + var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions); + + return File(entriesSerialized, + "application/json", + user.Username + "-time-tracker-archive-" + AppDateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json"); + } +} diff --git a/code/api/src/Endpoints/Internal/Account/GetRoute.cs b/code/api/src/Endpoints/Internal/Account/GetRoute.cs new file mode 100644 index 0000000..1aa7ecb --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/GetRoute.cs @@ -0,0 +1,31 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public GetAccountRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get the logged on user's session data. + /// + /// + /// + [HttpGet("~/_/account")] + public override async Task> HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.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/LoginPayload.cs b/code/api/src/Endpoints/Internal/Account/LoginPayload.cs new file mode 100644 index 0000000..807662c --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/LoginPayload.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for logging in a user. +/// +public class LoginPayload +{ + /// + /// Username of the user's account. + /// + public string Username { get; set; } + + /// + /// Password of the user's account. + /// + public string Password { get; set; } + + /// + /// Specify that the created session should be long lived and continually refreshed. + /// + public bool Persist { get; set; } +} 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..5b41c61 --- /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 + .WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public LoginRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Login a user. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/account/login")] + public override async Task HandleAsync(LoginPayload request, CancellationToken cancellationToken = default) { + if (!ModelState.IsValid) { + return BadRequest(ModelState); + } + + var user = _context.Users.SingleOrDefault(u => u.Username == request.Username); + if (user == default || !user.VerifyPassword(request.Password)) { + return BadRequest(new ErrorResult("Invalid username or password")); + } + + await _userService.LogInUser(HttpContext, user, request.Persist); + return Ok(); + } +} 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..4a06f4a --- /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; + } + + /// + /// Logout a user. + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/account/logout")] + public override async Task HandleAsync(CancellationToken cancellationToken = default) { + await _userService.LogOutUser(HttpContext); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/Internal/Account/UpdateAccountPayload.cs b/code/api/src/Endpoints/Internal/Account/UpdateAccountPayload.cs new file mode 100644 index 0000000..88a3237 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for updating an account. +/// +public class UpdatePayload +{ + /// + /// Username to set on the logged on user's account. + /// + public string Username { get; set; } + + /// + /// Password to set on the logged on user's account. + /// + public string Password { get; set; } +} 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..a997dcb --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -0,0 +1,51 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class UpdateAccountRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public UpdateAccountRoute(AppDbContext context) { + _context = context; + } + + /// + /// Update the logged on user's data. + /// + /// + /// + /// + [HttpPost("~/_/account/update")] + public override async Task HandleAsync(UpdatePayload request, CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await HttpContext.SignOutAsync(); + return Unauthorized(); + } + + if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) { + return BadRequest(new ErrorResult("Invalid request", "No data was submitted")); + } + + if (request.Password.HasValue() && request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid request", + "The new password must contain at least 6 characters")); + } + + if (request.Password.HasValue()) { + user.HashAndSetPassword(request.Password); + } + + if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { + return BadRequest(new ErrorResult("Invalid request", + "The new username does not look like a valid email address")); + } + + if (request.Username.HasValue()) { + user.Username = request.Username.Trim(); + } + + await _context.SaveChangesAsync(cancellationToken); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/Internal/BaseRoute.cs b/code/api/src/Endpoints/Internal/BaseRoute.cs new file mode 100644 index 0000000..3e2c6af --- /dev/null +++ b/code/api/src/Endpoints/Internal/BaseRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +[Authorize] +[ApiController] +[ApiExplorerSettings(IgnoreApi = true)] +[ApiVersionNeutral] +public class BaseRoute : ControllerBase +{ + /// + /// User data for the currently logged on user. + /// + protected LoggedInUserModel LoggedInUser => new() { + Username = User.FindFirstValue(AppClaims.NAME), + Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + }; +} 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..8fbc9a0 --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -0,0 +1,59 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class CreateResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly ILogger _logger; + private readonly PasswordResetService _passwordResetService; + private readonly AppDbContext _context; + + /// + public CreateResetRequestRoute(ILogger logger, PasswordResetService passwordResetService, AppDbContext context) { + _logger = logger; + _passwordResetService = passwordResetService; + _context = context; + } + + /// + /// Create a new password reset request. + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/create")] + public override async Task HandleAsync(string username, CancellationToken cancellationToken = default) { + if (!username.IsValidEmailAddress()) { + _logger.LogInformation("Username is invalid, not doing request for password change"); + return BadRequest(new ErrorResult("Invalid email address", username + " looks like an invalid email address")); + } + + 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"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz); + _logger.LogInformation("Creating forgot password request with date time: " + requestDateTime.ToString("u")); + + try { + var user = _context.Users.SingleOrDefault(c => c.Username.Equals(username)); + if (user != default) { + await _passwordResetService.AddRequestAsync(user, tz, cancellationToken); + return Ok(); + } + + _logger.LogInformation("User was not found, not doing request for password change"); + return Ok(); + } catch (Exception e) { + _logger.LogError(e, "ForgotAction failed badly"); + return Ok(); + } + } +} diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs new file mode 100644 index 0000000..f0fb59f --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs @@ -0,0 +1,14 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class FulfillResetRequestPayload +{ + /// + /// Id of the password reset request to fulfill + /// + public Guid Id { get; set; } + + /// + /// New password to set on the relevant account + /// + public string NewPassword { get; set; } +} 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..96f344a --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly PasswordResetService _passwordResetService; + + /// + public FulfillResetRequestRoute(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } + + /// + /// Fulfill a password reset request. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/forgot-password-requests/fulfill")] + public override async Task HandleAsync(FulfillResetRequestPayload request, CancellationToken cancellationToken = default) { + try { + var fulfilled = await _passwordResetService.FullFillRequestAsync(request.Id, request.NewPassword, cancellationToken); + return Ok(fulfilled); + } catch (Exception e) { + if (e is ForgotPasswordRequestNotFoundException or UserNotFoundException) { + return NotFound(); + } + + throw; + } + } +} 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..c4dcd22 --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs @@ -0,0 +1,29 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly PasswordResetService _passwordResetService; + + /// + public IsResetRequestValidRoute(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } + + /// + /// Check if a given password reset request is still valid. + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/is-valid")] + public override async Task HandleAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _passwordResetService.GetRequestAsync(id, cancellationToken); + if (request == default) { + return NotFound(); + } + + return Ok(request.IsExpired == false); + } +} diff --git a/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs new file mode 100644 index 0000000..5fb8213 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs @@ -0,0 +1,21 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class GetApplicationVersionRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + private readonly IWebHostEnvironment _environment; + + /// + public GetApplicationVersionRoute(IWebHostEnvironment environment) { + _environment = environment; + } + + /// + /// Get the running api version number. + /// + /// + [HttpGet("~/_/version")] + public override ActionResult Handle() { + var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt"); + return Ok(System.IO.File.ReadAllText(versionFilePath)); + } +} diff --git a/code/api/src/Endpoints/Internal/Root/LogRoute.cs b/code/api/src/Endpoints/Internal/Root/LogRoute.cs new file mode 100644 index 0000000..48b497a --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/LogRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class LogRoute : RouteBaseSync.WithRequest.WithoutResult +{ + private readonly ILogger _logger; + + public LogRoute(ILogger logger) { + _logger = logger; + } + + [AllowAnonymous] + [HttpPost("~/_/log")] + public override void Handle([FromBody] string request) { + _logger.LogInformation(request); + } +} 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..e0dcca3 --- /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"); + } +} 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..4b1beec --- /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.RefreshCurrentAppConfiguration(); + } +} diff --git a/code/api/src/Endpoints/Internal/Root/ValidSessionRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidSessionRoute.cs new file mode 100644 index 0000000..f377ff6 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/ValidSessionRoute.cs @@ -0,0 +1,10 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class ValidSessionRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + [Authorize] + [HttpGet("~/_/valid-session")] + public override ActionResult Handle() { + return Ok(); + } +} \ 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..1bb0af0 --- /dev/null +++ b/code/api/src/Endpoints/Internal/RouteBaseAsync.cs @@ -0,0 +1,73 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseAsync +{ + public static class WithRequest + { + public abstract class WithResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task 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..173999d --- /dev/null +++ b/code/api/src/Endpoints/Internal/RouteBaseSync.cs @@ -0,0 +1,53 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseSync +{ + public static class WithRequest + { + public abstract class WithResult : BaseRoute + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + 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..2086619 --- /dev/null +++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs @@ -0,0 +1,57 @@ +using System.Text; + +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class CreateTokenRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly AppConfiguration _configuration; + private readonly ILogger _logger; + + public CreateTokenRoute(AppDbContext context, VaultService vaultService, ILogger logger) + { + _context = context; + _configuration = vaultService.GetCurrentAppConfiguration(); + _logger = logger; + } + + /// + /// Create a new api token with the provided claims. + /// + /// The claims to set on the api token + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpPost("~/v{version:apiVersion}/api-tokens/create")] + [ProducesResponseType(200, Type = typeof(string))] + [ProducesResponseType(404, Type = typeof(ErrorResult))] + public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) + { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) + { + return NotFound(new ErrorResult("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 access_token = new ApiAccessToken() + { + Id = Guid.NewGuid(), + User = user, + ExpiryDate = request.ExpiryDate.ToUniversalTime(), + AllowCreate = request.AllowCreate, + AllowRead = request.AllowRead, + AllowDelete = request.AllowDelete, + AllowUpdate = request.AllowUpdate + }; + + _context.AccessTokens.Add(access_token); + _context.SaveChanges(); + return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy)))); + } +} 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..a90b4c0 --- /dev/null +++ b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs @@ -0,0 +1,33 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class DeleteTokenRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly ILogger _logger; + + public DeleteTokenRoute(AppDbContext context, ILogger logger) { + _context = context; + _logger = logger; + } + + /// + /// Delete an api token, rendering it unusable + /// + /// Id of the token to delete + /// Nothing + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public override ActionResult Handle(Guid id) { + var token = _context.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(); + } + + _context.AccessTokens.Remove(token); + _context.SaveChanges(); + return Ok(); + } +} 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..59fd077 --- /dev/null +++ b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult>> +{ + private readonly AppDbContext _context; + + public GetTokensRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get all tokens, both active and inactive. + /// + /// A list of tokens + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpGet("~/v{version:apiVersion}/api-tokens")] + [ProducesResponseType(200, Type = typeof(List))] + [ProducesResponseType(204)] + public override ActionResult> Handle() { + return Ok(_context.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto)); + } +} diff --git a/code/api/src/Endpoints/V1/BaseRoute.cs b/code/api/src/Endpoints/V1/BaseRoute.cs new file mode 100644 index 0000000..e7d72ac --- /dev/null +++ b/code/api/src/Endpoints/V1/BaseRoute.cs @@ -0,0 +1,39 @@ +using System.Net.Http.Headers; + +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// +[ApiVersion(ApiSpecV1.VERSION_STRING)] +[Authorize(AuthenticationSchemes = AuthSchemes)] +[ApiController] +public class BaseRoute : ControllerBase +{ + private const string AuthSchemes = CookieAuthenticationDefaults.AuthenticationScheme + "," + AppConstants.BASIC_AUTH_SCHEME; + + /// + /// User data for the currently logged on user. + /// + protected LoggedInUserModel LoggedInUser => new() { + Username = User.FindFirstValue(AppClaims.NAME), + Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + }; + + 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" + }; + } +} diff --git a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs new file mode 100644 index 0000000..fac2b5e --- /dev/null +++ b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs @@ -0,0 +1,43 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class CreateCategoryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public CreateCategoryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Create a new time entry category. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [HttpPost("~/v{version:apiVersion}/categories/create")] + [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))] + public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { + var duplicate = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); + if (duplicate) { + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); + if (category != default) { + return Ok(category.AsDto); + } + } + + var newCategory = new TimeCategory(LoggedInUser.Id) { + Name = categoryTimeCategoryDto.Name.Trim(), + Color = categoryTimeCategoryDto.Color + }; + + _context.TimeCategories.Add(newCategory); + _context.SaveChanges(); + categoryTimeCategoryDto.Id = newCategory.Id; + return Ok(categoryTimeCategoryDto); + } +} diff --git a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs new file mode 100644 index 0000000..3d438a0 --- /dev/null +++ b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs @@ -0,0 +1,38 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class DeleteCategoryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public DeleteCategoryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Delete a time entry category. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/categories/{id:guid}/delete")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public override ActionResult Handle(Guid id) { + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); + + if (category == default) { + return NotFound(); + } + + var entries = _context.TimeEntries + .Include(c => c.Category) + .Where(c => c.Category.Id == category.Id); + _context.TimeEntries.RemoveRange(entries); + _context.TimeCategories.Remove(category); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs new file mode 100644 index 0000000..a40a832 --- /dev/null +++ b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs @@ -0,0 +1,35 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +/// +public class GetCategoriesRoute : RouteBaseSync.WithoutRequest.WithActionResult> +{ + private readonly AppDbContext _context; + + /// + public GetCategoriesRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get a minimal list of time entry categories. + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [ProducesResponseType(200, Type = typeof(List))] + [ProducesResponseType(204)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/categories")] + public override ActionResult> Handle() { + var categories = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .OrderByDescending(c => c.CreatedAt) + .Select(c => c.AsDto) + .ToList(); + + if (categories.Count == 0) { + return NoContent(); + } + + return Ok(categories); + } +} diff --git a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs new file mode 100644 index 0000000..ca7dfdf --- /dev/null +++ b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs @@ -0,0 +1,39 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class UpdateCategoryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public UpdateCategoryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Update a time entry category. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/categories/update")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(403)] + public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == categoryTimeCategoryDto.Id); + if (category == default) { + return NotFound(); + } + + if (LoggedInUser.Id != category.UserId) { + return Forbid(); + } + + category.Name = categoryTimeCategoryDto.Name; + category.Color = categoryTimeCategoryDto.Color; + _context.SaveChanges(); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs new file mode 100644 index 0000000..362e430 --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs @@ -0,0 +1,65 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class CreateEntryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public CreateEntryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Create a time entry. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [ProducesResponseType(200)] + [ProducesResponseType(400, Type = typeof(ErrorResult))] + [ProducesResponseType(404, Type = typeof(ErrorResult))] + [HttpPost("~/v{version:apiVersion}/entries/create")] + public override ActionResult Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { + if (timeEntryTimeEntryDto.Stop == default) { + return BadRequest(new ErrorResult("Invalid form", "A stop date is required")); + } + + if (timeEntryTimeEntryDto.Start == default) { + return BadRequest(new ErrorResult("Invalid form", "A start date is required")); + } + + if (timeEntryTimeEntryDto.Category == default) { + return BadRequest(new ErrorResult("Invalid form", "A category is required")); + } + + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); + if (category == default) { + return NotFound(new ErrorResult("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } + + var entry = new TimeEntry(LoggedInUser.Id) { + Category = category, + Start = timeEntryTimeEntryDto.Start.ToUniversalTime(), + Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(), + Description = timeEntryTimeEntryDto.Description, + }; + + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id)) + .ToList(); + if (labels.Count != timeEntryTimeEntryDto.Labels.Count) { + return NotFound(new ErrorResult("Not found", "Could not find all of the specified labels")); + } + + entry.Labels = labels; + } + + _context.TimeEntries.Add(entry); + _context.SaveChanges(); + return Ok(entry.AsDto); + } +} diff --git a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs new file mode 100644 index 0000000..0850af0 --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs @@ -0,0 +1,35 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// +public class DeleteEntryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public DeleteEntryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Delete a time entry. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/entries/{id:guid}/delete")] + [ProducesResponseType(404)] + [ProducesResponseType(200)] + public override ActionResult Handle(Guid id) { + var entry = _context.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); + if (entry == default) { + return NotFound(); + } + + _context.TimeEntries.Remove(entry); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryPayload.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryPayload.cs new file mode 100644 index 0000000..763ac8b --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/EntryQueryPayload.cs @@ -0,0 +1,60 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// +/// Query model for querying time entries. +/// +public class EntryQueryPayload +{ + /// + /// Duration to filter with. + /// + public TimeEntryQueryDuration Duration { get; set; } + + /// + /// List of categories to filter with. + /// + public List Categories { get; set; } + + /// + /// List of labels to filter with. + /// + public List Labels { get; set; } + + /// + /// Date range to filter with, only respected if Duration is set to TimeEntryQueryDuration.DATE_RANGE. + /// + /// + public QueryDateRange DateRange { get; set; } + + /// + /// Spesific date to filter with, only respected if Duration is set to TimeEntryQueryDuration.SPECIFIC_DATE. + /// + /// + public DateTime SpecificDate { get; set; } + + /// + /// Optional page number to show, goes well with PageSize. + /// + public int Page { get; set; } + + /// + /// Optional page size to show, goes well with Page. + /// + public int PageSize { get; set; } + + /// + /// Represents a date range. + /// + public class QueryDateRange + { + /// + /// Range start + /// + public DateTime From { get; set; } + + /// + /// Range end + /// + public DateTime To { get; set; } + } +} diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryResponse.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryResponse.cs new file mode 100644 index 0000000..b1b07a3 --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/EntryQueryResponse.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// +/// Response given for a successful query. +/// +public class EntryQueryResponse +{ + /// + public EntryQueryResponse() { + Results = new List(); + } + + /// + /// List of entries. + /// + public List Results { get; set; } + + /// + /// Current page. + /// + public int Page { get; set; } + + /// + /// Current page size (amount of entries). + /// + public int PageSize { get; set; } + + /// + /// Total amount of entries in query. + /// + public int TotalSize { get; set; } + + /// + /// Total amount of page(s) in query. + /// + public int TotalPageCount { get; set; } +} diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs new file mode 100644 index 0000000..d431ac5 --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs @@ -0,0 +1,186 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class EntryQueryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly ILogger _logger; + private readonly AppDbContext _context; + + public EntryQueryRoute(ILogger logger, AppDbContext context) { + _logger = logger; + _context = context; + } + + /// + /// Get a list of entries based on a given query. + /// + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpPost("~/v{version:apiVersion}/entries/query")] + [ProducesResponseType(204)] + [ProducesResponseType(400, Type = typeof(ErrorResult))] + [ProducesResponseType(200, Type = typeof(EntryQueryResponse))] + public override ActionResult Handle(EntryQueryPayload entryQuery) { + var result = new TimeQueryDto(); + + Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader); + var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC"); + var offsetInHours = 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)) { + offsetInHours++; + } + + _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz); + _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u")); + + var skipCount = 0; + if (entryQuery.Page > 1) { + skipCount = entryQuery.PageSize * entryQuery.Page; + } + + result.Page = entryQuery.Page; + result.PageSize = entryQuery.PageSize; + + var baseQuery = _context.TimeEntries + .AsNoTracking() + .Include(c => c.Category) + .Include(c => c.Labels) + .Where(c => c.UserId == LoggedInUser.Id) + .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id)) + .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id))) + .OrderByDescending(c => c.Start); + + switch (entryQuery.Duration) { + case TimeEntryQueryDuration.TODAY: + var baseTodaysEntries = baseQuery + .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, AppDateTime.UtcNow.Date) == 0); + var baseTodaysEntriesCount = baseTodaysEntries.Count(); + + if (baseTodaysEntriesCount == 0) { + return NoContent(); + } + + result.TotalSize = baseTodaysEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / entryQuery.PageSize)); + + var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_WEEK: + var lastMonday = AppDateTime.UtcNow.StartOfWeek(DayOfWeek.Monday); + + var baseEntriesThisWeek = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date); + + var baseEntriesThisWeekCount = baseEntriesThisWeek.Count(); + + if (baseEntriesThisWeekCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisWeekCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / entryQuery.PageSize)); + + var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_MONTH: + var baseEntriesThisMonth = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Month == AppDateTime.UtcNow.Month + && c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); + var baseEntriesThisMonthCount = baseEntriesThisMonth.Count(); + if (baseEntriesThisMonthCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisMonthCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisMonthCount / entryQuery.PageSize)); + + var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_YEAR: + var baseEntriesThisYear = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); + + var baseEntriesThisYearCount = baseEntriesThisYear.Count(); + if (baseEntriesThisYearCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisYearCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / entryQuery.PageSize)); + + var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.SPECIFIC_DATE: + var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc); + var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date); + var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count(); + + if (baseEntriesOnThisDateCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesOnThisDateCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / entryQuery.PageSize)); + + var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.DATE_RANGE: + if (entryQuery.DateRange.From == default) { + return BadRequest(new ErrorResult("Invalid query", "From date cannot be empty")); + } + + var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc); + + if (entryQuery.DateRange.To == default) { + return BadRequest(new ErrorResult("Invalid query", "To date cannot be empty")); + } + + var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc); + + if (DateTime.Compare(fromDate, toDate) > 0) { + return BadRequest(new ErrorResult("Invalid query", "To date cannot be less than From date")); + } + + var baseDateRangeEntries = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate); + + var baseDateRangeEntriesCount = baseDateRangeEntries.Count(); + if (baseDateRangeEntriesCount == 0) { + return NoContent(); + } + + result.TotalSize = baseDateRangeEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / entryQuery.PageSize)); + + var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query"); + } + + if (result.Results.Any() && result.Page == 0) { + result.Page = 1; + result.TotalPageCount = 1; + } + + return Ok(result); + } +} diff --git a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs new file mode 100644 index 0000000..87038db --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs @@ -0,0 +1,34 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class GetEntryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public GetEntryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get a spesific time entry. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/entries/{id:guid}")] + [ProducesResponseType(404)] + [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] + public override ActionResult Handle(Guid id) { + var entry = _context.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .Include(c => c.Category) + .Include(c => c.Labels) + .SingleOrDefault(c => c.Id == id); + + if (entry == default) { + return NotFound(); + } + + return Ok(entry); + } +} diff --git a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs new file mode 100644 index 0000000..ac233e0 --- /dev/null +++ b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs @@ -0,0 +1,66 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class UpdateEntryRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + public UpdateEntryRoute(AppDbContext context) { + _context = context; + } + + /// + /// Update a time entry. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/entries/update")] + [ProducesResponseType(404, Type = typeof(ErrorResult))] + [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] + public override ActionResult Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { + var entry = _context.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .Include(c => c.Labels) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id); + + if (entry == default) { + return NotFound(); + } + + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); + if (category == default) { + return NotFound(new ErrorResult("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } + + entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime(); + entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(); + entry.Description = timeEntryTimeEntryDto.Description; + entry.Category = category; + + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = new List(); + + foreach (var labelDto in timeEntryTimeEntryDto.Labels) { + var label = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == labelDto.Id); + + if (label == default) { + continue; + } + + labels.Add(label); + } + + entry.Labels = labels; + } else { + entry.Labels = default; + } + + _context.SaveChanges(); + return Ok(entry.AsDto); + } +} diff --git a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs new file mode 100644 index 0000000..31ef7d0 --- /dev/null +++ b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs @@ -0,0 +1,46 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// +public class CreateLabelRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public CreateLabelRoute(AppDbContext context) { + _context = context; + } + + /// + /// Create a time entry label. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [HttpPost("~/v{version:apiVersion}/labels/create")] + public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { + var duplicate = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); + if (duplicate) { + var label = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); + + if (label != default) { + return Ok(label.AsDto); + } + } + + var newLabel = new TimeLabel(LoggedInUser.Id) { + Name = labelTimeLabelDto.Name.Trim(), + Color = labelTimeLabelDto.Color + }; + + _context.TimeLabels.Add(newLabel); + _context.SaveChanges(); + labelTimeLabelDto.Id = newLabel.Id; + return Ok(labelTimeLabelDto); + } +} diff --git a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs new file mode 100644 index 0000000..d845a6f --- /dev/null +++ b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs @@ -0,0 +1,35 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// +public class DeleteLabelEndpoint : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public DeleteLabelEndpoint(AppDbContext context) { + _context = context; + } + + /// + /// Delete a time entry label. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/labels/{id:guid}/delete")] + public override ActionResult Handle(Guid id) { + var label = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); + + if (label == default) { + return NotFound(); + } + + _context.TimeLabels.Remove(label); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs new file mode 100644 index 0000000..c9ccef3 --- /dev/null +++ b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// +public class GetEndpoint : RouteBaseSync.WithoutRequest.WithActionResult> +{ + private readonly AppDbContext _context; + + /// + public GetEndpoint(AppDbContext context) { + _context = context; + } + + /// + /// Get a minimal list of time entry labels. + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/labels")] + public override ActionResult> Handle() { + var labels = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .OrderByDescending(c => c.CreatedAt) + .Select(c => c.AsDto) + .ToList(); + + if (labels.Count == 0) { + return NoContent(); + } + + return Ok(labels); + } +} diff --git a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs new file mode 100644 index 0000000..30d72ec --- /dev/null +++ b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs @@ -0,0 +1,38 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// +public class UpdateLabelEndpoint : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public UpdateLabelEndpoint(AppDbContext context) { + _context = context; + } + + /// + /// Update a time entry label. + /// + /// + /// + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/labels/update")] + public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { + var label = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == labelTimeLabelDto.Id); + if (label == default) { + return NotFound(); + } + + if (LoggedInUser.Id != label.UserId) { + return Forbid(); + } + + label.Name = labelTimeLabelDto.Name; + label.Color = labelTimeLabelDto.Color; + _context.SaveChanges(); + return Ok(); + } +} diff --git a/code/api/src/Endpoints/V1/RouteBaseAsync.cs b/code/api/src/Endpoints/V1/RouteBaseAsync.cs new file mode 100644 index 0000000..1d179f7 --- /dev/null +++ b/code/api/src/Endpoints/V1/RouteBaseAsync.cs @@ -0,0 +1,73 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseAsync +{ + public static class WithRequest + { + public abstract class WithResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task 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..cb27c14 --- /dev/null +++ b/code/api/src/Endpoints/V1/RouteBaseSync.cs @@ -0,0 +1,53 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseSync +{ + public static class WithRequest + { + public abstract class WithResult : BaseRoute + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + } +} diff --git a/code/api/src/IOL.GreatOffice.Api.csproj b/code/api/src/IOL.GreatOffice.Api.csproj new file mode 100644 index 0000000..0b3d37e --- /dev/null +++ b/code/api/src/IOL.GreatOffice.Api.csproj @@ -0,0 +1,56 @@ + + + + net6.0 + ed5ff3e5-46e2-4d7e-8272-7081f5abfee4 + true + true + disable + CS1591 + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + README.md + + + build_and_push.sh + + + CHANGELOG.md + + + cliff.toml + + + Dockerfile + + + + diff --git a/code/api/src/Jobs/JobRegister.cs b/code/api/src/Jobs/JobRegister.cs new file mode 100644 index 0000000..72c2cc7 --- /dev/null +++ b/code/api/src/Jobs/JobRegister.cs @@ -0,0 +1,18 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public static class JobRegister +{ + public static readonly JobKey TokenCleanupKey = new("TokenCleanupJob"); + + public static IServiceCollectionQuartzConfigurator RegisterJobs(this IServiceCollectionQuartzConfigurator configurator) { + configurator.AddJob(TokenCleanupKey); + configurator.AddTrigger(options => { + options.ForJob(TokenCleanupKey) + .WithIdentity(TokenCleanupKey.Name + "-trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(1, 0)); + }); + return configurator; + } +} diff --git a/code/api/src/Jobs/TokenCleanupJob.cs b/code/api/src/Jobs/TokenCleanupJob.cs new file mode 100644 index 0000000..fce40c9 --- /dev/null +++ b/code/api/src/Jobs/TokenCleanupJob.cs @@ -0,0 +1,22 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public class TokenCleanupJob : IJob +{ + private readonly ILogger _logger; + private readonly AppDbContext _context; + + public TokenCleanupJob(ILogger logger, AppDbContext 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/VaultTokenRenewalJob.cs b/code/api/src/Jobs/VaultTokenRenewalJob.cs new file mode 100644 index 0000000..fffbf7c --- /dev/null +++ b/code/api/src/Jobs/VaultTokenRenewalJob.cs @@ -0,0 +1,15 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public class VaultTokenRenewalJob : IJob +{ + private readonly ILogger _logger; + public VaultTokenRenewalJob(ILogger logger) { + _logger = logger; + } + + public Task Execute(IJobExecutionContext context) { + return Task.CompletedTask; + } +} 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..b6a01ff --- /dev/null +++ b/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs @@ -0,0 +1,238 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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(type: "uuid", nullable: false), + name = table.Column(type: "text", nullable: true), + color = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "uuid", nullable: false), + created = table.Column(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(type: "uuid", nullable: false), + name = table.Column(type: "text", nullable: true), + color = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "uuid", nullable: false), + created = table.Column(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(type: "uuid", nullable: false), + username = table.Column(type: "text", nullable: true), + password = table.Column(type: "text", nullable: true), + created = table.Column(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(type: "uuid", nullable: false), + start = table.Column(type: "timestamp with time zone", nullable: false), + stop = table.Column(type: "timestamp with time zone", nullable: false), + note = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "uuid", nullable: false), + category_id = table.Column(type: "uuid", nullable: true), + created = table.Column(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(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: true), + created = table.Column(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(type: "uuid", nullable: false), + labels_id = table.Column(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..368e6b3 --- /dev/null +++ b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs @@ -0,0 +1,229 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Start") + .HasColumnType("timestamp without time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp without time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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..59e6112 --- /dev/null +++ b/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs @@ -0,0 +1,233 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + name: "created", + table: "users", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_labels", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "stop", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "start", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_categories", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + 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( + name: "created", + table: "users", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_labels", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "stop", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "start", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "created", + table: "time_categories", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + 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..2b95f9d --- /dev/null +++ b/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs @@ -0,0 +1,270 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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(type: "text", nullable: false), + user_id = table.Column(type: "uuid", nullable: true), + email = table.Column(type: "text", nullable: true), + refresh_token = table.Column(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..3d57f1a --- /dev/null +++ b/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs @@ -0,0 +1,270 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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..f75400e --- /dev/null +++ b/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs @@ -0,0 +1,290 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + name: "modified_at", + table: "users", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "modified_at", + table: "time_labels", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "modified_at", + table: "time_entries", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "modified_at", + table: "time_categories", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + 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..6c7a76f --- /dev/null +++ b/code/api/src/Migrations/20220319203018_UserBase.Designer.cs @@ -0,0 +1,322 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.Property("password") + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + 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( + 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( + 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( + 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( + 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..c7463fb --- /dev/null +++ b/code/api/src/Migrations/20220320115601_Update1.Designer.cs @@ -0,0 +1,342 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.Property("password") + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + 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( + 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( + 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( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + 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..3a18463 --- /dev/null +++ b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs @@ -0,0 +1,344 @@ +// + + +#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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + 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( + 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..74f9b40 --- /dev/null +++ b/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs @@ -0,0 +1,401 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: true), + expiry_date = table.Column(type: "timestamp with time zone", nullable: false), + allow_read = table.Column(type: "boolean", nullable: false), + allow_create = table.Column(type: "boolean", nullable: false), + allow_update = table.Column(type: "boolean", nullable: false), + allow_delete = table.Column(type: "boolean", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + modified_at = table.Column(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..678c52d --- /dev/null +++ b/code/api/src/Migrations/20220530174741_Tenants.Designer.cs @@ -0,0 +1,710 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property("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( + name: "created_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "deleted_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "modified_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "tenant_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "created_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "deleted_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "modified_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "tenant_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "created_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "deleted_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "modified_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + 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(type: "uuid", nullable: false), + name = table.Column(type: "text", nullable: true), + description = table.Column(type: "text", nullable: true), + contact_email = table.Column(type: "text", nullable: true), + master_user_id = table.Column(type: "uuid", nullable: false), + master_user_password = table.Column(type: "text", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + modified_at = table.Column(type: "timestamp with time zone", nullable: true), + user_id = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), + tenant_id1 = table.Column(type: "uuid", nullable: true), + modified_by_id = table.Column(type: "uuid", nullable: false), + created_by_id = table.Column(type: "uuid", nullable: false), + deleted_by_id = table.Column(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..8fd6b40 --- /dev/null +++ b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs @@ -0,0 +1,686 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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( + 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(type: "uuid", nullable: false), + labels_id = table.Column(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..a05b0e4 --- /dev/null +++ b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs @@ -0,0 +1,656 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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( + name: "tenant_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "modified_by_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "deleted_by_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "created_by_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "tenant_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "modified_by_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "deleted_by_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "created_by_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "tenant_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "modified_by_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "deleted_by_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "created_by_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "tenant_id", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "modified_by_id", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "deleted_by_id", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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..69d4f7e --- /dev/null +++ b/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs @@ -0,0 +1,510 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TenantUser", b => + { + b.Property("TenantsId") + .HasColumnType("uuid") + .HasColumnName("tenants_id"); + + b.Property("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( + name: "deleted", + table: "users", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "email", + table: "users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "first_name", + table: "users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "last_name", + table: "users", + type: "text", + nullable: true); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "deleted", + table: "time_labels", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "deleted", + table: "time_entries", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "deleted", + table: "time_categories", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "user_id", + table: "tenants", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "deleted", + table: "tenants", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "deleted", + table: "api_access_tokens", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "tenant_user", + columns: table => new + { + tenants_id = table.Column(type: "uuid", nullable: false), + users_id = table.Column(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( + 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( + 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( + 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( + 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( + 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..b333f23 --- /dev/null +++ b/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs @@ -0,0 +1,533 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("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("TenantsId") + .HasColumnType("uuid") + .HasColumnName("tenants_id"); + + b.Property("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(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + friendly_name = table.Column(type: "text", nullable: true), + xml = table.Column(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..33b5cfd --- /dev/null +++ b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs @@ -0,0 +1,496 @@ +// +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(AppDbContext))] + [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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("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("TenantsId") + .HasColumnType("uuid") + .HasColumnName("tenants_id"); + + b.Property("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(type: "text", nullable: false), + user_id = table.Column(type: "uuid", nullable: true), + email = table.Column(type: "text", nullable: true), + refresh_token = table.Column(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/AppDbContextModelSnapshot.cs b/code/api/src/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..cc4bf72 --- /dev/null +++ b/code/api/src/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,494 @@ +// +using System; +using IOL.GreatOffice.Api.Data; +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(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("LastName") + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("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("TenantsId") + .HasColumnType("uuid") + .HasColumnName("tenants_id"); + + b.Property("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/Program.cs b/code/api/src/Program.cs new file mode 100644 index 0000000..9a6bc3f --- /dev/null +++ b/code/api/src/Program.cs @@ -0,0 +1,236 @@ +global using System; +global using System.Linq; +global using System.IO; +global using System.Net.Mail; +global using System.Net; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Collections.Generic; +global using System.Runtime.Serialization; +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.Exceptions; +global using IOL.GreatOffice.Api.Data.Dtos; +global using IOL.GreatOffice.Api.Data.Enums; +global using IOL.GreatOffice.Api.Data.Models; +global using IOL.GreatOffice.Api.Data.Results; +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.Hosting; +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.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Serilog; +global using IOL.GreatOffice.Api.Data; +global using IOL.GreatOffice.Api.Data.Static; +global using IOL.GreatOffice.Api.Services; +global using IOL.GreatOffice.Api.Utilities; +using System.Reflection; +using IOL.GreatOffice.Api.Endpoints.V1; +using IOL.GreatOffice.Api.Jobs; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Versioning; +using Quartz; + +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(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddTransient(); + var vaultService = builder.Services.BuildServiceProvider().GetRequiredService(); + var configuration = vaultService.GetCurrentAppConfiguration(); + var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .ReadFrom.Configuration(builder.Configuration) + .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); + builder.WebHost.ConfigureKestrel(kestrel => { + kestrel.AddServerHeader = false; + }); + + if (builder.Environment.IsDevelopment()) { + builder.Services.AddCors(); + } + + if (builder.Environment.IsProduction()) { + builder.Services.Configure(options => { + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; + }); + } + + builder.Services + .AddDataProtection() + .ProtectKeysWithCertificate(configuration.CERT1()) + .PersistKeysToDbContext(); + + builder.Services.Configure(JsonSettings.Default); + builder.Services.AddQuartz(options => { + options.UsePersistentStore(o => { + o.UsePostgres(builder.Configuration.GetQuartzDatabaseConnectionString(vaultService.GetCurrentAppConfiguration)); + o.UseSerializer(); + }); + 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 = "go_session"; + 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(null); + }; + }) + .AddScheme(AppConstants.BASIC_AUTH_SCHEME, default); + + builder.Services.AddDbContext(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, Assembly.GetExecutingAssembly().GetName().Name + ".xml")); + options.UseApiEndpoints(); + options.OperationFilter(); + 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() + } + }); + }); + + builder.Services + .AddControllers() + .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((origin) => true); + cors.AllowCredentials(); + }); + } + + if (app.Environment.IsProduction()) { + app.UseForwardedHeaders(); + } + + app.UseDefaultFiles() + .UseStaticFiles() + .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) { + // This is subject to change in future .net versions, see https://github.com/dotnet/runtime/issues/60600. + if (ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) { + throw; + } + + Log.Fatal(ex, "Unhandled exception"); + return 1; + } finally { + Log.Information("Shut down complete, flusing logs..."); + Log.CloseAndFlush(); + } + } +} 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/Services/MailService.cs b/code/api/src/Services/MailService.cs new file mode 100644 index 0000000..c08cb84 --- /dev/null +++ b/code/api/src/Services/MailService.cs @@ -0,0 +1,49 @@ +namespace IOL.GreatOffice.Api.Services; + +public class MailService +{ + private readonly ILogger _logger; + private static string _emailHost; + private static int _emailPort; + private static string _emailUser; + private static string _emailPassword; + + public MailService(VaultService vaultService, ILogger logger) { + var configuration = vaultService.GetCurrentAppConfiguration(); + _logger = logger; + _emailHost = configuration.SMTP_HOST; + _emailPort = Convert.ToInt32(configuration.SMTP_PORT); + _emailUser = configuration.SMTP_USER; + _emailPassword = configuration.SMTP_PASSWORD; + } + + /// + /// Send an email. + /// + /// + public void SendMail(MailMessage message) { + using var smtpClient = new SmtpClient { + Host = _emailHost, + EnableSsl = _emailPort == 587, + Port = _emailPort, + Credentials = new NetworkCredential { + UserName = _emailUser, + Password = _emailPassword, + } + }; + var configurationString = JsonSerializer.Serialize(new { + smtpClient.Host, + smtpClient.EnableSsl, + smtpClient.Port, + UserName = _emailUser.HasValue() ? "**REDACTED**" : "**MISSING**", + Password = _emailPassword.HasValue() ? "**REDACTED**" : "**MISSING**", + }, + new JsonSerializerOptions { + WriteIndented = true + }); + + _logger.LogDebug("SmtpClient was instansiated with the following configuration\n" + configurationString); + + smtpClient.Send(message); + } +} \ 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..1b4f147 --- /dev/null +++ b/code/api/src/Services/PasswordResetService.cs @@ -0,0 +1,115 @@ +namespace IOL.GreatOffice.Api.Services; + +public class PasswordResetService +{ + private readonly AppDbContext _context; + private readonly MailService _mailService; + private readonly AppConfiguration _configuration; + private readonly ILogger _logger; + + + public PasswordResetService( + AppDbContext context, + VaultService vaultService, + ILogger logger, + MailService mailService + ) { + _context = context; + _configuration = vaultService.GetCurrentAppConfiguration(); + _logger = logger; + _mailService = mailService; + } + + public async Task GetRequestAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _context.ForgotPasswordRequests + .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 FullFillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) { + var request = await GetRequestAsync(id, cancellationToken); + if (request == default) { + throw new ForgotPasswordRequestNotFoundException("Request with id: " + id + " was not found"); + } + + var user = _context.Users.SingleOrDefault(c => c.Id == request.User.Id); + if (user == default) { + throw new UserNotFoundException("User with id: " + request.User.Id + " was not found"); + } + + user.HashAndSetPassword(newPassword); + _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + _logger.LogInformation($"Fullfilled password reset request for user: {request.User.Username}"); + await DeleteRequestsForUserAsync(user.Id, cancellationToken); + return true; + } + + + public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) { + await DeleteRequestsForUserAsync(user.Id, cancellationToken); + var request = new ForgotPasswordRequest(user); + _context.ForgotPasswordRequests.Add(request); + await _context.SaveChangesAsync(cancellationToken); + var portalUrl = _configuration.PORTAL_URL; + var emailFromAddress = _configuration.EMAIL_FROM_ADDRESS; + var emailFromDisplayName = _configuration.EMAIL_FROM_DISPLAY_NAME; + var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id); + var message = new MailMessage { + From = new MailAddress(emailFromAddress, emailFromDisplayName), + To = { + new MailAddress(user.Username) + }, + Subject = "Reset password - Greatoffice", + Body = @$" +Hi {user.Username} + +Go to the following link to set a new password. + +{portalUrl}/reset-password/{request.Id} + +The link expires at {zonedExpirationDate:yyyy-MM-dd hh:mm}. +If you did not request a password reset, no action is required. +" + }; + +#pragma warning disable 4014 + Task.Run(() => { +#pragma warning restore 4014 + _mailService.SendMail(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 = _context.ForgotPasswordRequests.Where(c => c.UserId == userId).ToList(); + if (!requestsToRemove.Any()) return; + _context.ForgotPasswordRequests.RemoveRange(requestsToRemove); + await _context.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 _context.ForgotPasswordRequests.Where(c => c.IsExpired)) { + if (!request.IsExpired) { + continue; + } + + _context.ForgotPasswordRequests.Remove(request); + deleteCount++; + _logger.LogInformation($"Marking password reset request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}."); + } + + await _context.SaveChangesAsync(cancellationToken); + _logger.LogInformation($"Deleted {deleteCount} stale password reset requests."); + } +} \ 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..6db663a --- /dev/null +++ b/code/api/src/Services/UserService.cs @@ -0,0 +1,50 @@ +namespace IOL.GreatOffice.Api.Services; + +public class UserService +{ + private readonly PasswordResetService _passwordResetService; + + /// + /// Provides methods to perform common operations on user data. + /// + /// + public UserService(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } + + /// + /// Log in a user. + /// + /// + /// + /// + public async Task LogInUser(HttpContext httpContext, User user, bool persist = false) { + var claims = new List { + new(AppClaims.USER_ID, user.Id.ToString()), + new(AppClaims.NAME, user.Username), + }; + + var identity = new ClaimsIdentity(claims, 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); + } + + /// + /// Log out a user. + /// + /// + public async Task LogOutUser(HttpContext httpContext) { + await httpContext.SignOutAsync(); + } +} diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs new file mode 100644 index 0000000..732911a --- /dev/null +++ b/code/api/src/Services/VaultService.cs @@ -0,0 +1,158 @@ +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 _logger; + private int CACHE_TTL { get; set; } + + public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger logger) { + var token = configuration.GetValue(AppEnvironmentVariables.VAULT_TOKEN); + var vaultUrl = configuration.GetValue(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 static object Data { get; set; } + + public T Get(string path) { + var result = _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY, + cacheEntry => { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL); + var getSecretResponse = _client.GetFromJsonAsync>("/v1/kv/data/" + path).Result; + + 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 T Refresh(string path) { + _cache.Remove(AppConstants.VAULT_CACHE_KEY); + CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12); + return Get(path); + } + + public async Task RenewTokenAsync(string token) { + var response = await _client.PostAsJsonAsync("v1/auth/token/renew", + new { + Token = token + }); + if (response.IsSuccessStatusCode) { + return await response.Content.ReadFromJsonAsync(); + } + + return default; + } + + public AppConfiguration GetCurrentAppConfiguration() { + var path = _configuration.GetValue(AppEnvironmentVariables.MAIN_CONFIG_SHEET); + var result = Get(path); + var overwrites = new { + DB_HOST = _configuration.GetValue("OVERWRITE_DB_HOST", string.Empty), + DB_PORT = _configuration.GetValue("OVERWRITE_DB_PORT", string.Empty), + DB_USER = _configuration.GetValue("OVERWRITE_DB_USER", string.Empty), + DB_PASSWORD = _configuration.GetValue("OVERWRITE_DB_PASSWORD", string.Empty), + DB_NAME = _configuration.GetValue("OVERWRITE_DB_NAME", string.Empty), + }; + if (overwrites.DB_HOST.HasValue()) { + _logger.LogInformation("OVERWRITE_DB_HOST is specified, using it's value: " + overwrites.DB_HOST); + result.DB_HOST = overwrites.DB_HOST; + } + + if (overwrites.DB_PORT.HasValue()) { + _logger.LogInformation("OVERWRITE_DB_PORT is specified, using it's value: " + overwrites.DB_PORT); + result.DB_PORT = overwrites.DB_PORT; + } + + if (overwrites.DB_USER.HasValue()) { + _logger.LogInformation("OVERWRITE_DB_USER is specified, using it's value: " + overwrites.DB_USER); + result.DB_USER = overwrites.DB_USER; + } + + if (overwrites.DB_PASSWORD.HasValue()) { + _logger.LogInformation("OVERWRITE_DB_PASSWORD is specified, using it's value: " + "(redacted)"); + result.DB_PASSWORD = overwrites.DB_PASSWORD; + } + + if (overwrites.DB_NAME.HasValue()) { + _logger.LogInformation("OVERWRITE_DB_NAME is specified, using it's value: " + overwrites.DB_NAME); + result.DB_NAME = overwrites.DB_NAME; + } + + return result; + } + + public AppConfiguration RefreshCurrentAppConfiguration() { + var path = _configuration.GetValue(AppEnvironmentVariables.MAIN_CONFIG_SHEET); + return Refresh(path); + } + + 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 Warnings { get; set; } + public Auth Auth { get; set; } + } + + public class Auth + { + public string ClientToken { get; set; } + public string Accessor { get; set; } + public List Policies { get; set; } + public List 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 + { + public VaultSecret Data { get; set; } + } + + public class VaultSecret + { + 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; } + } +} 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..6138193 --- /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 +{ + private readonly AppDbContext _context; + private readonly AppConfiguration _configuration; + private readonly ILogger _logger; + + public BasicAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + AppDbContext context, + VaultService vaultService + ) : + base(options, logger, encoder, clock) { + _context = context; + _configuration = vaultService.GetCurrentAppConfiguration(); + _logger = logger.CreateLogger(); + } + + protected override Task HandleAuthenticateAsync() { + var endpoint = Context.GetEndpoint(); + if (endpoint?.Metadata.GetMetadata() != 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() { + 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..405c702 --- /dev/null +++ b/code/api/src/Utilities/ConfigurationExtensions.cs @@ -0,0 +1,88 @@ +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; + + var res = ""; + if (config.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; + } else { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + Log.Debug("Using app database connection string: " + res); + return res; + } + + public static string GetAppDatabaseConnectionString(this IConfiguration config, Func 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; + + var res = ""; + if (config.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; + } else { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + Log.Debug("Using app database connection string: " + res); + return res; + } + + 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; + + var res = ""; + if (config.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; + } else { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + Log.Debug("Using quartz database connection string: " + res); + return res; + } + + public static string GetQuartzDatabaseConnectionString(this IConfiguration config, Func 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; + + var res = ""; + if (config.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; + } else { + res = $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + Log.Debug("Using quartz database connection string: " + res); + return res; + } + + public static string GetVersion(this IConfiguration configuration) { + var versionFilePath = Path.Combine(AppPaths.AppData.HostPath, "version.txt"); + if (File.Exists(versionFilePath)) { + var versionText = File.ReadAllText(versionFilePath); + return versionText + "-" + configuration.GetValue("ASPNETCORE_ENVIRONMENT"); + } + + return "unknown-" + configuration.GetValue("ASPNETCORE_ENVIRONMENT"); + } +} diff --git a/code/api/src/Utilities/GithubAuthenticationHelpers.cs b/code/api/src/Utilities/GithubAuthenticationHelpers.cs new file mode 100644 index 0000000..a4461d2 --- /dev/null +++ b/code/api/src/Utilities/GithubAuthenticationHelpers.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authentication.OAuth; +using Npgsql; + +namespace IOL.GreatOffice.Api.Utilities; + +public static class GithubAuthenticationHelpers +{ + public static async Task HandleGithubTicketCreation(OAuthCreatingTicketContext context, IConfiguration configuration, AppConfiguration options) { + var githubId = context.Identity?.FindFirst(p => p.Type == ClaimTypes.NameIdentifier)?.Value; + var githubUsername = context.Identity?.FindFirst(p => p.Type == ClaimTypes.Name)?.Value; + var githubEmail = context.Identity?.FindFirst(p => p.Type == ClaimTypes.Email)?.Value; + + if (githubId.IsNullOrWhiteSpace() || githubUsername.IsNullOrWhiteSpace() || context.Identity == default) { + return; + } + + var claims = context.Identity.Claims.ToList(); + foreach (var claim in claims) { + context.Identity.RemoveClaim(claim); + } + + var connstring = configuration.GetAppDatabaseConnectionString(options); + var connection = new NpgsqlConnection(connstring); + + Log.Information($"Getting user mappings for github user: {githubId}"); + var getMappedUserQuery = @$"SELECT u.id,u.username FROM github_user_mappings INNER JOIN users u on u.id = github_user_mappings.user_id WHERE github_id='{githubId}'"; + await connection.OpenAsync(); + await using var getMappedUserCommand = new NpgsqlCommand(getMappedUserQuery, connection); + await using var reader = await getMappedUserCommand.ExecuteReaderAsync(); + var handled = false; + while (await reader.ReadAsync()) { + try { + var userId = reader.GetGuid(0); + var username = reader.GetString(1); + context.Identity.AddClaim(new Claim(AppClaims.USER_ID, userId.ToString())); + context.Identity.AddClaim(new Claim(AppClaims.NAME, username)); + if (context.AccessToken != default) { + context.Identity.AddClaim(new Claim(AppClaims.GITHUB_ACCESS_TOKEN, context.AccessToken ?? "")); + } + + Log.Information($"Found mapping for github id {githubId} mapped to user id {userId}"); + handled = true; + } catch (Exception e) { + Log.Error(e, "An exception occured when handling github user mappings"); + handled = false; + } + } + + await connection.CloseAsync(); + + if (!handled) { + var userId = Guid.NewGuid(); + + var insertUserQuery = $@"INSERT INTO users VALUES ('{userId}', '{githubUsername}', '', '{AppDateTime.UtcNow}')"; + await connection.OpenAsync(); + await using var insertUserCommand = new NpgsqlCommand(insertUserQuery, connection); + await insertUserCommand.ExecuteNonQueryAsync(); + await connection.CloseAsync(); + + var refreshTokenEncryptionKey = options.APP_AES_KEY; + string insertMappingQuery; + + if (context.RefreshToken.HasValue() && refreshTokenEncryptionKey.HasValue()) { + var encryptedRefreshToken = context.RefreshToken.EncryptWithAes(refreshTokenEncryptionKey); + insertMappingQuery = $@"INSERT INTO github_user_mappings VALUES ('{githubId}', '{userId}', '{githubEmail}', '{encryptedRefreshToken}')"; + } else { + insertMappingQuery = $@"INSERT INTO github_user_mappings VALUES ('{githubId}', '{userId}', '{githubEmail}', '')"; + } + + await connection.OpenAsync(); + await using var insertMappingCommand = new NpgsqlCommand(insertMappingQuery, connection); + await insertMappingCommand.ExecuteNonQueryAsync(); + await connection.CloseAsync(); + + context.Identity.AddClaim(new Claim(AppClaims.USER_ID, userId.ToString())); + context.Identity.AddClaim(new Claim(AppClaims.NAME, githubUsername)); + if (context.AccessToken != default) { + context.Identity.AddClaim(new Claim(AppClaims.GITHUB_ACCESS_TOKEN, context.AccessToken ?? "")); + } + + Log.Information($"Created mapping for github id {githubId} mapped to user id {userId}"); + } + } +} 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 obj) where T : class { + return JsonSerializer.SerializeToUtf8Bytes(obj); + } + + public T DeSerialize(byte[] data) where T : class { + return JsonSerializer.Deserialize(data); + } +} diff --git a/code/api/src/Utilities/SwaggerDefaultValues.cs b/code/api/src/Utilities/SwaggerDefaultValues.cs new file mode 100644 index 0000000..4b5c764 --- /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; + +/// +/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. +/// +/// This is only required due to bugs in the . +/// Once they are fixed and published, this class can be removed. +public class SwaggerDefaultValues : IOperationFilter +{ + /// + /// Applies the filter to the specified operation using the given context. + /// + /// The operation to apply the filter to. + /// The current operation filter context. + 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.Any(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..a2dcf7a --- /dev/null +++ b/code/api/src/Utilities/SwaggerGenOptionsExtensions.cs @@ -0,0 +1,43 @@ +#nullable enable +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Swashbuckle.AspNetCore.SwaggerGen; +using BaseRoute = IOL.GreatOffice.Api.Endpoints.V1.BaseRoute; + +namespace IOL.GreatOffice.Api.Utilities; + +public static class SwaggerGenOptionsExtensions +{ + /// + /// Updates Swagger document to support ApiEndpoints.

+ /// For controllers inherited from :
+ /// - Replaces action Tag with [namespace]
+ ///
+ public static void UseApiEndpoints(this SwaggerGenOptions options) { + options.TagActionsBy(EndpointNamespaceOrDefault); + } + + private static IList 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(BaseRoute))) { + return new[] { + actionDescriptor.ControllerTypeInfo.Namespace?.Split('.').Last() + }; + } + + return new[] { + actionDescriptor.ControllerName + }; + } + + public static IEnumerable GetBaseTypesAndThis(this Type type) { + Type? current = type; + while (current != null) { + yield return current; + current = current.BaseType; + } + } +} diff --git a/code/api/src/appsettings.json b/code/api/src/appsettings.json new file mode 100644 index 0000000..8727fd7 --- /dev/null +++ b/code/api/src/appsettings.json @@ -0,0 +1,22 @@ +{ + "AllowedHosts": "*", + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Filter": [ + { + "Name": "ByExcluding", + "Args": { + "expression": "@mt = 'An unhandled exception has occurred while executing the request.'" + } + } + ] + } +} 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/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..a51035e --- /dev/null +++ b/code/app/.typesafe-i18n.json @@ -0,0 +1,5 @@ +{ + "adapter": "svelte", + "$schema": "https://unpkg.com/typesafe-i18n@5.14.0/schema/typesafe-i18n.json", + "outputPath": "src/lib/i18n" +} \ No newline at end of file diff --git a/code/app/package.json b/code/app/package.json new file mode 100644 index 0000000..5eb0be7 --- /dev/null +++ b/code/app/package.json @@ -0,0 +1,46 @@ +{ + "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": { + "@developermuch/dev-svelte-headlessui": "0.0.1", + "@playwright/test": "^1.26.1", + "@rgossiaux/svelte-headlessui": "^1.0.2", + "@sveltejs/adapter-node": "1.0.0-next.96", + "@sveltejs/kit": "1.0.0-next.507", + "@sveltestack/svelte-query": "^1.6.0", + "@tailwindcss/forms": "^0.5.3", + "@tanstack/svelte-table": "^8.5.15", + "@types/cookie": "^0.5.1", + "@types/js-cookie": "^3.0.2", + "autoprefixer": "^10.4.12", + "cookie": "^0.5.0", + "devalue": "^3.1.3", + "js-cookie": "^3.0.1", + "npm-run-all": "^4.1.5", + "pino": "^8.6.1", + "pino-pretty": "^9.1.0", + "postcss": "^8.4.17", + "postcss-load-config": "^4.0.1", + "svelte": "^3.50.1", + "svelte-check": "^2.9.1", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "^3.1.8", + "temporal-polyfill": "^0.0.8", + "tslib": "^2.4.0", + "typesafe-i18n": "^5.14.0", + "typescript": "^4.8.4", + "vite": "^3.1.4" + } +} \ 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..fa03ab4 --- /dev/null +++ b/code/app/pnpm-lock.yaml @@ -0,0 +1,2299 @@ +lockfileVersion: 5.4 + +specifiers: + '@developermuch/dev-svelte-headlessui': 0.0.1 + '@playwright/test': ^1.26.1 + '@rgossiaux/svelte-headlessui': ^1.0.2 + '@sveltejs/adapter-node': 1.0.0-next.96 + '@sveltejs/kit': 1.0.0-next.507 + '@sveltestack/svelte-query': ^1.6.0 + '@tailwindcss/forms': ^0.5.3 + '@tanstack/svelte-table': ^8.5.15 + '@types/cookie': ^0.5.1 + '@types/js-cookie': ^3.0.2 + autoprefixer: ^10.4.12 + cookie: ^0.5.0 + devalue: ^3.1.3 + js-cookie: ^3.0.1 + npm-run-all: ^4.1.5 + pino: ^8.6.1 + pino-pretty: ^9.1.0 + postcss: ^8.4.17 + postcss-load-config: ^4.0.1 + svelte: ^3.50.1 + svelte-check: ^2.9.1 + svelte-preprocess: ^4.10.7 + tailwindcss: ^3.1.8 + temporal-polyfill: ^0.0.8 + tslib: ^2.4.0 + typesafe-i18n: ^5.14.0 + typescript: ^4.8.4 + vite: ^3.1.4 + +devDependencies: + '@developermuch/dev-svelte-headlessui': 0.0.1_svelte@3.50.1 + '@playwright/test': 1.26.1 + '@rgossiaux/svelte-headlessui': 1.0.2_svelte@3.50.1 + '@sveltejs/adapter-node': 1.0.0-next.96 + '@sveltejs/kit': 1.0.0-next.507_svelte@3.50.1+vite@3.1.4 + '@sveltestack/svelte-query': 1.6.0 + '@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8 + '@tanstack/svelte-table': 8.5.15_svelte@3.50.1 + '@types/cookie': 0.5.1 + '@types/js-cookie': 3.0.2 + autoprefixer: 10.4.12_postcss@8.4.17 + cookie: 0.5.0 + devalue: 3.1.3 + js-cookie: 3.0.1 + npm-run-all: 4.1.5 + pino: 8.6.1 + pino-pretty: 9.1.0 + postcss: 8.4.17 + postcss-load-config: 4.0.1_postcss@8.4.17 + svelte: 3.50.1 + svelte-check: 2.9.1_ejhwqstqdwfnekvhsm3hus3z4i + svelte-preprocess: 4.10.7_or4gyn62tntw7ihg73nagmkdja + tailwindcss: 3.1.8_postcss@8.4.17 + temporal-polyfill: 0.0.8 + tslib: 2.4.0 + typesafe-i18n: 5.14.0_typescript@4.8.4 + typescript: 4.8.4 + vite: 3.1.4 + +packages: + + /@developermuch/dev-svelte-headlessui/0.0.1_svelte@3.50.1: + resolution: {integrity: sha512-tfBlHliv75oQFRrC430nIsw+A8+iFmr5c2g0A+VTlVD3960nEL9jOE0LDHYKq6VhX5LnOLTFIZwVKC1DxFo0QA==} + peerDependencies: + svelte: ^3.44.0 + dependencies: + svelte: 3.50.1 + dev: true + + /@esbuild/android-arm/0.15.10: + resolution: {integrity: sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64/0.15.10: + resolution: {integrity: sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + 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.15: + resolution: {integrity: sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==} + 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.13.0 + dev: true + + /@playwright/test/1.26.1: + resolution: {integrity: sha512-bNxyZASVt2adSZ9gbD7NCydzcb5JaI0OR9hc7s+nmPeH604gwp0zp17NNpwXY4c8nvuBGQQ9oGDx72LE+cUWvw==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@types/node': 18.8.0 + playwright-core: 1.26.1 + 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.50.1: + resolution: {integrity: sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==} + peerDependencies: + svelte: ^3.44.0 + dependencies: + svelte: 3.50.1 + dev: true + + /@rollup/plugin-commonjs/22.0.2_rollup@2.79.1: + resolution: {integrity: sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==} + engines: {node: '>= 12.0.0'} + peerDependencies: + rollup: ^2.68.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 7.2.3 + is-reference: 1.2.1 + magic-string: 0.25.9 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-json/4.1.0_rollup@2.79.1: + resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-node-resolve/14.1.0_rollup@2.79.1: + resolution: {integrity: sha512-5G2niJroNCz/1zqwXtk0t9+twOSDlG00k1Wfd7bkbbXmwg8H8dvgHdIWAun53Ps/rckfvOC7scDBjuGFg5OaWw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.78.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + '@types/resolve': 1.17.1 + deepmerge: 4.2.2 + is-builtin-module: 3.2.0 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + 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/4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@sveltejs/adapter-node/1.0.0-next.96: + resolution: {integrity: sha512-tIHaRolUYy2PiHl4RUWaOsYxEjK5lN9501qzCzFbYr/uoLnZcnPGSXNJICwX0AX9AUkV6cvkZey6bLbUQcwH0Q==} + dependencies: + '@rollup/plugin-commonjs': 22.0.2_rollup@2.79.1 + '@rollup/plugin-json': 4.1.0_rollup@2.79.1 + '@rollup/plugin-node-resolve': 14.1.0_rollup@2.79.1 + rollup: 2.79.1 + dev: true + + /@sveltejs/kit/1.0.0-next.507_svelte@3.50.1+vite@3.1.4: + resolution: {integrity: sha512-GAgFb1yLUVOYPWXIPxh8j0iEjUOVvN42Xgsqf6j6j1Sb2/f0m0bC1O7eVbc8NQjNZIvgGuN8yxai188iIYQt7w==} + engines: {node: '>=16.14'} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.44.0 + vite: ^3.1.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 1.0.8_svelte@3.50.1+vite@3.1.4 + '@types/cookie': 0.5.1 + cookie: 0.5.0 + devalue: 3.1.3 + kleur: 4.1.5 + magic-string: 0.26.5 + mime: 3.0.0 + node-fetch: 3.2.10 + sade: 1.8.1 + set-cookie-parser: 2.5.1 + sirv: 2.0.2 + svelte: 3.50.1 + tiny-glob: 0.2.9 + undici: 5.10.0 + vite: 3.1.4 + transitivePeerDependencies: + - diff-match-patch + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte/1.0.8_svelte@3.50.1+vite@3.1.4: + resolution: {integrity: sha512-1xkVTB4pm6zuign858FzVYE9Fdw9MQBOlxrdd85STV0NvTDmcofcRpcrK+zcIyT8SZ2dseHLu8hvDwzssF6RfA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + diff-match-patch: ^1.0.5 + svelte: ^3.44.0 + vite: ^3.0.0 + peerDependenciesMeta: + diff-match-patch: + optional: true + dependencies: + '@rollup/pluginutils': 4.2.1 + debug: 4.3.4 + deepmerge: 4.2.2 + kleur: 4.1.5 + magic-string: 0.26.5 + svelte: 3.50.1 + svelte-hmr: 0.15.0_svelte@3.50.1 + vite: 3.1.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltestack/svelte-query/1.6.0: + resolution: {integrity: sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==} + peerDependencies: + broadcast-channel: ^4.5.0 + peerDependenciesMeta: + broadcast-channel: + optional: true + dev: true + + /@tailwindcss/forms/0.5.3_tailwindcss@3.1.8: + 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.1.8_postcss@8.4.17 + dev: true + + /@tanstack/svelte-table/8.5.15_svelte@3.50.1: + resolution: {integrity: sha512-Rxisdm7kcH8I33DOAv17bsVELbMTqJcZks5PK3Khe9jtXZADXurTEM9RSgmm/HNdHPF0CHnp90eOwtdpIlp23Q==} + engines: {node: '>=12'} + peerDependencies: + svelte: ^3.48.0 + dependencies: + '@tanstack/table-core': 8.5.15 + svelte: 3.50.1 + dev: true + + /@tanstack/table-core/8.5.15: + resolution: {integrity: sha512-k+BcCOAYD610Cij6p1BPyEqjMQjZIdAnVDoIUKVnA/tfHbF4JlDP7pKAftXPBxyyX5Z1yQPurPnOdEY007Snyg==} + engines: {node: '>=12'} + 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/js-cookie/3.0.2: + resolution: {integrity: sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==} + dev: true + + /@types/node/18.8.0: + resolution: {integrity: sha512-u+h43R6U8xXDt2vzUaVP3VwjjLyOJk6uEciZS8OSyziUQGOwmk+l+4drxcsDboHXwyTaqS1INebghmWMRxq3LA==} + dev: true + + /@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.8.0 + dev: true + + /@types/sass/1.43.1: + resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==} + dependencies: + '@types/node': 18.8.0 + 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 + dev: true + + /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 + + /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 + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + 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 + + /atomic-sleep/1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: true + + /autoprefixer/10.4.12_postcss@8.4.17: + resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.4 + caniuse-lite: 1.0.30001414 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.17 + postcss-value-parser: 4.2.0 + 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==} + dev: true + + /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.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.30001414 + electron-to-chromium: 1.4.270 + node-releases: 2.0.6 + update-browserslist-db: 1.0.9_browserslist@4.21.4 + dev: true + + /buffer-crc32/0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.1.3 + 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.30001414: + resolution: {integrity: sha512-t55jfSaWjCdocnFdKQoO+d2ct9C59UZg4dY3OnUlSZ447r8pUtIKdp0hpAzrGFultmTC+Us+KpKi4GZl/LXlFg==} + 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 + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + 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-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 + + /commondir/1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + 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 + + /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 + + /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.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + 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.0: + resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} + 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.0 + minimist: 1.2.6 + dev: true + + /devalue/3.1.3: + resolution: {integrity: sha512-9KO89Cb+qjzf2CqdrH+NuLaqdk9GhDP5EhR4zlkR51dvuIaiqtlkDkGzLMShDemwUy21raSMdu+kpX8Enw3yGQ==} + 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 + + /electron-to-chromium/1.4.270: + resolution: {integrity: sha512-KNhIzgLiJmDDC444dj9vEOpZEgsV96ult9Iff98Vanumn+ShJHd5se8aX6KeVxdc0YQeqdrezBZv89rleDbvSg==} + 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.20.3: + resolution: {integrity: sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.1.3 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + 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-weakref: 1.0.2 + object-inspect: 1.12.2 + 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.5 + string.prototype.trimstart: 1.0.5 + unbox-primitive: 1.0.2 + 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-android-64/0.15.10: + resolution: {integrity: sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.15.10: + resolution: {integrity: sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.15.10: + resolution: {integrity: sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.15.10: + resolution: {integrity: sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.15.10: + resolution: {integrity: sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.15.10: + resolution: {integrity: sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.15.10: + resolution: {integrity: sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.15.10: + resolution: {integrity: sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.15.10: + resolution: {integrity: sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.15.10: + resolution: {integrity: sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.15.10: + resolution: {integrity: sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.15.10: + resolution: {integrity: sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.15.10: + resolution: {integrity: sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.15.10: + resolution: {integrity: sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.15.10: + resolution: {integrity: sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.15.10: + resolution: {integrity: sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.15.10: + resolution: {integrity: sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.15.10: + resolution: {integrity: sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.15.10: + resolution: {integrity: sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.15.10: + resolution: {integrity: sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.15.10: + resolution: {integrity: sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.15.10 + '@esbuild/linux-loong64': 0.15.10 + esbuild-android-64: 0.15.10 + esbuild-android-arm64: 0.15.10 + esbuild-darwin-64: 0.15.10 + esbuild-darwin-arm64: 0.15.10 + esbuild-freebsd-64: 0.15.10 + esbuild-freebsd-arm64: 0.15.10 + esbuild-linux-32: 0.15.10 + esbuild-linux-64: 0.15.10 + esbuild-linux-arm: 0.15.10 + esbuild-linux-arm64: 0.15.10 + esbuild-linux-mips64le: 0.15.10 + esbuild-linux-ppc64le: 0.15.10 + esbuild-linux-riscv64: 0.15.10 + esbuild-linux-s390x: 0.15.10 + esbuild-netbsd-64: 0.15.10 + esbuild-openbsd-64: 0.15.10 + esbuild-sunos-64: 0.15.10 + esbuild-windows-32: 0.15.10 + esbuild-windows-64: 0.15.10 + esbuild-windows-arm64: 0.15.10 + 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 + + /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 + + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /fast-copy/2.1.7: + resolution: {integrity: sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==} + 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-redact/3.1.2: + resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==} + engines: {node: '>=6'} + dev: true + + /fast-safe-stringify/2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + 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 + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + 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.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.20.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-intrinsic/1.1.3: + resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + 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.1.3 + 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.0 + once: 1.4.0 + 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 + + /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-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.1.3 + 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.1.0: + resolution: {integrity: sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw==} + dependencies: + glob: 8.0.3 + readable-stream: 3.6.0 + dev: true + + /hosted-git-info/2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + 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 + + /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 + + /internal-slot/1.0.3: + resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.3 + has: 1.0.3 + side-channel: 1.0.4 + 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.0: + resolution: {integrity: sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==} + 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.10.0: + resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + 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-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-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + 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-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 + + /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: true + + /json-parse-better-errors/1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + 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 + + /magic-string/0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string/0.26.5: + resolution: {integrity: sha512-yXUIYOOQnEHKHOftp5shMWpB9ImfgfDJpapa38j/qMtTj5QHWucvxP4lUtuRmHT9vAzvtpHkWKXW9xBwimXeNg==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /memorystream/0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + 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.0: + resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist/1.2.6: + resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + dev: true + + /mkdirp/0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.6 + 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-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: true + + /node-fetch/3.2.10: + resolution: {integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==} + 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-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.7.3 + string.prototype.padend: 3.1.3 + dev: true + + /object-hash/3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect/1.12.2: + resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + 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==} + dev: true + + /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.2.0 + split2: 4.1.0 + dev: true + + /pino-pretty/9.1.0: + resolution: {integrity: sha512-IM6NY9LLo/dVgY7/prJhCh4rAJukafdt0ibxeNOWc2fxKMyTk90SOB9Ao2HfbtShT9QPeP0ePpJktksMhSQMYA==} + hasBin: true + dependencies: + colorette: 2.0.19 + dateformat: 4.6.3 + fast-copy: 2.1.7 + fast-safe-stringify: 2.1.1 + help-me: 4.1.0 + joycon: 3.1.1 + minimist: 1.2.6 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pump: 3.0.0 + readable-stream: 4.2.0 + secure-json-parse: 2.5.0 + sonic-boom: 3.2.0 + strip-json-comments: 3.1.1 + dev: true + + /pino-std-serializers/6.0.0: + resolution: {integrity: sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==} + dev: true + + /pino/8.6.1: + resolution: {integrity: sha512-fi+V2K98eMZjQ/uEHHSiMALNrz7HaFdKNYuyA3ZUrbH0f1e8sPFDmeRGzg7ZH2q4QDxGnJPOswmqlEaTAZeDPA==} + 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.0.0 + process-warning: 2.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.0 + sonic-boom: 3.2.0 + thread-stream: 2.2.0 + dev: true + + /playwright-core/1.26.1: + resolution: {integrity: sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /postcss-import/14.1.0_postcss@8.4.17: + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.17 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: true + + /postcss-js/4.0.0_postcss@8.4.17: + resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.3.3 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.17 + dev: true + + /postcss-load-config/3.1.4_postcss@8.4.17: + 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.17 + yaml: 1.10.2 + dev: true + + /postcss-load-config/4.0.1_postcss@8.4.17: + 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.17 + yaml: 2.1.2 + dev: true + + /postcss-nested/5.0.6_postcss@8.4.17: + resolution: {integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.17 + postcss-selector-parser: 6.0.10 + dev: true + + /postcss-selector-parser/6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + 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.17: + resolution: {integrity: sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /process-warning/2.0.0: + resolution: {integrity: sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==} + dev: true + + /process/0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + 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 + + /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: 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/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 + dev: true + + /readable-stream/4.2.0: + resolution: {integrity: sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==} + 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 + dev: true + + /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: 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 + + /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.10.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/2.78.1: + resolution: {integrity: sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.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==} + dev: true + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-regex: 1.1.4 + dev: true + + /safe-stable-stringify/2.4.0: + resolution: {integrity: sha512-eehKHKpab6E741ud7ZIMcXhKcP6TSIezPkNZhy5U8xC6+VvrRdUA2tMgxGxaGl4cz7c2Ew5+mg5+wNB16KQqrA==} + engines: {node: '>=10'} + dev: true + + /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.5.0: + resolution: {integrity: sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==} + dev: true + + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + 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.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + dev: true + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + object-inspect: 1.12.2 + 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.0: + resolution: {integrity: sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + + /sorcery/0.10.0: + resolution: {integrity: sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==} + hasBin: true + dependencies: + buffer-crc32: 0.2.13 + minimist: 1.2.6 + sander: 0.5.1 + sourcemap-codec: 1.4.8 + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + 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/4.1.0: + resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} + engines: {node: '>= 10.x'} + dev: true + + /string.prototype.padend/3.1.3: + resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.3 + dev: true + + /string.prototype.trimend/1.0.5: + resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.3 + dev: true + + /string.prototype.trimstart/1.0.5: + resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.3 + dev: true + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-bom/3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + 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-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svelte-check/2.9.1_ejhwqstqdwfnekvhsm3hus3z4i: + resolution: {integrity: sha512-+BFPsj6irZ+t2pVSVo//2Ic1mI3A52xCwbkSTVhTqYZqgawcyZd9pYZoEac3fIWbEeTyCb5X82ORKI/gjn+P7A==} + hasBin: true + peerDependencies: + svelte: ^3.24.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.15 + chokidar: 3.5.3 + fast-glob: 3.2.12 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 3.50.1 + svelte-preprocess: 4.10.7_or4gyn62tntw7ihg73nagmkdja + typescript: 4.8.4 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - node-sass + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-hmr/0.15.0_svelte@3.50.1: + resolution: {integrity: sha512-Aw21SsyoohyVn4yiKXWPNCSW2DQNH/76kvUnE9kpt4h9hcg9tfyQc6xshx9hzgMfGF0kVx0EGD8oBMWSnATeOg==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: '>=3.19.0' + dependencies: + svelte: 3.50.1 + dev: true + + /svelte-preprocess/4.10.7_or4gyn62tntw7ihg73nagmkdja: + resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==} + engines: {node: '>= 9.11.2'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + node-sass: '*' + 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 + svelte: ^3.23.0 + typescript: ^3.9.5 || ^4.0.0 + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + node-sass: + 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.25.9 + postcss: 8.4.17 + postcss-load-config: 4.0.1_postcss@8.4.17 + sorcery: 0.10.0 + strip-indent: 3.0.0 + svelte: 3.50.1 + typescript: 4.8.4 + dev: true + + /svelte/3.50.1: + resolution: {integrity: sha512-bS4odcsdj5D5jEg6riZuMg5NKelzPtmsCbD9RG+8umU03TeNkdWnP6pqbCm0s8UQNBkqk29w/Bdubn3C+HWSwA==} + engines: {node: '>= 8'} + dev: true + + /tailwindcss/3.1.8_postcss@8.4.17: + resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} + 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 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.17 + postcss-import: 14.1.0_postcss@8.4.17 + postcss-js: 4.0.0_postcss@8.4.17 + postcss-load-config: 3.1.4_postcss@8.4.17 + postcss-nested: 5.0.6_postcss@8.4.17 + postcss-selector-parser: 6.0.10 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.1 + transitivePeerDependencies: + - ts-node + dev: true + + /temporal-polyfill/0.0.8: + resolution: {integrity: sha512-IuA8GhS1PRC04H/zVNAIxJvCZQum6V5HjqFj7gz1a3SMUf/Kf1xIXILNYtxrWYnGqIU/RrDRxlCKCm/vmqnBvw==} + dependencies: + temporal-spec: 0.0.3 + dev: true + + /temporal-spec/0.0.3: + resolution: {integrity: sha512-gJu7QRqn5c2vTSkYWGC4qz1i+FZ9C+Cz16UIBMRcjgXOsHfXeSIgaWUKeq/2rz1iNfFxvmF/ywqbfC6ggTpjkA==} + dev: true + + /thread-stream/2.2.0: + resolution: {integrity: sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==} + dependencies: + real-require: 0.2.0 + dev: true + + /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-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 + + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: true + + /typesafe-i18n/5.14.0_typescript@4.8.4: + resolution: {integrity: sha512-ZNHysUvZZhmUuMjBvDGtUI8vT3g//4ay5fFOk2sJCsjx4ztippW1Hrhrq59nJ9mV/Q0u4OX80Gyorq8L3rwNLw==} + hasBin: true + peerDependencies: + typescript: '>=3.5.1' + dependencies: + typescript: 4.8.4 + dev: true + + /typescript/4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + 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.10.0: + resolution: {integrity: sha512-c8HsD3IbwmjjbLvoZuRI26TZic+TSEe8FPMLLOkN1AfYRhdjnKBU6yL+IwcSCbdZiX4e5t0lfMDLDCqj4Sq70g==} + engines: {node: '>=12.18'} + dev: true + + /update-browserslist-db/1.0.9_browserslist@4.21.4: + resolution: {integrity: sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.4 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + 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 + + /vite/3.1.4: + resolution: {integrity: sha512-JoQI08aBjY9lycL7jcEq4p9o1xUjq5aRvdH4KWaXtkSx7e7RpAh9D3IjzDWRD4Fg44LS3oDAIOG/Kq1L+82psA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + terser: ^5.4.0 + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.15.10 + postcss: 8.4.17 + resolve: 1.22.1 + rollup: 2.78.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /web-streams-polyfill/3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + 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/1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + 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 + + /yaml/1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yaml/2.1.2: + resolution: {integrity: sha512-VSdf2/K3FqAetooKQv45Hcu6sA00aDgWZeGcG6V9IYJnVLTnb6988Tie79K5nx2vK7cEpf+yW8Oy+7iPAbdiHA==} + 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.js b/code/app/src/actions/pwKey.js new file mode 100644 index 0000000..2c019f3 --- /dev/null +++ b/code/app/src/actions/pwKey.js @@ -0,0 +1,12 @@ +import { is_development, is_testing } from "$lib/configuration"; +export default function pwKey(node, value) { + if (!value) + return; + if (!is_testing()) { + if (is_development()) + console.warn("VITE_TESTING is false, so not setting pw-key attributes"); + return; + } + node.setAttribute("pw-key", value); +} +//# sourceMappingURL=pwKey.js.map \ No newline at end of file diff --git a/code/app/src/actions/pwKey.js.map b/code/app/src/actions/pwKey.js.map new file mode 100644 index 0000000..2c37f87 --- /dev/null +++ b/code/app/src/actions/pwKey.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pwKey.js","sourceRoot":"","sources":["pwKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhE,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAiB,EAAE,KAAyB;IACtE,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,IAAI,CAAC,UAAU,EAAE,EAAE;QACf,IAAI,cAAc,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;QAC9F,OAAO;KACV;IACD,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC"} \ No newline at end of file diff --git a/code/app/src/actions/pwKey.ts b/code/app/src/actions/pwKey.ts new file mode 100644 index 0000000..a2f22e7 --- /dev/null +++ b/code/app/src/actions/pwKey.ts @@ -0,0 +1,10 @@ +import { is_development, is_testing } from "$lib/configuration"; + +export default function pwKey(node: HTMLElement, value: string | undefined) { + if (!value) return; + if (!is_testing()) { + if (is_development()) console.warn("VITE_TESTING is false, so not setting pw-key attributes"); + 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..220ddc1 --- /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 @@ + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + + \ 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..d256fea --- /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; + + &.danger { + @apply text-red-600 hover:text-red-700; + } + + &.active { + @apply underline + } +} \ 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 @@ +/// + +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/lib/api/internal-fetch.ts b/code/app/src/lib/api/internal-fetch.ts new file mode 100644 index 0000000..b21d669 --- /dev/null +++ b/code/app/src/lib/api/internal-fetch.ts @@ -0,0 +1,170 @@ +import { Temporal } from "temporal-polyfill"; +import { clear_session_data } from "$lib/session"; +import { resolve_references } from "$lib/helpers"; +import type { IInternalFetchResponse } from "$lib/models/IInternalFetchResponse"; +import type { IInternalFetchRequest } from "$lib/models/IInternalFetchRequest"; +import { redirect } from "@sveltejs/kit"; + +export async function http_post(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise { + const init = { + method: "post", + } as RequestInit; + + if (abort_signal) { + init.signal = abort_signal; + } + + if (body) { + init.headers = { + "Content-Type": "application/json;charset=UTF-8", + }; + init.body = JSON.stringify(body); + } + + const response = await internal_fetch({ url, init, timeout }); + const result = {} as IInternalFetchResponse; + + if (!skip_401_check && await is_401(response)) return result; + + result.ok = response.ok; + result.status = response.status; + result.http_response = response; + + if (response.status !== 204) { + try { + const ct = response.headers.get("Content-Type")?.toString() ?? ""; + if (ct.startsWith("application/json")) { + const data = await response.json(); + result.data = resolve_references(data); + } else if (ct.startsWith("text/plain")) { + const text = await response.text(); + result.data = text as string; + } + } catch { + // Ignored + } + } + + return result; +} + +export async function http_get(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise { + const init = { + method: "get", + } as RequestInit; + + if (abort_signal) { + init.signal = abort_signal; + } + + const response = await internal_fetch({ url, init, timeout }); + const result = {} as IInternalFetchResponse; + + if (!skip_401_check && await is_401(response)) return result; + + result.ok = response.ok; + result.status = response.status; + result.http_response = response; + + if (response.status !== 204) { + try { + const ct = response.headers.get("Content-Type")?.toString() ?? ""; + if (ct.startsWith("application/json")) { + const data = await response.json(); + result.data = resolve_references(data); + } else if (ct.startsWith("text/plain")) { + const text = await response.text(); + result.data = text as string; + } + } catch { + // Ignored + } + } + + return result; +} + +export async function http_delete(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise { + const init = { + method: "delete", + } as RequestInit; + + if (abort_signal) { + init.signal = abort_signal; + } + + if (body) { + init.headers = { + "Content-Type": "application/json;charset=UTF-8", + }; + init.body = JSON.stringify(body); + } + + const response = await internal_fetch({ url, init, timeout }); + const result = {} as IInternalFetchResponse; + + if (!skip_401_check && await is_401(response)) return result; + + result.ok = response.ok; + result.status = response.status; + result.http_response = response; + + if (response.status !== 204) { + try { + const ct = response.headers.get("Content-Type")?.toString() ?? ""; + if (ct.startsWith("application/json")) { + const data = await response.json(); + result.data = resolve_references(data); + } else if (ct.startsWith("text/plain")) { + const text = await response.text(); + result.data = text as string; + } + } catch (error) { + // ignored + } + } + + return result; +} + +async function internal_fetch(request: IInternalFetchRequest): Promise { + if (!request.init) request.init = {}; + request.init.credentials = "include"; + request.init.headers = { + "X-TimeZone": Temporal.Now.timeZone().id, + ...request.init.headers + }; + + 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) { + console.log(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; // rethrow other unexpected errors + } + } + + return response; +} + +async function is_401(response: Response): Promise { + if (response.status === 401) { + clear_session_data(); + throw redirect(307, "/login"); + } + return false; +} diff --git a/code/app/src/lib/api/root.ts b/code/app/src/lib/api/root.ts new file mode 100644 index 0000000..3e5bda2 --- /dev/null +++ b/code/app/src/lib/api/root.ts @@ -0,0 +1,6 @@ +import {http_post} from "$lib/api/internal-fetch"; +import {api_base} from "$lib/configuration"; + +export function server_log(message: string): void { + http_post(api_base("_/api/log"), message); +} diff --git a/code/app/src/lib/api/time-entry.ts b/code/app/src/lib/api/time-entry.ts new file mode 100644 index 0000000..a40b0c2 --- /dev/null +++ b/code/app/src/lib/api/time-entry.ts @@ -0,0 +1,83 @@ +import {api_base} from "$lib/configuration"; +import {is_guid} from "$lib/helpers"; +import {http_delete, http_get, http_post} from "./internal-fetch"; +import type {TimeCategoryDto} from "$lib/models/TimeCategoryDto"; +import type {TimeLabelDto} from "$lib/models/TimeLabelDto"; +import type {TimeEntryDto} from "$lib/models/TimeEntryDto"; +import type {TimeEntryQuery} from "$lib/models/TimeEntryQuery"; +import type {IInternalFetchResponse} from "$lib/models/IInternalFetchResponse"; + + +// ENTRIES + +export async function create_time_entry(payload: TimeEntryDto): Promise { + return http_post(api_base("v1/entries/create"), payload); +} + +export async function get_time_entry(entryId: string): Promise { + if (is_guid(entryId)) { + return http_get(api_base("v1/entries/" + entryId)); + } + throw new Error("entryId is not a valid guid."); +} + +export async function get_time_entries(entryQuery: TimeEntryQuery): Promise { + return http_post(api_base("v1/entries/query"), entryQuery); +} + +export async function delete_time_entry(id: string): Promise { + if (!is_guid(id)) throw new Error("id is not a valid guid"); + return http_delete(api_base("v1/entries/" + id + "/delete")); +} + +export async function update_time_entry(entryDto: TimeEntryDto): Promise { + if (!is_guid(entryDto.id ?? "")) throw new Error("id is not a valid guid"); + if (!entryDto.category) throw new Error("category is empty"); + if (!entryDto.stop) throw new Error("stop is empty"); + if (!entryDto.start) throw new Error("start is empty"); + return http_post(api_base("v1/entries/update"), entryDto); +} + +// LABELS +export async function create_time_label(labelDto: TimeLabelDto): Promise { + return http_post(api_base("v1/labels/create"), labelDto); +} + +export async function get_time_labels(): Promise { + return http_get(api_base("v1/labels")); +} + +export async function delete_time_label(id: string): Promise { + if (!is_guid(id)) throw new Error("id is not a valid guid"); + return http_delete(api_base("v1/labels/" + id + "/delete")); +} + +export async function update_time_label(labelDto: TimeLabelDto): Promise { + if (!is_guid(labelDto.id ?? "")) throw new Error("id is not a valid guid"); + if (!labelDto.name) throw new Error("name is empty"); + if (!labelDto.color) throw new Error("color is empty"); + return http_post(api_base("v1/labels/update"), labelDto); +} + +// CATEGORIES +export async function create_time_category(category: TimeCategoryDto): Promise { + if (!category.name) throw new Error("name is empty"); + if (!category.color) throw new Error("color is empty"); + return http_post(api_base("v1/categories/create"), category); +} + +export async function get_time_categories(): Promise { + return http_get(api_base("v1/categories")); +} + +export async function delete_time_category(id: string): Promise { + if (!is_guid(id)) throw new Error("id is not a valid guid"); + return http_delete(api_base("v1/categories/" + id + "/delete")); +} + +export async function update_time_category(category: TimeCategoryDto): Promise { + if (!is_guid(category.id ?? "")) throw new Error("id is not a valid guid"); + if (!category.name) throw new Error("name is empty"); + if (!category.color) throw new Error("color is empty"); + return http_post(api_base("v1/categories/update"), category); +} diff --git a/code/app/src/lib/api/user.ts b/code/app/src/lib/api/user.ts new file mode 100644 index 0000000..f0dc932 --- /dev/null +++ b/code/app/src/lib/api/user.ts @@ -0,0 +1,47 @@ +import {api_base} from "$lib/configuration"; +import {http_delete, http_get, http_post} from "./internal-fetch"; +import type {LoginPayload} from "$lib/models/LoginPayload"; +import type {UpdateProfilePayload} from "$lib/models/UpdateProfilePayload"; +import type {CreateAccountPayload} from "$lib/models/CreateAccountPayload"; +import type {IInternalFetchResponse} from "$lib/models/IInternalFetchResponse"; + +export async function login(payload: LoginPayload): Promise { + return http_post(api_base("_/account/login"), payload); +} + +export async function logout(): Promise { + return http_get(api_base("_/account/logout")); +} + +export async function create_forgot_password_request(username: string): Promise { + if (!username) throw new Error("Username is empty"); + return http_get(api_base("_/forgot-password-requests/create?username=" + username)); +} + +export async function check_forgot_password_request(public_id: string): Promise { + if (!public_id) throw new Error("Id is empty"); + return http_get(api_base("_/forgot-password-requests/is-valid?id=" + public_id)); +} + +export async function fulfill_forgot_password_request(public_id: string, newPassword: string): Promise { + if (!public_id) throw new Error("Id is empty"); + return http_post(api_base("_/forgot-password-requests/fulfill"), {id: public_id, newPassword}); +} + +export async function delete_account(): Promise { + return http_delete(api_base("_/account/delete")); +} + +export async function update_profile(payload: UpdateProfilePayload): Promise { + if (!payload.password && !payload.username) throw new Error("Password and Username is empty"); + return http_post(api_base("_/account/update"), payload); +} + +export async function create_account(payload: CreateAccountPayload): Promise { + if (!payload.password && !payload.username) throw new Error("Password and Username is empty"); + return http_post(api_base("_/account/create"), payload); +} + +export async function get_profile_for_active_check(): Promise { + return http_get(api_base("_/account"), 0, true); +} diff --git a/code/app/src/lib/colors.ts b/code/app/src/lib/colors.ts new file mode 100644 index 0000000..34c7992 --- /dev/null +++ b/code/app/src/lib/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/lib/components/alert.svelte b/code/app/src/lib/components/alert.svelte new file mode 100644 index 0000000..fd57105 --- /dev/null +++ b/code/app/src/lib/components/alert.svelte @@ -0,0 +1,268 @@ + + +{#if visible} +
+
+
+ +
+
+ {#if !rightLinkText} + {#if title} +

+ {title} +

+ {/if} + {#if message} +
+

+ {@html message} +

+
+ {/if} + {#if listItems?.length ?? 0} +
    + {#each listItems as listItem} +
  • {listItem}
  • + {/each} +
+ {/if} + {:else} +
+
+ {#if title} +

+ {title} +

+ {/if} + {#if message} +
+

+ {@html message} +

+
+ {/if} + {#if listItems?.length ?? 0} +
    + {#each listItems as listItem} +
  • {listItem}
  • + {/each} +
+ {/if} +
+

+ rightLinkClicked()} + class="whitespace-nowrap font-medium text-{colorClassPart}-700 hover:text-{colorClassPart}-600" + > + {rightLinkText} + + +

+
+ {/if} + {#if actions?.length ?? 0} +
+
+ {#each actions as action} + {@const color = action?.color ?? colorClassPart} + + {/each} +
+
+ {/if} +
+ {#if closeable} +
+
+ +
+
+ {/if} +
+
+{/if} diff --git a/code/app/src/lib/components/button.svelte b/code/app/src/lib/components/button.svelte new file mode 100644 index 0000000..cbc09e2 --- /dev/null +++ b/code/app/src/lib/components/button.svelte @@ -0,0 +1,103 @@ + + + + +{#if href} + + {#if loading} + + {/if} + {text} + +{:else} + +{/if} diff --git a/code/app/src/lib/components/checkbox.svelte b/code/app/src/lib/components/checkbox.svelte new file mode 100644 index 0000000..b2fcddb --- /dev/null +++ b/code/app/src/lib/components/checkbox.svelte @@ -0,0 +1,24 @@ + + +
+ + +
diff --git a/code/app/src/lib/components/icons/adjustments.svelte b/code/app/src/lib/components/icons/adjustments.svelte new file mode 100644 index 0000000..83bda27 --- /dev/null +++ b/code/app/src/lib/components/icons/adjustments.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/bars-3-center-left.svelte b/code/app/src/lib/components/icons/bars-3-center-left.svelte new file mode 100644 index 0000000..785ece3 --- /dev/null +++ b/code/app/src/lib/components/icons/bars-3-center-left.svelte @@ -0,0 +1,15 @@ + diff --git a/code/app/src/lib/components/icons/calendar.svelte b/code/app/src/lib/components/icons/calendar.svelte new file mode 100644 index 0000000..e0053ee --- /dev/null +++ b/code/app/src/lib/components/icons/calendar.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/check-circle.svelte b/code/app/src/lib/components/icons/check-circle.svelte new file mode 100644 index 0000000..e30778e --- /dev/null +++ b/code/app/src/lib/components/icons/check-circle.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/chevron-up-down.svelte b/code/app/src/lib/components/icons/chevron-up-down.svelte new file mode 100644 index 0000000..c07aed5 --- /dev/null +++ b/code/app/src/lib/components/icons/chevron-up-down.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/database.svelte b/code/app/src/lib/components/icons/database.svelte new file mode 100644 index 0000000..6ffdadb --- /dev/null +++ b/code/app/src/lib/components/icons/database.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/exclamation-circle.svelte b/code/app/src/lib/components/icons/exclamation-circle.svelte new file mode 100644 index 0000000..2ce79b1 --- /dev/null +++ b/code/app/src/lib/components/icons/exclamation-circle.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/exclamation-triangle.svelte b/code/app/src/lib/components/icons/exclamation-triangle.svelte new file mode 100644 index 0000000..8d807db --- /dev/null +++ b/code/app/src/lib/components/icons/exclamation-triangle.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/folder-open.svelte b/code/app/src/lib/components/icons/folder-open.svelte new file mode 100644 index 0000000..409c8e2 --- /dev/null +++ b/code/app/src/lib/components/icons/folder-open.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/home.svelte b/code/app/src/lib/components/icons/home.svelte new file mode 100644 index 0000000..ee8305d --- /dev/null +++ b/code/app/src/lib/components/icons/home.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/index.ts b/code/app/src/lib/components/icons/index.ts new file mode 100644 index 0000000..8c24873 --- /dev/null +++ b/code/app/src/lib/components/icons/index.ts @@ -0,0 +1,41 @@ +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"; + +export { + 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/lib/components/icons/information-circle.svelte b/code/app/src/lib/components/icons/information-circle.svelte new file mode 100644 index 0000000..68dbc60 --- /dev/null +++ b/code/app/src/lib/components/icons/information-circle.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/magnifying-glass.svelte b/code/app/src/lib/components/icons/magnifying-glass.svelte new file mode 100644 index 0000000..f8fdb6e --- /dev/null +++ b/code/app/src/lib/components/icons/magnifying-glass.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/megaphone.svelte b/code/app/src/lib/components/icons/megaphone.svelte new file mode 100644 index 0000000..7ada5f3 --- /dev/null +++ b/code/app/src/lib/components/icons/megaphone.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/menu.svelte b/code/app/src/lib/components/icons/menu.svelte new file mode 100644 index 0000000..471d85f --- /dev/null +++ b/code/app/src/lib/components/icons/menu.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/queue-list.svelte b/code/app/src/lib/components/icons/queue-list.svelte new file mode 100644 index 0000000..6148394 --- /dev/null +++ b/code/app/src/lib/components/icons/queue-list.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/icons/spinner.svelte b/code/app/src/lib/components/icons/spinner.svelte new file mode 100644 index 0000000..80cc57c --- /dev/null +++ b/code/app/src/lib/components/icons/spinner.svelte @@ -0,0 +1,20 @@ + + + + diff --git a/code/app/src/lib/components/icons/x-circle.svelte b/code/app/src/lib/components/icons/x-circle.svelte new file mode 100644 index 0000000..3793b5a --- /dev/null +++ b/code/app/src/lib/components/icons/x-circle.svelte @@ -0,0 +1,13 @@ + diff --git a/code/app/src/lib/components/icons/x-mark.svelte b/code/app/src/lib/components/icons/x-mark.svelte new file mode 100644 index 0000000..fd1c6a1 --- /dev/null +++ b/code/app/src/lib/components/icons/x-mark.svelte @@ -0,0 +1,11 @@ + diff --git a/code/app/src/lib/components/icons/x.svelte b/code/app/src/lib/components/icons/x.svelte new file mode 100644 index 0000000..6125ab8 --- /dev/null +++ b/code/app/src/lib/components/icons/x.svelte @@ -0,0 +1,14 @@ + + + diff --git a/code/app/src/lib/components/index.ts b/code/app/src/lib/components/index.ts new file mode 100644 index 0000000..a81e0c3 --- /dev/null +++ b/code/app/src/lib/components/index.ts @@ -0,0 +1,15 @@ +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"; + +export { + Alert, + Button, + Checkbox, + Input, + LocaleSwitcher, + Switch +} \ No newline at end of file diff --git a/code/app/src/lib/components/input.svelte b/code/app/src/lib/components/input.svelte new file mode 100644 index 0000000..c0ed654 --- /dev/null +++ b/code/app/src/lib/components/input.svelte @@ -0,0 +1,103 @@ + + +
+ {#if label && !cornerHint && !hideLabel} + + {:else if cornerHint && !hideLabel} +
+ {#if label} + + {/if} + + {cornerHint} + +
+ {/if} +
+ {#if icon} +
+ +
+ {:else if addon} +
+ {addon} +
+ {/if} + + {#if errorText} +
+ +
+ {/if} +
+ {#if helpText && !errorText} +

+ {helpText} +

+ {/if} + {#if errorText} +

+ {errorText} +

+ {/if} +
diff --git a/code/app/src/lib/components/locale-switcher.svelte b/code/app/src/lib/components/locale-switcher.svelte new file mode 100644 index 0000000..f880bfb --- /dev/null +++ b/code/app/src/lib/components/locale-switcher.svelte @@ -0,0 +1,55 @@ + + + diff --git a/code/app/src/lib/components/switch.svelte b/code/app/src/lib/components/switch.svelte new file mode 100644 index 0000000..16da23a --- /dev/null +++ b/code/app/src/lib/components/switch.svelte @@ -0,0 +1,143 @@ + + + + +
+ {#if hasLabelOrDescription && !rightAlignedLabelDescription} + + {#if label} + {label} + {/if} + {#if description} + {description} + {/if} + + {/if} + {#if type === "short"} + + {:else if type === "icon"} + + {:else if type === "default"} + + {/if} + {#if hasLabelOrDescription && rightAlignedLabelDescription} + + {#if label} + {label} + {/if} + {#if description} + {description} + {/if} + + {/if} +
diff --git a/code/app/src/lib/configuration.ts b/code/app/src/lib/configuration.ts new file mode 100644 index 0000000..5a6a1bf --- /dev/null +++ b/code/app/src/lib/configuration.ts @@ -0,0 +1,60 @@ +export const BASE_DOMAIN = "dev.greatoffice.app"; +export const DEV_BASE_DOMAIN = "http://localhost"; +export const API_ADDRESS = "https://api." + BASE_DOMAIN; +export const DEV_API_ADDRESS = "http://localhost:5000"; +export const SECONDS_BETWEEN_SESSION_CHECK = 600; + +export function api_base(path: string = ""): string { + 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 import.meta.env.VITE_TESTING; +} + +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: import.meta.env.VITE_TEST_USERNAME, + password: import.meta.env.VITE_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" +}; \ No newline at end of file diff --git a/code/app/src/lib/helpers.ts b/code/app/src/lib/helpers.ts new file mode 100644 index 0000000..3fa1653 --- /dev/null +++ b/code/app/src/lib/helpers.ts @@ -0,0 +1,497 @@ +import { browser } from "$app/environment"; +import type { TimeEntryDto } from "$lib/models/TimeEntryDto"; +import type { UnwrappedEntryDateTime } from "$lib/models/UnwrappedEntryDateTime"; +import { logInfo } from "$lib/logger"; +import { Temporal } from "temporal-polyfill"; + +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 get_default_sorted(unsorted: Array): Array { + if (unsorted.length < 1) return unsorted; + const byStart = unsorted.sort((a, b) => { + return Temporal.Instant.compare(Temporal.Instant.from(b.start), Temporal.Instant.from(a.start)); + }); + + return byStart.sort((a, b) => { + return Temporal.Instant.compare(Temporal.Instant.from(b.stop), Temporal.Instant.from(a.stop)); + }); +} + +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 + "']"; +} + +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 get_cookie(name: string) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop()?.split(";").shift(); +} + +export function set_cookie(name: string, value: string, baseDomain = window.location.hostname) { + document.cookie = name + "=" + encodeURIComponent(value) + (baseDomain ? ";domain=" + baseDomain : ""); +} + +export function unwrap_date_time_from_entry(entry: TimeEntryDto): UnwrappedEntryDateTime { + if (!entry) throw new Error("entry was undefined"); + const currentTimeZone = Temporal.Now.timeZone().id; + const startInstant = Temporal.Instant.from(entry.start).toZonedDateTimeISO(currentTimeZone); + const stopInstant = Temporal.Instant.from(entry.stop).toZonedDateTimeISO(currentTimeZone); + + return { + start_date: startInstant.toPlainDate(), + stop_date: stopInstant.toPlainDate(), + start_time: startInstant.toPlainTime(), + stop_time: stopInstant.toPlainTime(), + duration: Temporal.Duration.from({ + hours: stopInstant.hour, + minutes: stopInstant.minute, + }).subtract(Temporal.Duration.from({ + hours: startInstant.hour, + minutes: startInstant.minute, + })), + }; +} + + +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 { + return obj !== void 0 && Object.keys(obj).length > 0; +} + +export function merge_obj_arr(a: Array, b: Array, props: Array): Array { + 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 set_favicon(url: string) { + // Find the current favicon element + const favicon = document.querySelector("link[rel=\"icon\"]") as HTMLLinkElement; + if (favicon) { + // Update the new link + favicon.href = url; + } else { + // Create new `link` + const link = document.createElement("link"); + link.rel = "icon"; + link.href = url; + + // Append to the `head` element + document.head.appendChild(link); + } +} +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 set_emoji_favicon(emoji: string) { + // Create a canvas element + const canvas = document.createElement("canvas"); + canvas.height = 64; + canvas.width = 64; + + // Get the canvas context + const context = canvas.getContext("2d") as CanvasRenderingContext2D; + context.font = "64px serif"; + context.fillText(emoji, 0, 64); + + // Get the custom URL + const url = canvas.toDataURL(); + + // Update the favicon + set_favicon(url); +} + + +// https://stackoverflow.com/a/48400665/11961742 +export function seconds_to_hour_minute_string(seconds: number, hourChar = "h", minuteChar = "m") { + const hours = Math.floor(seconds / (60 * 60)); + seconds -= hours * (60 * 60); + const minutes = Math.floor(seconds / 60); + return hours + "h" + minutes + "m"; +} + +export function seconds_to_hour_minute(seconds: number) { + const hours = Math.floor(seconds / (60 * 60)); + seconds -= hours * (60 * 60); + const minutes = Math.floor(seconds / 60); + return { hours, minutes }; +} + +export function get_query_string(params: any = {}): string { + const map = Object.keys(params).reduce((arr: Array, 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 async function run_async(functionToRun: Function): Promise { + return new Promise((greatSuccess, graveFailure) => { + try { + greatSuccess(functionToRun()); + } catch (exception) { + graveFailure(exception); + } + }); +} + +// https://stackoverflow.com/a/45215694/11961742 +export function get_selected_options(domElement: HTMLSelectElement): Array { + 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 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; +} + +interface CreateElementOptions { + name: string, + properties?: object, + children?: Array +} + +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 { + 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; +} + +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 = [], mergeSpecialsWithDefaults: boolean = false): void { + if (element) { + 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(); + } +} + +export function get_url_parameter(name: string): string { + // @ts-ignore + return new RegExp("[?&]" + name + "=([^&#]*)")?.exec(window.location.href)[1]; +} + +export function update_url_parameter(param: string, newVal: string): void { + let newAdditionalURL = ""; + let tempArray = location.href.split("?"); + const baseURL = tempArray[0]; + const additionalURL = tempArray[1]; + let temp = ""; + if (additionalURL) { + tempArray = additionalURL.split("&"); + for (let i = 0; i < tempArray.length; i++) { + if (tempArray[i].split("=")[0] !== param) { + newAdditionalURL += temp + tempArray[i]; + temp = "&"; + } + } + } + const rows_txt = temp + "" + param + "=" + newVal; + const newUrl = baseURL + "?" + newAdditionalURL + rows_txt; + window.history.replaceState("", "", newUrl); +} + + +export function get_style_string(rules: CSSRuleList) { + let styleString = ""; + for (const [key, value] of Object.entries(rules)) { + styleString += key + ":" + value + ";"; + } + return styleString; +} + +export function parse_iso_local(s: string) { + const b = s.split(/\D/); + //@ts-ignore + return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); +} + +export function resolve_references(json: any) { + if (!json) return; + if (typeof json === "string") { + json = JSON.parse(json ?? "{}"); + } + const byid = {}, refs = []; + json = function recurse(obj, prop, parent) { + if (typeof obj !== "object" || !obj) { + return obj; + } + if (Object.prototype.toString.call(obj) === "[object Array]") { + for (let i = 0; i < obj.length; i++) { + if (typeof obj[i] !== "object" || !obj[i]) { + continue; + } else if ("$ref" in obj[i]) { + // @ts-ignore + obj[i] = recurse(obj[i], i, obj); + } else { + obj[i] = recurse(obj[i], prop, obj); + } + } + return obj; + } + if ("$ref" in obj) { + let ref = obj.$ref; + if (ref in byid) { + // @ts-ignore + return byid[ref]; + } + refs.push([parent, prop, ref]); + return; + } else if ("$id" in obj) { + let id = obj.$id; + delete obj.$id; + if ("$values" in obj) { + obj = obj.$values.map(recurse); + } else { + for (let prop2 in obj) { + // @ts-ignore + obj[prop2] = recurse(obj[prop2], prop2, obj); + } + } + // @ts-ignore + byid[id] = obj; + } + return obj; + }(json); + for (let i = 0; i < refs.length; i++) { + let ref = refs[i]; + // @ts-ignore + ref[0][ref[1]] = byid[ref[2]]; + } + return json; +} + +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 to_readable_bytes(bytes: number): string { + const s = ["bytes", "kB", "MB", "GB", "TB", "PB"]; + const e = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e]; +} + +export function can_use_dom(): boolean { + return !!(typeof window !== "undefined" && window.document && window.document.createElement); +} + +export function session_storage_remove_regex(regex: RegExp): void { + if (!browser) { + logInfo("sessionStorage is not available in non-browser contexts"); + return; + } + let n = sessionStorage.length; + while (n--) { + const key = sessionStorage.key(n); + if (key && regex.test(key)) { + sessionStorage.removeItem(key); + } + } +} + +export function local_storage_remove_regex(regex: RegExp): void { + if (!browser) { + logInfo("sessionStorage is not available in non-browser contexts"); + return; + } + let n = localStorage.length; + while (n--) { + const key = localStorage.key(n); + if (key && regex.test(key)) { + localStorage.removeItem(key); + } + } +} + +export function session_storage_set_json(key: string, value: object): void { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return; + } + sessionStorage.setItem(key, JSON.stringify(value)); +} + +export function session_storage_get_json(key: string): object { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return {}; + } + return JSON.parse(sessionStorage.getItem(key) ?? "{}"); +} + +export function local_storage_set_json(key: string, value: object): void { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return; + } + localStorage.setItem(key, JSON.stringify(value)); +} + +export function local_storage_get_json(key: string): object { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return {}; + } + return JSON.parse(localStorage.getItem(key) ?? "{}"); +} + +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/lib/i18n/en/app/index.ts b/code/app/src/lib/i18n/en/app/index.ts new file mode 100644 index 0000000..7cd05ee --- /dev/null +++ b/code/app/src/lib/i18n/en/app/index.ts @@ -0,0 +1,5 @@ +import type { BaseTranslation } from '../../i18n-types' + +const en_app: BaseTranslation = {} + +export default en_app \ No newline at end of file diff --git a/code/app/src/lib/i18n/en/index.ts b/code/app/src/lib/i18n/en/index.ts new file mode 100644 index 0000000..e084a6c --- /dev/null +++ b/code/app/src/lib/i18n/en/index.ts @@ -0,0 +1,50 @@ +import type { BaseTranslation } from "../i18n-types"; + +const en: BaseTranslation = { + or: "Or", + 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", + signInPage: { + 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: { + createYourNewAccount: "Create your new account", + }, + resetPasswordPage: { + setANewPassword: "Set a new password", + expired: "Expired", + requestHasExpired: "Your request has expired", + requestANewReset: "Request a new reset", + 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/lib/i18n/formatters.ts b/code/app/src/lib/i18n/formatters.ts new file mode 100644 index 0000000..5232b7d --- /dev/null +++ b/code/app/src/lib/i18n/formatters.ts @@ -0,0 +1,13 @@ +import { capitalise } from '$lib/helpers' +import type { FormattersInitializer } from 'typesafe-i18n' +import type { Locales, Formatters } from './i18n-types' + +export const initFormatters: FormattersInitializer = (locale: Locales) => { + + const formatters: Formatters = { + // add your formatter functions here + capitalise: (value: string) => capitalise(value) + } + + return formatters +} diff --git a/code/app/src/lib/i18n/i18n-svelte.ts b/code/app/src/lib/i18n/i18n-svelte.ts new file mode 100644 index 0000000..6cdffb3 --- /dev/null +++ b/code/app/src/lib/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(loadedLocales, loadedFormatters) + +export { locale, LL, setLocale } + +export default LL diff --git a/code/app/src/lib/i18n/i18n-types.ts b/code/app/src/lib/i18n/i18n-types.ts new file mode 100644 index 0000000..0df6d1a --- /dev/null +++ b/code/app/src/lib/i18n/i18n-types.ts @@ -0,0 +1,359 @@ +// 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 + /** + * 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 + signInPage: { + /** + * 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: { + /** + * C​r​e​a​t​e​ ​y​o​u​r​ ​n​e​w​ ​a​c​c​o​u​n​t + */ + createYourNewAccount: string + } + resetPasswordPage: { + /** + * 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 + /** + * 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 = {} + +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 + /** + * 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 + signInPage: { + /** + * 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: { + /** + * Create your new account + */ + createYourNewAccount: () => LocalizedString + } + resetPasswordPage: { + /** + * Set a new password + */ + setANewPassword: () => LocalizedString + /** + * Expired + */ + expired: () => LocalizedString + /** + * Your request has expired + */ + requestHasExpired: () => LocalizedString + /** + * Request a new reset + */ + requestANewReset: () => 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: { + } +} + +export type Formatters = {} diff --git a/code/app/src/lib/i18n/i18n-util.async.ts b/code/app/src/lib/i18n/i18n-util.async.ts new file mode 100644 index 0000000..00b8e0a --- /dev/null +++ b/code/app/src/lib/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) => + loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + +export const importLocaleAsync = async (locale: Locales) => + (await localeTranslationLoaders[locale]()).default as unknown as Translations + +export const loadLocaleAsync = async (locale: Locales): Promise => { + updateDictionary(locale, await importLocaleAsync(locale)) + loadFormatters(locale) +} + +export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) + +export const importNamespaceAsync = async(locale: Locales, namespace: Namespace) => + (await localeNamespaceLoaders[locale][namespace]()).default as unknown as Translations[Namespace] + +export const loadNamespaceAsync = async (locale: Locales, namespace: Namespace): Promise => + void updateDictionary(locale, { [namespace]: await importNamespaceAsync(locale, namespace )}) diff --git a/code/app/src/lib/i18n/i18n-util.sync.ts b/code/app/src/lib/i18n/i18n-util.sync.ts new file mode 100644 index 0000000..8144fdc --- /dev/null +++ b/code/app/src/lib/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/lib/i18n/i18n-util.ts b/code/app/src/lib/i18n/i18n-util.ts new file mode 100644 index 0000000..35f023c --- /dev/null +++ b/code/app/src/lib/i18n/i18n-util.ts @@ -0,0 +1,39 @@ +// 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 { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' +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) => locales.includes(locale as Locales) + +export const isNamespace = (namespace: string) => namespaces.includes(namespace as Namespaces) + +export const loadedLocales = {} as Record + +export const loadedFormatters = {} as Record + +export const i18nString = (locale: Locales) => initI18nString(locale, loadedFormatters[locale]) + +export const i18nObject = (locale: Locales) => + initI18nObject( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ) + +export const i18n = () => initI18n(loadedLocales, loadedFormatters) + +export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn(baseLocale, locales, ...detectors) diff --git a/code/app/src/lib/i18n/nb/app/index.ts b/code/app/src/lib/i18n/nb/app/index.ts new file mode 100644 index 0000000..15d0b9a --- /dev/null +++ b/code/app/src/lib/i18n/nb/app/index.ts @@ -0,0 +1,8 @@ +import type { NamespaceAppTranslation } from '../../i18n-types' + +const nb_app: NamespaceAppTranslation = { + // TODO: insert translations + +} + +export default nb_app diff --git a/code/app/src/lib/i18n/nb/index.ts b/code/app/src/lib/i18n/nb/index.ts new file mode 100644 index 0000000..fa81477 --- /dev/null +++ b/code/app/src/lib/i18n/nb/index.ts @@ -0,0 +1,50 @@ +import type { Translation } from "../i18n-types"; + +const nb: Translation = { + or: "Eller", + 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/lib/logger.ts b/code/app/src/lib/logger.ts new file mode 100644 index 0000000..df0a821 --- /dev/null +++ b/code/app/src/lib/logger.ts @@ -0,0 +1,86 @@ +import { browser, dev } from "$app/environment"; +import { StorageKeys } from "$lib/configuration"; +import pino from "pino"; + +const pinoConfig = dev ? { + transport: { + target: "pino-pretty", + } +} : {}; + +const pinoLogger = pino(pinoConfig); + +function browserLogLevel(): number { + if (browser) return LogLevel.toNumber(sessionStorage.getItem(StorageKeys.logLevel), LogLevel.INFO); + throw new Error("Called browser api in server"); +} + +function serverLogLevel(): number { + if (!browser) return LogLevel.toNumber(import.meta.env.VITE_LOG_LEVEL, LogLevel.ERROR); + throw new Error("Called server api in browser"); +} + +export const LogLevel = { + DEBUG: 0, + INFO: 1, + ERROR: 2, + SILENT: 3, + toString(levelInt: number): string { + switch (levelInt) { + case 0: + return "DEBUG"; + case 1: + return "INFO"; + case 2: + return "ERROR"; + case 3: + return "SILENT"; + default: + throw new Error("Log level int is unknown"); + } + }, + toNumber(levelString?: string | null, fallback?: number): number { + if (!levelString && fallback) return fallback; + else if (!levelString && !fallback) throw new Error("levelString was empty, and no fallback was specified"); + switch (levelString?.toUpperCase()) { + case "DEBUG": + return 0; + case "INFO": + return 1; + case "ERROR": + return 2; + case "SILENT": + return 3; + default: + if (!fallback) throw new Error("Log level string is unknown"); + else return fallback; + } + }, +}; + +export function logDebug(message: string, ...additional: any[]): void { + if (browser && browserLogLevel() <= LogLevel.DEBUG) { + pinoLogger.debug(message, additional); + } + if (!browser && serverLogLevel() <= LogLevel.DEBUG) { + pinoLogger.debug(message, additional); + } +} + +export function logInfo(message: string, ...additional: any[]): void { + if (browser && browserLogLevel() <= LogLevel.INFO) { + pinoLogger.info(message, additional); + } + if (!browser && serverLogLevel() <= LogLevel.INFO) { + pinoLogger.info(message, additional); + } +} + +export function logError(message: any, ...additional: any[]): void { + if (browser && browserLogLevel() <= LogLevel.ERROR) { + pinoLogger.error(message, additional); + } + if (!browser && serverLogLevel() <= LogLevel.ERROR) { + pinoLogger.error(message, additional); + } +} \ No newline at end of file diff --git a/code/app/src/lib/models/CreateAccountPayload.ts b/code/app/src/lib/models/CreateAccountPayload.ts new file mode 100644 index 0000000..d116308 --- /dev/null +++ b/code/app/src/lib/models/CreateAccountPayload.ts @@ -0,0 +1,4 @@ +export interface CreateAccountPayload { + username: string, + password: string +} diff --git a/code/app/src/lib/models/ErrorResult.ts b/code/app/src/lib/models/ErrorResult.ts new file mode 100644 index 0000000..7c70017 --- /dev/null +++ b/code/app/src/lib/models/ErrorResult.ts @@ -0,0 +1,4 @@ +export interface ErrorResult { + title: string, + text: string +} diff --git a/code/app/src/lib/models/IInternalFetchRequest.ts b/code/app/src/lib/models/IInternalFetchRequest.ts new file mode 100644 index 0000000..68505e2 --- /dev/null +++ b/code/app/src/lib/models/IInternalFetchRequest.ts @@ -0,0 +1,6 @@ +export interface IInternalFetchRequest { + url: string, + init?: RequestInit, + timeout?: number + retry_count?: number +} diff --git a/code/app/src/lib/models/IInternalFetchResponse.ts b/code/app/src/lib/models/IInternalFetchResponse.ts new file mode 100644 index 0000000..6c91b35 --- /dev/null +++ b/code/app/src/lib/models/IInternalFetchResponse.ts @@ -0,0 +1,6 @@ +export interface IInternalFetchResponse { + ok: boolean, + status: number, + data: any, + http_response: Response +} diff --git a/code/app/src/lib/models/ISession.ts b/code/app/src/lib/models/ISession.ts new file mode 100644 index 0000000..7587145 --- /dev/null +++ b/code/app/src/lib/models/ISession.ts @@ -0,0 +1,8 @@ +export interface ISession { + profile: { + username: string, + displayName: string, + id: string, + }, + lastChecked: number, +} \ No newline at end of file diff --git a/code/app/src/lib/models/IValidationResult.ts b/code/app/src/lib/models/IValidationResult.ts new file mode 100644 index 0000000..9a21b13 --- /dev/null +++ b/code/app/src/lib/models/IValidationResult.ts @@ -0,0 +1,31 @@ +export interface IValidationResult { + errors: Array, + has_errors: Function, + add_error: Function, + remove_error: Function, +} + +export interface IValidationError { + _id?: string, + title: string, + text?: string +} + +export default class ValidationResult implements IValidationResult { + errors: IValidationError[] + has_errors(): boolean { + return this.errors?.length > 0; + } + add_error(prop: string, error: IValidationError): void { + if (!this.errors) this.errors = []; + error._id = prop; + this.errors.push(error); + } + remove_error(property: string): void { + const new_errors = []; + for (const error of this.errors) { + if (error._id != property) new_errors.push(error) + } + this.errors = new_errors; + } +} diff --git a/code/app/src/lib/models/LoginPayload.ts b/code/app/src/lib/models/LoginPayload.ts new file mode 100644 index 0000000..beb96cf --- /dev/null +++ b/code/app/src/lib/models/LoginPayload.ts @@ -0,0 +1,5 @@ +export interface LoginPayload { + username: string, + password: string, + persist: boolean +} diff --git a/code/app/src/lib/models/TimeCategoryDto.ts b/code/app/src/lib/models/TimeCategoryDto.ts new file mode 100644 index 0000000..fcdb7ea --- /dev/null +++ b/code/app/src/lib/models/TimeCategoryDto.ts @@ -0,0 +1,9 @@ +import { Temporal } from "temporal-polyfill"; + +export interface TimeCategoryDto { + selected?: boolean; + id?: string, + modified_at?: Temporal.PlainDate, + name?: string, + color?: string +} diff --git a/code/app/src/lib/models/TimeEntryDto.ts b/code/app/src/lib/models/TimeEntryDto.ts new file mode 100644 index 0000000..571c52e --- /dev/null +++ b/code/app/src/lib/models/TimeEntryDto.ts @@ -0,0 +1,13 @@ +import type { TimeLabelDto } from "./TimeLabelDto"; +import type { TimeCategoryDto } from "./TimeCategoryDto"; +import { Temporal } from "temporal-polyfill"; + +export interface TimeEntryDto { + id: string, + modified_at?: Temporal.PlainDate, + start: string, + stop: string, + description: string, + labels?: Array, + category: TimeCategoryDto, +} diff --git a/code/app/src/lib/models/TimeEntryQuery.ts b/code/app/src/lib/models/TimeEntryQuery.ts new file mode 100644 index 0000000..d983d1a --- /dev/null +++ b/code/app/src/lib/models/TimeEntryQuery.ts @@ -0,0 +1,27 @@ +import type { TimeCategoryDto } from "./TimeCategoryDto"; +import type { TimeLabelDto } from "./TimeLabelDto"; +import type { Temporal } from "temporal-polyfill"; + +export interface TimeEntryQuery { + duration: TimeEntryQueryDuration, + categories?: Array, + labels?: Array, + dateRange?: TimeEntryQueryDateRange, + specificDate?: Temporal.PlainDateTime + page: number, + pageSize: number +} + +export interface TimeEntryQueryDateRange { + from: Temporal.PlainDateTime, + to: Temporal.PlainDateTime +} + +export enum TimeEntryQueryDuration { + TODAY = 0, + THIS_WEEK = 1, + THIS_MONTH = 2, + THIS_YEAR = 3, + SPECIFIC_DATE = 4, + DATE_RANGE = 5, +} diff --git a/code/app/src/lib/models/TimeLabelDto.ts b/code/app/src/lib/models/TimeLabelDto.ts new file mode 100644 index 0000000..7183bcf --- /dev/null +++ b/code/app/src/lib/models/TimeLabelDto.ts @@ -0,0 +1,8 @@ +import { Temporal } from "temporal-polyfill"; + +export interface TimeLabelDto { + id?: string, + modified_at?: Temporal.PlainDate, + name?: string, + color?: string +} diff --git a/code/app/src/lib/models/TimeQueryDto.ts b/code/app/src/lib/models/TimeQueryDto.ts new file mode 100644 index 0000000..607c51e --- /dev/null +++ b/code/app/src/lib/models/TimeQueryDto.ts @@ -0,0 +1,29 @@ +import type { TimeEntryDto } from "./TimeEntryDto"; +import ValidationResult, { IValidationResult } from "./IValidationResult"; + +export interface ITimeQueryDto { + results: Array, + page: number, + pageSize: number, + totalRecords: number, + totalPageCount: number, + is_valid: Function +} + +export class TimeQueryDto implements ITimeQueryDto { + results: TimeEntryDto[]; + page: number; + pageSize: number; + totalRecords: number; + totalPageCount: number; + + is_valid(): IValidationResult { + const result = new ValidationResult(); + if (this.page < 0) { + result.add_error("page", { + title: "Page cannot be less than zero", + }) + } + return result; + } +} diff --git a/code/app/src/lib/models/UnwrappedEntryDateTime.ts b/code/app/src/lib/models/UnwrappedEntryDateTime.ts new file mode 100644 index 0000000..d614f91 --- /dev/null +++ b/code/app/src/lib/models/UnwrappedEntryDateTime.ts @@ -0,0 +1,9 @@ +import { Temporal } from "temporal-polyfill"; + +export interface UnwrappedEntryDateTime { + start_date: Temporal.PlainDate, + stop_date: Temporal.PlainDate, + start_time: Temporal.PlainTime, + stop_time: Temporal.PlainTime, + duration: Temporal.Duration, +} diff --git a/code/app/src/lib/models/UpdateProfilePayload.ts b/code/app/src/lib/models/UpdateProfilePayload.ts new file mode 100644 index 0000000..d2983ff --- /dev/null +++ b/code/app/src/lib/models/UpdateProfilePayload.ts @@ -0,0 +1,4 @@ +export interface UpdateProfilePayload { + username?: string, + password?: string, +} diff --git a/code/app/src/lib/persistent-store.ts b/code/app/src/lib/persistent-store.ts new file mode 100644 index 0000000..922f3ab --- /dev/null +++ b/code/app/src/lib/persistent-store.ts @@ -0,0 +1,102 @@ +import { writable as _writable, readable as _readable, } from "svelte/store"; +import type { Writable, Readable, StartStopNotifier } from "svelte/store"; + +enum StoreType { + SESSION = 0, + LOCAL = 1 +} + +interface StoreOptions { + store?: StoreType; +} + +const default_store_options = { + store: StoreType.SESSION +} as StoreOptions; + +interface WritableStore { + name: string, + initialState: T, + options?: StoreOptions +} + +interface ReadableStore { + name: string, + initialState: T, + callback: StartStopNotifier, + options?: StoreOptions +} + +function get_store(type: StoreType): Storage { + 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(options: WritableStore | ReadableStore): any { + try { + const storage = get_store(options.options.store); + const value = storage.getItem(options.name); + if (!value) return false; + return JSON.parse(value); + } catch (e) { + console.error(e); + return { __INVALID__: true }; + } +} + +function hydrate(store: Writable, options: WritableStore | ReadableStore): void { + const value = get_store_value(options); + if (value && store.set) store.set(value); +} + +function subscribe(store: Writable | Readable, options: WritableStore | ReadableStore): void { + const storage = get_store(options.options.store); + if (!store.subscribe) return; + store.subscribe((state: any) => { + storage.setItem(options.name, prepared_store_value(state)); + }); +} + +function writable_persistent(options: WritableStore): Writable { + if (options.options === undefined) options.options = default_store_options; + console.log("Creating writable store with options: ", options); + const store = _writable(options.initialState); + hydrate(store, options); + subscribe(store, options); + return store; +} + +function readable_persistent(options: ReadableStore): Readable { + if (options.options === undefined) options.options = default_store_options; + console.log("Creating readable store with options: ", options); + const store = _readable(options.initialState, options.callback); + // hydrate(store, options); + subscribe(store, options); + return store; +} + +export { + writable_persistent, + readable_persistent, + StoreType +}; + +export type { + WritableStore, + ReadableStore, + StoreOptions +}; + diff --git a/code/app/src/lib/session.ts b/code/app/src/lib/session.ts new file mode 100644 index 0000000..ee79933 --- /dev/null +++ b/code/app/src/lib/session.ts @@ -0,0 +1,69 @@ +import {logError, logInfo} from "$lib/logger"; +import { Temporal } from "temporal-polyfill"; +import { get_profile_for_active_check, logout } from "./api/user"; +import { is_guid, session_storage_get_json, session_storage_set_json } from "./helpers"; +import { SECONDS_BETWEEN_SESSION_CHECK, StorageKeys } from "./configuration"; +import type { ISession } from "$lib/models/ISession"; + +export async function is_active(forceRefresh: boolean = false): Promise { + const nowEpoch = Temporal.Now.instant().epochSeconds; + const data = session_storage_get_json(StorageKeys.session) as ISession; + const expiryEpoch = data?.lastChecked + SECONDS_BETWEEN_SESSION_CHECK; + const lastCheckIsStaleOrNone = !is_guid(data?.profile?.id) || (expiryEpoch < nowEpoch); + if (forceRefresh || lastCheckIsStaleOrNone) { + return await call_api(); + } else { + const sessionIsValid = data.profile && is_guid(data.profile.id); + if (!sessionIsValid) { + clear_session_data(); + logInfo("Session data is not valid"); + } + return sessionIsValid; + } +} + +export async function end_session(cb: Function): Promise { + await logout(); + clear_session_data(); + cb(); +} + +async function call_api(): Promise { + logInfo("Getting profile data while checking session state"); + try { + const response = await get_profile_for_active_check(); + if (response.ok) { + const userData = await response.data; + if (is_guid(userData.id) && userData.username) { + const session = { + profile: userData, + lastChecked: Temporal.Now.instant().epochSeconds + } as ISession; + session_storage_set_json(StorageKeys.session, session); + logInfo("Successfully got profile data while checking session state"); + return true; + } else { + logError("Api returned invalid data while getting profile data"); + clear_session_data(); + return false; + } + } else { + logError("Api returned unsuccessfully while getting profile data"); + clear_session_data(); + return false; + } + } catch (e) { + logError(e); + clear_session_data(); + return false; + } +} + +export function clear_session_data() { + session_storage_set_json(StorageKeys.session, {}); + logInfo("Cleared session data."); +} + +export function get_session_data(): ISession { + return session_storage_get_json(StorageKeys.session) as ISession; +} 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..0be6ff3 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/+layout.svelte @@ -0,0 +1,297 @@ + + +
+ + + (sidebarOpen = false)}> + +
+ + +
+ + + +
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ + Open user menu + +
+ + +
+ + View profile + + + Settings + +
+ + sign_out()} class="text-gray-700 block px-4 py-2 text-sm"> Sign out + +
+
+
+
+
+
+
+
+
+ +
+
+
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 @@ +

Welcome Home

\ 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 @@ + + +

$ORGNAME

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 @@ + + +

Hi, Ivar

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..683938a --- /dev/null +++ b/code/app/src/routes/(main)/(app)/projects/+page.svelte @@ -0,0 +1,5 @@ + + +

Projects

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..ae6d403 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/settings/+page.svelte @@ -0,0 +1,4 @@ + + +

Settings

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 @@ + + +

Tickets

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 @@ + + +

Todo

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 @@ + + +

Wiki

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..69c29c5 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/+layout.svelte @@ -0,0 +1,18 @@ + + + + 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..aa26892 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte @@ -0,0 +1,82 @@ + + +
+
+

+ {$LL.resetPasswordPage.requestAPasswordReset()} +

+

+ {$LL.or().toLowerCase()} + + {$LL.signIntoYourAccount().toLowerCase()} + +

+
+ +
+
+
+ + + + + +
+
+
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js new file mode 100644 index 0000000..1c7fa30 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js @@ -0,0 +1,11 @@ +import { is_guid } from '$lib/helpers'; +import { redirect } from '@sveltejs/kit'; +export const load = async ({ params }) => { + const resetRequestId = params.id ?? ""; + if (!is_guid(resetRequestId)) + throw redirect(302, "/reset-password"); + return { + resetRequestId + }; +}; +//# sourceMappingURL=+page.server.js.map \ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js.map b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js.map new file mode 100644 index 0000000..52fb93b --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"+page.server.js","sourceRoot":"","sources":["+page.server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,MAAM,CAAC,MAAM,IAAI,GAAmB,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;IACrD,MAAM,cAAc,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IACvC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC;QAAE,MAAM,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACrE,OAAO;QACH,cAAc;KACjB,CAAC;AACN,CAAC,CAAC"} \ 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..389d04c --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts @@ -0,0 +1,11 @@ +import { is_guid } from '$lib/helpers'; +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..562d902 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte @@ -0,0 +1,132 @@ + + +
+ {#if finishedPreliminaryLoading} +
+

+ {$LL.resetPasswordPage.setANewPassword()} +

+

+ {$LL.or().toLowerCase()} + + {$LL.signIntoYourAccount().toLowerCase()} + +

+
+ +
+
+
+ {#if errorState === "404"} + + {:else if errorState === "expired"} + + {:else if errorState === "unknown"} + + {/if} + + + +
+
+ {:else} +

Checking your request...

+ {/if} +
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..908e2ba --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte @@ -0,0 +1,133 @@ + + +
+ {#if messageType} +
+ {#if messageType === "after-password-reset"} + + {:else if messageType === "user-disabled"} + + {:else if messageType === "user-inactivity"} + + {/if} +
+ {/if} +
+

+ {$LL.signInPage.signIn()} +

+

+ {$LL.or().toLowerCase()} + {$LL.createANewAccount().toLowerCase()} +

+
+
+
+ {#if showErrorAlert} + + {/if} +
+ + + + + + +
+
+
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..cbdcbf6 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/index.ts @@ -0,0 +1,19 @@ +export enum Message { + AFTER_PASSWORD_RESET = "after-password-reset", + USER_INACTIVITY = "user-inactivity", + USER_DISABLED = "user-disabled", +} + +export const messageQueryKey = "m"; +export const signInPageTestKeys = { + passwordInput: "password-input", + usernameInput: "username-input", + rememberMeCheckbox: "remember-me-checkbox", + signInForm: "sign-in-form", + userInactivityAlert: Message.USER_INACTIVITY + "-alert", + userDisabledAlert: Message.USER_DISABLED + "-alert", + afterPasswordResetAlert: Message.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-in/tests/index.spec.ts b/code/app/src/routes/(main)/(public)/sign-in/tests/index.spec.ts new file mode 100644 index 0000000..ea8c494 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/tests/index.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from "@playwright/test"; +import { signInPageTestKeys } from "../index"; +import { get_test_context } from "$lib/configuration"; +import { get_pw_key_selector } from "$lib/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-up/+page.svelte b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte new file mode 100644 index 0000000..0dfa41a --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte @@ -0,0 +1,82 @@ + + +
+
+

+ {$LL.signUpPage.createYourNewAccount()} +

+

+ {$LL.or().toLowerCase()} + + {$LL.signIntoYourAccount().toLowerCase()} + +

+
+ +
+
+ +
+ + + +
+
+
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..d2eb2eb --- /dev/null +++ b/code/app/src/routes/(main)/+layout.server.ts @@ -0,0 +1,34 @@ +import { api_base, CookieNames } from "$lib/configuration"; +import { logError } from "$lib/logger"; +import { error, redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ routeId, cookies, locals }) => { + const isPublicRoute = (routeId?.startsWith("(main)/(public)") || routeId === "(main)") ?? true; + + let sessionIsValid = (await fetch(api_base("_/valid-session"), { + headers: { + Cookie: CookieNames.session + "=" + cookies.get(CookieNames.session) + } + }).catch((e) => { + logError(e); + throw error(503, { + message: "We are experiencing a service distruption! Have patience while we resolve the issue." + }) + })).ok; + + console.log("Base Layout loaded", { + sessionIsValid, + isPublicRoute, + routeId + }); + + if (sessionIsValid && isPublicRoute) { + throw redirect(302, "/home"); + } else if (!sessionIsValid && !isPublicRoute) { + throw redirect(302, "/sign-in"); + } + return { + locale: locals.locale + } +}; \ No newline at end of file diff --git a/code/app/src/routes/(main)/+layout.svelte b/code/app/src/routes/(main)/+layout.svelte new file mode 100644 index 0000000..1a870bb --- /dev/null +++ b/code/app/src/routes/(main)/+layout.svelte @@ -0,0 +1,29 @@ + + + + +{#if !online} +
+
+
+ +
+
+

You seem to be offline, please check your internet connection.

+
+
+
+{/if} + + + diff --git a/code/app/src/routes/(main)/+layout.ts b/code/app/src/routes/(main)/+layout.ts new file mode 100644 index 0000000..5d0e005 --- /dev/null +++ b/code/app/src/routes/(main)/+layout.ts @@ -0,0 +1,15 @@ +import type { LayoutLoad } from './$types' +import type { Locales } from '$lib/i18n/i18n-types' +import { loadLocaleAsync } from '$lib/i18n/i18n-util.async' +import { setLocale } from '$lib/i18n/i18n-svelte' + +export const load: LayoutLoad<{ locale: Locales }> = async ({ data: { locale } }) => { + // load dictionary into memory + await loadLocaleAsync(locale) + + // if you need to output a localized string in a `load` function, + // you always need to call `setLocale` right before you access the `LL` store + setLocale(locale) + // pass locale to the "rendering context" + 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 @@ +

Hold on...

diff --git a/code/app/src/routes/book/+layout.svelte b/code/app/src/routes/book/+layout.svelte new file mode 100644 index 0000000..aeed0d4 --- /dev/null +++ b/code/app/src/routes/book/+layout.svelte @@ -0,0 +1,64 @@ + + +
+ +
+ +
+
+ + 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 @@ +

A showcase of greatoffices components

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..d008d85 --- /dev/null +++ b/code/app/src/routes/book/alerts/+page.svelte @@ -0,0 +1,70 @@ + + +
+

Info

+ +
+
+

Warning

+ +
+
+

Error

+ +
+
+

Success

+ +
+
+

Actions

+ +
+
+

Right link

+ alert("Right link clicked")} + rightLinkText="Link or action" + title="Go here" + message="Hehe" + type="error" + /> +
+
+

List

+ { + alert("Repeat requested"); + }} + actions={[{ id: "repeat", text: "Try again" }]} + /> +
+
+

Closeable

+ +
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..19ba163 --- /dev/null +++ b/code/app/src/routes/book/buttons/+page.svelte @@ -0,0 +1,23 @@ + + +
+

Primary

+
+
+

Secondary

+
+
+

White

+
+
+

Loading

+
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..a693f69 --- /dev/null +++ b/code/app/src/routes/book/inputs/+page.svelte @@ -0,0 +1,48 @@ + + +
+

Default

+ +
+ +
+

With icon

+ +
+ +
+

With corner hint

+ +
+ +
+

Disabled

+ +
+ +
+

Errored

+ +
+ +
+

Help

+ +
+
+

Addon

+ +
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..94228b4 --- /dev/null +++ b/code/app/src/routes/book/toggles/+page.svelte @@ -0,0 +1,27 @@ + + +
+

Default

+ +
+
+

Short

+ +
+
+

Icon

+ +
+
+

Label / Description

+
+ +
+
+ +
+

Label / Description (right aligned)

+ +
\ 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 Binary files /dev/null and b/code/app/static/favicon.ico differ diff --git a/code/app/svelte.config.js b/code/app/svelte.config.js new file mode 100644 index 0000000..3dff752 --- /dev/null +++ b/code/app/svelte.config.js @@ -0,0 +1,24 @@ +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", + "$lib": "src/lib", + "$routes": "src/routes", + }, + prerender: { + enabled: false, + } + }, +}; + +export default config; diff --git a/code/app/tailwind.config.cjs b/code/app/tailwind.config.cjs new file mode 100644 index 0000000..2f80e55 --- /dev/null +++ b/code/app/tailwind.config.cjs @@ -0,0 +1,135 @@ +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", + "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", + "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..01d0864 --- /dev/null +++ b/code/app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "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..f777f75 --- /dev/null +++ b/code/app/vite.config.js @@ -0,0 +1,14 @@ +import { sveltekit } from '@sveltejs/kit/vite'; + +/** @type {import('vite').UserConfig} */ +const config = { + plugins: [sveltekit()], + build: { target: "es2020" }, + optimizeDeps: { + esbuildOptions: { + target: "es2020" + } + } +}; + +export default config; diff --git a/code/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs b/code/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs new file mode 100644 index 0000000..10525fd --- /dev/null +++ b/code/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 +{ + 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/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs b/code/tests/IOL.GreatOffice.IntegrationTests/Helpers/Element.cs new file mode 100644 index 0000000..da83cc3 --- /dev/null +++ b/code/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/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs b/code/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs new file mode 100644 index 0000000..080fa9f --- /dev/null +++ b/code/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/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj b/code/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj new file mode 100644 index 0000000..0376a10 --- /dev/null +++ b/code/tests/IOL.GreatOffice.IntegrationTests/IOL.GreatOffice.IntegrationTests.csproj @@ -0,0 +1,21 @@ + + + + Exe + true + net6.0 + enable + disable + + + + + + + + + + + + + -- cgit v1.3