aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--code/api/src/Data/AppDbContext.cs51
-rw-r--r--code/api/src/Data/Database/Api/ApiAccessToken.cs (renamed from code/api/src/Data/Database/ApiAccessToken.cs)0
-rw-r--r--code/api/src/Data/Database/Base.cs11
-rw-r--r--code/api/src/Data/Database/BaseWithOwner.cs31
-rw-r--r--code/api/src/Data/Database/Customer.cs6
-rw-r--r--code/api/src/Data/Database/Customer/Customer.cs29
-rw-r--r--code/api/src/Data/Database/Customer/CustomerContact.cs (renamed from code/api/src/Data/Database/CustomerContact.cs)0
-rw-r--r--code/api/src/Data/Database/Customer/CustomerEvent.cs (renamed from code/api/src/Data/Database/CustomerEvent.cs)3
-rw-r--r--code/api/src/Data/Database/Customer/CustomerGroup.cs7
-rw-r--r--code/api/src/Data/Database/Customer/CustomerGroupMembership.cs7
-rw-r--r--code/api/src/Data/Database/Internal/ForgotPasswordRequest.cs (renamed from code/api/src/Data/Database/ForgotPasswordRequest.cs)0
-rw-r--r--code/api/src/Data/Database/Internal/Tenant.cs (renamed from code/api/src/Data/Database/Tenant.cs)0
-rw-r--r--code/api/src/Data/Database/Internal/User.cs (renamed from code/api/src/Data/Database/User.cs)0
-rw-r--r--code/api/src/Data/Database/MainAppDatabase.cs85
-rw-r--r--code/api/src/Data/Database/Project.cs12
-rw-r--r--code/api/src/Data/Database/Project/Project.cs15
-rw-r--r--code/api/src/Data/Database/Project/ProjectLabel.cs (renamed from code/api/src/Data/Database/ProjectLabel.cs)4
-rw-r--r--code/api/src/Data/Database/Project/ProjectMember.cs8
-rw-r--r--code/api/src/Data/Database/Time/TimeCategory.cs32
-rw-r--r--code/api/src/Data/Database/Time/TimeEntry.cs46
-rw-r--r--code/api/src/Data/Database/Time/TimeLabel.cs32
-rw-r--r--code/api/src/Data/Database/TimeCategory.cs31
-rw-r--r--code/api/src/Data/Database/TimeEntry.cs45
-rw-r--r--code/api/src/Data/Database/TimeLabel.cs31
-rw-r--r--code/api/src/Data/Database/Todo/Todo.cs (renamed from code/api/src/Data/Database/Todo.cs)0
-rw-r--r--code/api/src/Data/Database/Todo/TodoComment.cs (renamed from code/api/src/Data/Database/TodoComment.cs)0
-rw-r--r--code/api/src/Data/Database/Todo/TodoLabel.cs (renamed from code/api/src/Data/Database/TodoLabel.cs)0
-rw-r--r--code/api/src/Data/Database/Todo/TodoProject.cs (renamed from code/api/src/Data/Database/TodoProject.cs)0
-rw-r--r--code/api/src/Data/Database/Todo/TodoProjectAccessControl.cs (renamed from code/api/src/Data/Database/TodoProjectAccessControl.cs)0
-rw-r--r--code/api/src/Data/Database/Todo/TodoStatus.cs (renamed from code/api/src/Data/Database/TodoStatus.cs)8
-rw-r--r--code/api/src/Data/Enums/ProjectRole.cs9
-rw-r--r--code/api/src/Data/Enums/StringsLang.cs7
-rw-r--r--code/api/src/Data/Models/AppConfiguration.cs (renamed from code/api/src/Data/Static/AppConfiguration.cs)2
-rw-r--r--code/api/src/Data/Models/LoggedInUserModel.cs7
-rw-r--r--code/api/src/Endpoints/EndpointBase.cs30
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs18
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs68
-rw-r--r--code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs52
-rw-r--r--code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs77
-rw-r--r--code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs99
-rw-r--r--code/api/src/Endpoints/Internal/Account/GetRoute.cs8
-rw-r--r--code/api/src/Endpoints/Internal/Account/LoginRoute.cs57
-rw-r--r--code/api/src/Endpoints/Internal/Account/LogoutRoute.cs32
-rw-r--r--code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs77
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestPayload.cs (renamed from code/api/src/Endpoints/Internal/PasswordResetRequests/Create/RequestModel.cs)4
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs (renamed from code/api/src/Endpoints/Internal/PasswordResetRequests/Create/Route.cs)33
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs51
-rw-r--r--code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs42
-rw-r--r--code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs29
-rw-r--r--code/api/src/Endpoints/Internal/Root/LogRoute.cs20
-rw-r--r--code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs22
-rw-r--r--code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs18
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs30
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs32
-rw-r--r--code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs71
-rw-r--r--code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs60
-rw-r--r--code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs62
-rw-r--r--code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs63
-rw-r--r--code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs107
-rw-r--r--code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs54
-rw-r--r--code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs282
-rw-r--r--code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs102
-rw-r--r--code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs70
-rw-r--r--code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs51
-rw-r--r--code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs49
-rw-r--r--code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs58
-rw-r--r--code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs80
-rw-r--r--code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs43
-rw-r--r--code/api/src/Endpoints/V1/Projects/_calls.http12
-rw-r--r--code/api/src/Endpoints/V1/RouteBaseAsync.cs4
-rw-r--r--code/api/src/IOL.GreatOffice.Api.csproj30
-rw-r--r--code/api/src/Jobs/TokenCleanupJob.cs4
-rw-r--r--code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs2
-rw-r--r--code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs2
-rw-r--r--code/api/src/Migrations/20211002113037_V6Migration.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220319203018_UserBase.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220320115601_Update1.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220530174741_Tenants.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs2
-rw-r--r--code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs2
-rw-r--r--code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs1074
-rw-r--r--code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs304
-rw-r--r--code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs1126
-rw-r--r--code/api/src/Migrations/20221030081459_DeletedAt.cs146
-rw-r--r--code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs1126
-rw-r--r--code/api/src/Migrations/20221030084716_MinorChanges.cs105
-rw-r--r--code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs1148
-rw-r--r--code/api/src/Migrations/20221030090557_MoreMinorChanges.cs78
-rw-r--r--code/api/src/Migrations/AppDbContextModelSnapshot.cs658
-rw-r--r--code/api/src/Program.cs11
-rw-r--r--code/api/src/Resources/SharedResources.cs4
-rw-r--r--code/api/src/Resources/SharedResources.en.Designer.cs78
-rw-r--r--code/api/src/Resources/SharedResources.en.resx41
-rw-r--r--code/api/src/Resources/SharedResources.nb.Designer.cs72
-rw-r--r--code/api/src/Resources/SharedResources.nb.resx42
-rw-r--r--code/api/src/Resources/SharedResources.resx30
-rw-r--r--code/api/src/Services/MailService.cs2
-rw-r--r--code/api/src/Services/PasswordResetService.cs33
-rw-r--r--code/api/src/Services/UserService.cs80
-rw-r--r--code/api/src/Services/VaultService.cs288
-rw-r--r--code/api/src/Utilities/BasicAuthenticationHandler.cs4
-rw-r--r--code/api/src/Utilities/DateTimeExtensions.cs8
-rw-r--r--code/api/src/Utilities/QueryableExtensions.cs12
-rw-r--r--code/api/src/Utilities/Validators.cs12
115 files changed, 7785 insertions, 1405 deletions
diff --git a/code/api/src/Data/AppDbContext.cs b/code/api/src/Data/AppDbContext.cs
deleted file mode 100644
index c970429..0000000
--- a/code/api/src/Data/AppDbContext.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
-
-namespace IOL.GreatOffice.Api.Data;
-
-public class AppDbContext : DbContext, IDataProtectionKeyContext
-{
- public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
- public DbSet<User> Users { get; set; }
- public DbSet<ForgotPasswordRequest> ForgotPasswordRequests { get; set; }
- public DbSet<TimeLabel> TimeLabels { get; set; }
- public DbSet<TimeEntry> TimeEntries { get; set; }
- public DbSet<TimeCategory> TimeCategories { get; set; }
- public DbSet<ApiAccessToken> AccessTokens { get; set; }
- public DbSet<Tenant> Tenants { get; set; }
- public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
-
- protected override void OnModelCreating(ModelBuilder modelBuilder) {
- modelBuilder.Entity<User>(e => {
- e.ToTable("users");
- });
-
- modelBuilder.Entity<ForgotPasswordRequest>(e => {
- e.HasOne(c => c.User);
- e.ToTable("forgot_password_requests");
- });
-
- modelBuilder.Entity<TimeCategory>(e => {
- e.ToTable("time_categories");
- });
-
- modelBuilder.Entity<TimeLabel>(e => {
- e.ToTable("time_labels");
- });
-
- modelBuilder.Entity<TimeEntry>(e => {
- e.HasOne(c => c.Category);
- e.HasMany(c => c.Labels);
- e.ToTable("time_entries");
- });
-
- modelBuilder.Entity<ApiAccessToken>(e => {
- e.ToTable("api_access_tokens");
- });
-
- modelBuilder.Entity<Tenant>(e => {
- e.ToTable("tenants");
- });
-
- base.OnModelCreating(modelBuilder);
- }
-}
diff --git a/code/api/src/Data/Database/ApiAccessToken.cs b/code/api/src/Data/Database/Api/ApiAccessToken.cs
index 9582869..9582869 100644
--- a/code/api/src/Data/Database/ApiAccessToken.cs
+++ b/code/api/src/Data/Database/Api/ApiAccessToken.cs
diff --git a/code/api/src/Data/Database/Base.cs b/code/api/src/Data/Database/Base.cs
index ae9efa2..900b923 100644
--- a/code/api/src/Data/Database/Base.cs
+++ b/code/api/src/Data/Database/Base.cs
@@ -10,6 +10,13 @@ public class Base
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;
+ public DateTime? DeletedAt { get; private set; }
+ public bool Deleted { get; private set; }
+
+ public void SetModified() => ModifiedAt = AppDateTime.UtcNow;
+
+ public void SetDeleted() {
+ Deleted = true;
+ DeletedAt = AppDateTime.UtcNow;
+ }
} \ No newline at end of file
diff --git a/code/api/src/Data/Database/BaseWithOwner.cs b/code/api/src/Data/Database/BaseWithOwner.cs
index 1eb99f4..84b5bfe 100644
--- a/code/api/src/Data/Database/BaseWithOwner.cs
+++ b/code/api/src/Data/Database/BaseWithOwner.cs
@@ -1,19 +1,34 @@
namespace IOL.GreatOffice.Api.Data.Database;
/// <summary>
-/// Base class for all entities.
+/// Base class for all entities with ownership.
/// </summary>
public class BaseWithOwner : Base
{
protected BaseWithOwner() { }
- protected BaseWithOwner(Guid userId) {
- UserId = userId;
+ protected BaseWithOwner(LoggedInUserModel loggedInUser) {
+ CreatedBy = loggedInUser.Id;
}
- 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; }
+ public Guid? UserId { get; private set; }
+ public Guid? TenantId { get; private set; }
+ public Guid? ModifiedBy { get; private set; }
+ public Guid? CreatedBy { get; private set; }
+ public Guid? DeletedBy { get; private set; }
+
+ public void SetDeleted(Guid userId) {
+ DeletedBy = userId;
+ SetDeleted();
+ }
+
+ public void SetModified(Guid userId) {
+ ModifiedBy = userId;
+ SetModified();
+ }
+
+ public void SetOwnerIds(Guid userId = default, Guid tenantId = default) {
+ if (tenantId != default) TenantId = tenantId;
+ if (userId != default) UserId = userId;
+ }
} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Customer.cs b/code/api/src/Data/Database/Customer.cs
deleted file mode 100644
index c6b06a4..0000000
--- a/code/api/src/Data/Database/Customer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-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/Customer/Customer.cs b/code/api/src/Data/Database/Customer/Customer.cs
new file mode 100644
index 0000000..f9953da
--- /dev/null
+++ b/code/api/src/Data/Database/Customer/Customer.cs
@@ -0,0 +1,29 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Customer : BaseWithOwner
+{
+ public Customer() { }
+ public Customer(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+
+ public string CustomerNumber { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Address1 { get; set; }
+ public string Address2 { get; set; }
+ public string PostalCode { get; set; }
+ public string PostalCity { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Email { get; set; }
+ public string VATNumber { get; set; }
+ public string ORGNumber { get; set; }
+ public string DefaultReference { get; set; }
+ public string Website { get; set; }
+ public string Currency { get; set; }
+
+ public User Owner { get; set; }
+ public ICollection<CustomerGroup> Groups { get; set; }
+ public ICollection<CustomerContact> Contacts { get; set; }
+ public ICollection<CustomerEvent> Events { get; set; }
+ public ICollection<Project> Projects { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/CustomerContact.cs b/code/api/src/Data/Database/Customer/CustomerContact.cs
index f5a951d..f5a951d 100644
--- a/code/api/src/Data/Database/CustomerContact.cs
+++ b/code/api/src/Data/Database/Customer/CustomerContact.cs
diff --git a/code/api/src/Data/Database/CustomerEvent.cs b/code/api/src/Data/Database/Customer/CustomerEvent.cs
index da3e3ed..a87da4c 100644
--- a/code/api/src/Data/Database/CustomerEvent.cs
+++ b/code/api/src/Data/Database/Customer/CustomerEvent.cs
@@ -3,5 +3,6 @@ namespace IOL.GreatOffice.Api.Data.Database;
public class CustomerEvent : BaseWithOwner
{
public Customer Customer { get; set; }
- public string Name { get; set; }
+ public string Title { get; set; }
+ public string Note { get; set; }
}
diff --git a/code/api/src/Data/Database/Customer/CustomerGroup.cs b/code/api/src/Data/Database/Customer/CustomerGroup.cs
new file mode 100644
index 0000000..9438f3c
--- /dev/null
+++ b/code/api/src/Data/Database/Customer/CustomerGroup.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerGroup : BaseWithOwner
+{
+ public string Name { get; set; }
+ public ICollection<Customer> Customers { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Customer/CustomerGroupMembership.cs b/code/api/src/Data/Database/Customer/CustomerGroupMembership.cs
new file mode 100644
index 0000000..ec0d4af
--- /dev/null
+++ b/code/api/src/Data/Database/Customer/CustomerGroupMembership.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerGroupMembership : Base
+{
+ public Customer Customer { get; set; }
+ public CustomerGroup Group { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/ForgotPasswordRequest.cs b/code/api/src/Data/Database/Internal/ForgotPasswordRequest.cs
index 1510a35..1510a35 100644
--- a/code/api/src/Data/Database/ForgotPasswordRequest.cs
+++ b/code/api/src/Data/Database/Internal/ForgotPasswordRequest.cs
diff --git a/code/api/src/Data/Database/Tenant.cs b/code/api/src/Data/Database/Internal/Tenant.cs
index b185c7a..b185c7a 100644
--- a/code/api/src/Data/Database/Tenant.cs
+++ b/code/api/src/Data/Database/Internal/Tenant.cs
diff --git a/code/api/src/Data/Database/User.cs b/code/api/src/Data/Database/Internal/User.cs
index 9db5d35..9db5d35 100644
--- a/code/api/src/Data/Database/User.cs
+++ b/code/api/src/Data/Database/Internal/User.cs
diff --git a/code/api/src/Data/Database/MainAppDatabase.cs b/code/api/src/Data/Database/MainAppDatabase.cs
new file mode 100644
index 0000000..b529791
--- /dev/null
+++ b/code/api/src/Data/Database/MainAppDatabase.cs
@@ -0,0 +1,85 @@
+using IOL.GreatOffice.Api.Endpoints.V1.Projects;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class MainAppDatabase : DbContext, IDataProtectionKeyContext
+{
+ public MainAppDatabase(DbContextOptions<MainAppDatabase> options) : base(options) { }
+ public DbSet<User> Users { get; set; }
+ public DbSet<ForgotPasswordRequest> ForgotPasswordRequests { get; set; }
+ public DbSet<TimeLabel> TimeLabels { get; set; }
+ public DbSet<TimeEntry> TimeEntries { get; set; }
+ public DbSet<TimeCategory> TimeCategories { get; set; }
+ public DbSet<ApiAccessToken> AccessTokens { get; set; }
+ public DbSet<Tenant> Tenants { get; set; }
+ public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
+ public DbSet<Project> Projects { get; set; }
+ public DbSet<ProjectLabel> ProjectLabels { get; set; }
+ public DbSet<Customer> Customers { get; set; }
+ public DbSet<CustomerContact> CustomersContacts { get; set; }
+ public DbSet<CustomerEvent> CustomerEvents { get; set; }
+ public DbSet<CustomerGroup> CustomerGroups { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder) {
+ modelBuilder.Entity<User>(e => {
+ e.HasMany(n => n.Tenants);
+ e.ToTable("users");
+ });
+ modelBuilder.Entity<ForgotPasswordRequest>(e => {
+ e.HasOne(c => c.User);
+ e.ToTable("forgot_password_requests");
+ });
+ modelBuilder.Entity<TimeCategory>(e => { e.ToTable("time_categories"); });
+ modelBuilder.Entity<TimeLabel>(e => { e.ToTable("time_labels"); });
+ modelBuilder.Entity<TimeEntry>(e => {
+ e.HasOne(c => c.Category);
+ e.HasMany(c => c.Labels);
+ e.ToTable("time_entries");
+ });
+ modelBuilder.Entity<ApiAccessToken>(e => {
+ e.HasOne(n => n.User);
+ e.ToTable("api_access_tokens");
+ });
+ modelBuilder.Entity<Tenant>(e => {
+ e.HasMany(n => n.Users);
+ e.ToTable("tenants");
+ });
+ modelBuilder.Entity<Project>(e => {
+ e.HasMany(n => n.Members);
+ e.HasMany(n => n.Customers);
+ e.ToTable("projects");
+ });
+ modelBuilder.Entity<ProjectMember>(e => {
+ e.HasOne(n => n.Project);
+ e.HasOne(n => n.User);
+ e.ToTable("project_members");
+ });
+ modelBuilder.Entity<ProjectLabel>(e => {
+ e.HasOne(n => n.Project);
+ e.ToTable("project_labels");
+ });
+ modelBuilder.Entity<Customer>(e => {
+ e.HasOne(n => n.Owner);
+ e.HasMany(n => n.Events);
+ e.HasMany(n => n.Contacts);
+ e.HasMany(n => n.Groups);
+ e.HasMany(n => n.Projects);
+ e.ToTable("customers");
+ });
+ modelBuilder.Entity<CustomerContact>(e => {
+ e.HasOne(n => n.Customer);
+ e.ToTable("customer_contacts");
+ });
+ modelBuilder.Entity<CustomerEvent>(e => {
+ e.HasOne(n => n.Customer);
+ e.ToTable("customer_events");
+ });
+ modelBuilder.Entity<CustomerGroup>(e => {
+ e.HasMany(n => n.Customers);
+ e.ToTable("customer_groups");
+ });
+
+ base.OnModelCreating(modelBuilder);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Project.cs b/code/api/src/Data/Database/Project.cs
deleted file mode 100644
index 99c6e7f..0000000
--- a/code/api/src/Data/Database/Project.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace IOL.GreatOffice.Api.Data.Database;
-
-public class Project : BaseWithOwner
-{
- public string Name { get; set; }
- public string Description { get; set; }
- public DateTime? Start { get; set; }
- public DateTime? Stop { get; set; }
- public List<Customer> Customers { get; set; }
- public List<User> Owners { get; set; }
- public List<ProjectLabel> Labels { get; set; }
-}
diff --git a/code/api/src/Data/Database/Project/Project.cs b/code/api/src/Data/Database/Project/Project.cs
new file mode 100644
index 0000000..de9e2cb
--- /dev/null
+++ b/code/api/src/Data/Database/Project/Project.cs
@@ -0,0 +1,15 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Project : BaseWithOwner
+{
+ public Project() { }
+
+ public Project(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public DateTime? Start { get; set; }
+ public DateTime? Stop { get; set; }
+ public ICollection<Customer> Customers { get; set; }
+ public ICollection<ProjectMember> Members { get; set; }
+ public ICollection<ProjectLabel> Labels { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/ProjectLabel.cs b/code/api/src/Data/Database/Project/ProjectLabel.cs
index 8fb18c2..0e1dc5d 100644
--- a/code/api/src/Data/Database/ProjectLabel.cs
+++ b/code/api/src/Data/Database/Project/ProjectLabel.cs
@@ -2,7 +2,7 @@ namespace IOL.GreatOffice.Api.Data.Database;
public class ProjectLabel : BaseWithOwner
{
- public string Name { get; set; }
+ public string Value { get; set; }
public string Color { get; set; }
- public Project Todo { get; set; }
+ public Project Project { get; set; }
}
diff --git a/code/api/src/Data/Database/Project/ProjectMember.cs b/code/api/src/Data/Database/Project/ProjectMember.cs
new file mode 100644
index 0000000..a5e0682
--- /dev/null
+++ b/code/api/src/Data/Database/Project/ProjectMember.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ProjectMember : Base
+{
+ public Project Project { get; set; }
+ public User User { get; set; }
+ public ProjectRole Role { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Time/TimeCategory.cs b/code/api/src/Data/Database/Time/TimeCategory.cs
new file mode 100644
index 0000000..c3f013d
--- /dev/null
+++ b/code/api/src/Data/Database/Time/TimeCategory.cs
@@ -0,0 +1,32 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeCategory : BaseWithOwner
+{
+ public TimeCategory() { }
+
+ public TimeCategory(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+ 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; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Time/TimeEntry.cs b/code/api/src/Data/Database/Time/TimeEntry.cs
new file mode 100644
index 0000000..0405df2
--- /dev/null
+++ b/code/api/src/Data/Database/Time/TimeEntry.cs
@@ -0,0 +1,46 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeEntry : BaseWithOwner
+{
+ public TimeEntry() { }
+
+ public TimeEntry(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+ public DateTime Start { get; set; }
+ public DateTime Stop { get; set; }
+ public string Description { get; set; }
+ public ICollection<TimeLabel> 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<TimeLabel.TimeLabelDto> Labels { get; set; }
+ public TimeCategory.TimeCategoryDto Category { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Time/TimeLabel.cs b/code/api/src/Data/Database/Time/TimeLabel.cs
new file mode 100644
index 0000000..365161c
--- /dev/null
+++ b/code/api/src/Data/Database/Time/TimeLabel.cs
@@ -0,0 +1,32 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeLabel : BaseWithOwner
+{
+ public TimeLabel() { }
+
+ public TimeLabel(LoggedInUserModel loggedInUserModel) : base(loggedInUserModel) { }
+ 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; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/TimeCategory.cs b/code/api/src/Data/Database/TimeCategory.cs
deleted file mode 100644
index 69c6957..0000000
--- a/code/api/src/Data/Database/TimeCategory.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-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
deleted file mode 100644
index 46c62e1..0000000
--- a/code/api/src/Data/Database/TimeEntry.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-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<TimeLabel> 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<TimeLabel.TimeLabelDto> 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
deleted file mode 100644
index 55e20b0..0000000
--- a/code/api/src/Data/Database/TimeLabel.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-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/Todo.cs
index 5fe3c9a..5fe3c9a 100644
--- a/code/api/src/Data/Database/Todo.cs
+++ b/code/api/src/Data/Database/Todo/Todo.cs
diff --git a/code/api/src/Data/Database/TodoComment.cs b/code/api/src/Data/Database/Todo/TodoComment.cs
index 44dcbed..44dcbed 100644
--- a/code/api/src/Data/Database/TodoComment.cs
+++ b/code/api/src/Data/Database/Todo/TodoComment.cs
diff --git a/code/api/src/Data/Database/TodoLabel.cs b/code/api/src/Data/Database/Todo/TodoLabel.cs
index 7753ade..7753ade 100644
--- a/code/api/src/Data/Database/TodoLabel.cs
+++ b/code/api/src/Data/Database/Todo/TodoLabel.cs
diff --git a/code/api/src/Data/Database/TodoProject.cs b/code/api/src/Data/Database/Todo/TodoProject.cs
index 5e22bbe..5e22bbe 100644
--- a/code/api/src/Data/Database/TodoProject.cs
+++ b/code/api/src/Data/Database/Todo/TodoProject.cs
diff --git a/code/api/src/Data/Database/TodoProjectAccessControl.cs b/code/api/src/Data/Database/Todo/TodoProjectAccessControl.cs
index 964f831..964f831 100644
--- a/code/api/src/Data/Database/TodoProjectAccessControl.cs
+++ b/code/api/src/Data/Database/Todo/TodoProjectAccessControl.cs
diff --git a/code/api/src/Data/Database/TodoStatus.cs b/code/api/src/Data/Database/Todo/TodoStatus.cs
index 416212d..9ace7d0 100644
--- a/code/api/src/Data/Database/TodoStatus.cs
+++ b/code/api/src/Data/Database/Todo/TodoStatus.cs
@@ -10,35 +10,27 @@ public class TodoStatus : BaseWithOwner
return new List<TodoStatus>() {
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/Enums/ProjectRole.cs b/code/api/src/Data/Enums/ProjectRole.cs
new file mode 100644
index 0000000..c4a3f29
--- /dev/null
+++ b/code/api/src/Data/Enums/ProjectRole.cs
@@ -0,0 +1,9 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+public enum ProjectRole
+{
+ EXTERNAL,
+ RESOURCE,
+ LEADER,
+ OWNER
+} \ No newline at end of file
diff --git a/code/api/src/Data/Enums/StringsLang.cs b/code/api/src/Data/Enums/StringsLang.cs
deleted file mode 100644
index e4e2066..0000000
--- a/code/api/src/Data/Enums/StringsLang.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace IOL.GreatOffice.Api.Data.Enums;
-
-public enum StringsLang
-{
- ENGLISH_GB = 0,
- NORWEGIAN_NB = 1
-} \ No newline at end of file
diff --git a/code/api/src/Data/Static/AppConfiguration.cs b/code/api/src/Data/Models/AppConfiguration.cs
index 4ee7a8e..f4346bb 100644
--- a/code/api/src/Data/Static/AppConfiguration.cs
+++ b/code/api/src/Data/Models/AppConfiguration.cs
@@ -1,6 +1,6 @@
using System.Security.Cryptography.X509Certificates;
-namespace IOL.GreatOffice.Api.Data.Static;
+namespace IOL.GreatOffice.Api.Data.Models;
public class AppConfiguration
{
diff --git a/code/api/src/Data/Models/LoggedInUserModel.cs b/code/api/src/Data/Models/LoggedInUserModel.cs
index d802b77..541d4a5 100644
--- a/code/api/src/Data/Models/LoggedInUserModel.cs
+++ b/code/api/src/Data/Models/LoggedInUserModel.cs
@@ -2,6 +2,7 @@ namespace IOL.GreatOffice.Api.Data.Models;
public class LoggedInUserModel
{
- public Guid Id { get; set; }
- public string Username { get; set; }
-}
+ public Guid Id { get; set; }
+ public string Username { get; set; }
+ public Guid TenantId { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/EndpointBase.cs b/code/api/src/Endpoints/EndpointBase.cs
index de5d967..a5b0931 100644
--- a/code/api/src/Endpoints/EndpointBase.cs
+++ b/code/api/src/Endpoints/EndpointBase.cs
@@ -1,3 +1,5 @@
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
namespace IOL.GreatOffice.Api.Endpoints;
[ApiController]
@@ -20,4 +22,32 @@ public class EndpointBase : ControllerBase
TraceId = HttpContext.TraceIdentifier
});
}
+
+ [NonAction]
+ protected RequestTimeZoneInfo GetRequestTimeZone(ILogger logger = default) {
+ Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader);
+ var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC");
+ var offset = tz.BaseUtcOffset.Hours;
+
+ // This is fine as long as the client is not connecting from Australia: Lord Howe Island,
+ // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country
+ if (tz.IsDaylightSavingTime(AppDateTime.UtcNow)) {
+ offset++;
+ }
+
+ logger?.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours");
+
+ return new RequestTimeZoneInfo() {
+ TimeZoneInfo = tz,
+ Offset = offset,
+ LocalDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz)
+ };
+ }
+
+ public class RequestTimeZoneInfo
+ {
+ public TimeZoneInfo TimeZoneInfo { get; set; }
+ public int Offset { get; set; }
+ public DateTime LocalDateTime { get; set; }
+ }
} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs
index dc73e68..1161af3 100644
--- a/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs
+++ b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs
@@ -5,13 +5,13 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
/// </summary>
public class CreateAccountPayload
{
- /// <summary>
- /// Username for the new account.
- /// </summary>
- public string Username { get; set; }
+ /// <summary>
+ /// Username for the new account.
+ /// </summary>
+ public string Username { get; set; }
- /// <summary>
- /// Password for the new account.
- /// </summary>
- public string Password { get; set; }
-}
+ /// <summary>
+ /// Password for the new account.
+ /// </summary>
+ public string Password { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs
index 0f4a383..f34056d 100644
--- a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs
@@ -1,44 +1,42 @@
namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
-/// <inheritdoc />
public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountPayload>.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly UserService _userService;
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
- /// <inheritdoc />
- public CreateAccountRoute(UserService userService, AppDbContext context) {
- _userService = userService;
- _context = context;
- }
+ public CreateAccountRoute(UserService userService, MainAppDatabase database) {
+ _userService = userService;
+ _database = database;
+ }
- /// <summary>
- /// Create a new user account.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpPost("~/_/account/create")]
- public override async Task<ActionResult> HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) {
- if (request.Username.IsValidEmailAddress() == false) {
- return BadRequest(new KnownProblemModel("Invalid form", request.Username + " does not look like a valid email"));
- }
+ /// <summary>
+ /// Create a new user account.
+ /// </summary>
+ /// <param name="request"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpPost("~/_/account/create")]
+ public override async Task<ActionResult> HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) {
+ if (request.Username.IsValidEmailAddress() == false) {
+ return BadRequest(new KnownProblemModel("Invalid form", request.Username + " does not look like a valid email"));
+ }
- if (request.Password.Length < 6) {
- return BadRequest(new KnownProblemModel("Invalid form", "The password requires 6 or more characters."));
- }
+ if (request.Password.Length < 6) {
+ return BadRequest(new KnownProblemModel("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 KnownProblemModel("Username is not available", "There is already a user registered with email: " + username));
- }
+ var username = request.Username.Trim();
+ if (_database.Users.Any(c => c.Username == username)) {
+ return BadRequest(new KnownProblemModel("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();
- }
-}
+ var user = new User(username);
+ user.HashAndSetPassword(request.Password);
+ _database.Users.Add(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ await _userService.LogInUser(HttpContext, user);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs
index 13fbdf4..56ff9c6 100644
--- a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs
@@ -1,34 +1,32 @@
namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
-/// <inheritdoc />
public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly UserService _userService;
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
- /// <inheritdoc />
- public CreateInitialAccountRoute(AppDbContext context, UserService userService) {
- _context = context;
- _userService = userService;
- }
+ public CreateInitialAccountRoute(MainAppDatabase database, UserService userService) {
+ _database = database;
+ _userService = userService;
+ }
- /// <summary>
- /// Create an initial user account.
- /// </summary>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpGet("~/_/account/create-initial")]
- public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
- if (_context.Users.Any()) {
- return NotFound();
- }
+ /// <summary>
+ /// Create an initial user account.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpGet("~/_/account/create-initial")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ if (_database.Users.Any()) {
+ return NotFound();
+ }
- var user = new User("admin@ivarlovlie.no");
- user.HashAndSetPassword("ivar123");
- _context.Users.Add(user);
- await _context.SaveChangesAsync(cancellationToken);
- await _userService.LogInUser(HttpContext, user);
- return Redirect("/");
- }
-}
+ var user = new User("admin@ivarlovlie.no");
+ user.HashAndSetPassword("ivar123");
+ _database.Users.Add(user);
+ await _database.SaveChangesAsync(cancellationToken);
+ await _userService.LogInUser(HttpContext, user);
+ return Redirect("/");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs
index 2149e15..5df1fb6 100644
--- a/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs
@@ -2,48 +2,47 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly UserService _userService;
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
- /// <inheritdoc />
- public DeleteAccountRoute(AppDbContext context, UserService userService) {
- _context = context;
- _userService = userService;
- }
+ public DeleteAccountRoute(MainAppDatabase database, UserService userService) {
+ _database = database;
+ _userService = userService;
+ }
- /// <summary>
- /// Delete the logged on user's account.
- /// </summary>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [HttpDelete("~/_/account/delete")]
- public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
- var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
- if (user == default) {
- await _userService.LogOutUser(HttpContext);
- return Unauthorized();
- }
+ /// <summary>
+ /// Delete the logged on user's account.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [HttpDelete("~/_/account/delete")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ var user = _database.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();
- }
+ 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);
+ var githubMappings = _database.TimeCategories.Where(c => c.UserId == user.Id);
+ var passwordResets = _database.ForgotPasswordRequests.Where(c => c.UserId == user.Id);
+ var entries = _database.TimeEntries.Where(c => c.UserId == user.Id);
+ var labels = _database.TimeLabels.Where(c => c.UserId == user.Id);
+ var categories = _database.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);
+ _database.TimeCategories.RemoveRange(githubMappings);
+ _database.ForgotPasswordRequests.RemoveRange(passwordResets);
+ _database.TimeEntries.RemoveRange(entries);
+ _database.TimeLabels.RemoveRange(labels);
+ _database.TimeCategories.RemoveRange(categories);
+ _database.Users.Remove(user);
- await _context.SaveChangesAsync(cancellationToken);
- await _userService.LogOutUser(HttpContext);
- return Ok();
- }
-}
+ await _database.SaveChangesAsync(cancellationToken);
+ await _userService.LogOutUser(HttpContext);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs
index f1b70f3..0d9f817 100644
--- a/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs
@@ -2,61 +2,60 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
public class GetAccountArchiveRoute : RouteBaseAsync.WithoutRequest.WithActionResult<UserArchiveDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public GetAccountArchiveRoute(AppDbContext context) {
- _context = context;
- }
+ public GetAccountArchiveRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a data archive with the currently logged on user's data.
- /// </summary>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [HttpGet("~/_/account/archive")]
- public override async Task<ActionResult<UserArchiveDto>> HandleAsync(CancellationToken cancellationToken = default) {
- var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
- if (user == default) {
- await HttpContext.SignOutAsync();
- return Unauthorized();
- }
+ /// <summary>
+ /// Get a data archive with the currently logged on user's data.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [HttpGet("~/_/account/archive")]
+ public override async Task<ActionResult<UserArchiveDto>> HandleAsync(CancellationToken cancellationToken = default) {
+ var user = _database.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 entries = _database.TimeEntries
+ .AsNoTracking()
+ .Include(c => c.Labels)
+ .Include(c => c.Category)
+ .Where(c => c.UserId == user.Id)
+ .ToList();
- var jsonOptions = new JsonSerializerOptions {
- WriteIndented = true
- };
+ 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
- },
- }));
+ 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();
+ dto.CountEntries();
- var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions);
+ var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions);
- return File(entriesSerialized,
- "application/json",
- user.Username + "-time-tracker-archive-" + AppDateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json");
- }
-}
+ return File(entriesSerialized,
+ "application/json",
+ user.Username + "-time-tracker-archive-" + AppDateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/GetRoute.cs b/code/api/src/Endpoints/Internal/Account/GetRoute.cs
index 1aa7ecb..8d6c50f 100644
--- a/code/api/src/Endpoints/Internal/Account/GetRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/GetRoute.cs
@@ -2,10 +2,10 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<LoggedInUserModel>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public GetAccountRoute(AppDbContext context) {
- _context = context;
+ public GetAccountRoute(MainAppDatabase database) {
+ _database = database;
}
/// <summary>
@@ -15,7 +15,7 @@ public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<Lo
/// <returns></returns>
[HttpGet("~/_/account")]
public override async Task<ActionResult<LoggedInUserModel>> HandleAsync(CancellationToken cancellationToken = default) {
- var user = _context.Users
+ var user = _database.Users
.Select(x => new {x.Username, x.Id})
.SingleOrDefault(c => c.Id == LoggedInUser.Id);
if (user != default) {
diff --git a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs
index e4ef54c..696c3c2 100644
--- a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs
@@ -1,37 +1,34 @@
namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
-public class LoginRoute : RouteBaseAsync
- .WithRequest<LoginPayload>
- .WithActionResult
+public class LoginRoute : RouteBaseAsync.WithRequest<LoginPayload>.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly UserService _userService;
+ private readonly MainAppDatabase _database;
+ private readonly UserService _userService;
- /// <inheritdoc />
- public LoginRoute(AppDbContext context, UserService userService) {
- _context = context;
- _userService = userService;
- }
+ public LoginRoute(MainAppDatabase database, UserService userService) {
+ _database = database;
+ _userService = userService;
+ }
- /// <summary>
- /// Login a user.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpPost("~/_/account/login")]
- public override async Task<ActionResult> HandleAsync(LoginPayload request, CancellationToken cancellationToken = default) {
- if (!ModelState.IsValid) {
- return BadRequest(ModelState);
- }
+ /// <summary>
+ /// Login a user.
+ /// </summary>
+ /// <param name="request"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpPost("~/_/account/login")]
+ public override async Task<ActionResult> 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 KnownProblemModel("Invalid username or password"));
- }
+ var user = _database.Users.SingleOrDefault(u => u.Username == request.Username);
+ if (user == default || !user.VerifyPassword(request.Password)) {
+ return BadRequest(new KnownProblemModel("Invalid username or password"));
+ }
- await _userService.LogInUser(HttpContext, user, request.Persist);
- return Ok();
- }
-}
+ await _userService.LogInUser(HttpContext, user, request.Persist);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs
index 4a06f4a..042d729 100644
--- a/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs
@@ -2,21 +2,21 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
public class LogoutRoute : RouteBaseAsync.WithoutRequest.WithActionResult
{
- private readonly UserService _userService;
+ private readonly UserService _userService;
- public LogoutRoute(UserService userService) {
- _userService = userService;
- }
+ public LogoutRoute(UserService userService) {
+ _userService = userService;
+ }
- /// <summary>
- /// Logout a user.
- /// </summary>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpGet("~/_/account/logout")]
- public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
- await _userService.LogOutUser(HttpContext);
- return Ok();
- }
-}
+ /// <summary>
+ /// Logout a user.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpGet("~/_/account/logout")]
+ public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) {
+ await _userService.LogOutUser(HttpContext);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs
index 31ff10b..02dc3f1 100644
--- a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs
+++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs
@@ -2,50 +2,49 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account;
public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdatePayload>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public UpdateAccountRoute(AppDbContext context) {
- _context = context;
- }
+ public UpdateAccountRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update the logged on user's data.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [HttpPost("~/_/account/update")]
- public override async Task<ActionResult> HandleAsync(UpdatePayload request, CancellationToken cancellationToken = default) {
- var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
- if (user == default) {
- await HttpContext.SignOutAsync();
- return Unauthorized();
- }
+ /// <summary>
+ /// Update the logged on user's data.
+ /// </summary>
+ /// <param name="request"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [HttpPost("~/_/account/update")]
+ public override async Task<ActionResult> HandleAsync(UpdatePayload request, CancellationToken cancellationToken = default) {
+ var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
+ await HttpContext.SignOutAsync();
+ return Unauthorized();
+ }
- if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) {
- return BadRequest(new KnownProblemModel("Invalid request", "No data was submitted"));
- }
+ if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) {
+ return BadRequest(new KnownProblemModel("Invalid request", "No data was submitted"));
+ }
- if (request.Password.HasValue() && request.Password.Length < 6) {
- return BadRequest(new KnownProblemModel("Invalid request",
- "The new password must contain at least 6 characters"));
- }
+ if (request.Password.HasValue() && request.Password.Length < 6) {
+ return BadRequest(new KnownProblemModel("Invalid request",
+ "The new password must contain at least 6 characters"));
+ }
- if (request.Password.HasValue()) {
- user.HashAndSetPassword(request.Password);
- }
+ if (request.Password.HasValue()) {
+ user.HashAndSetPassword(request.Password);
+ }
- if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) {
- return BadRequest(new KnownProblemModel("Invalid request",
- "The new username does not look like a valid email address"));
- }
+ if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) {
+ return BadRequest(new KnownProblemModel("Invalid request",
+ "The new username does not look like a valid email address"));
+ }
- if (request.Username.HasValue()) {
- user.Username = request.Username.Trim();
- }
+ if (request.Username.HasValue()) {
+ user.Username = request.Username.Trim();
+ }
- await _context.SaveChangesAsync(cancellationToken);
- return Ok();
- }
-}
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/RequestModel.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestPayload.cs
index 236c650..1adb344 100644
--- a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/RequestModel.cs
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestPayload.cs
@@ -1,6 +1,6 @@
-namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests.Create;
+namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
-public class RequestModel
+public class CreateResetRequestPayload
{
public string Username { get; set; }
} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/Route.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs
index f837fc0..bb72d38 100644
--- a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/Route.cs
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs
@@ -1,15 +1,15 @@
-namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests.Create;
+namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
-public class Route : RouteBaseAsync.WithRequest<RequestModel>.WithActionResult
+public class Route : RouteBaseAsync.WithRequest<CreateResetRequestPayload>.WithActionResult
{
private readonly ILogger<Route> _logger;
private readonly PasswordResetService _passwordResetService;
- private readonly AppDbContext _context;
-
- public Route(ILogger<Route> logger, PasswordResetService passwordResetService, AppDbContext context) {
+ private readonly MainAppDatabase _database;
+
+ public Route(ILogger<Route> logger, PasswordResetService passwordResetService, MainAppDatabase database) {
_logger = logger;
_passwordResetService = passwordResetService;
- _context = context;
+ _database = database;
}
/// <summary>
@@ -20,30 +20,19 @@ public class Route : RouteBaseAsync.WithRequest<RequestModel>.WithActionResult
/// <returns></returns>
[AllowAnonymous]
[HttpPost("~/_/password-reset-request/create")]
- public override async Task<ActionResult> HandleAsync(RequestModel request, CancellationToken cancellationToken = default) {
+ public override async Task<ActionResult> HandleAsync(CreateResetRequestPayload request, CancellationToken cancellationToken = default) {
if (!request.Username.IsValidEmailAddress()) {
_logger.LogInformation("Username is invalid, not doing request for password change");
return KnownProblem("Invalid email address", request.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"));
+ var tz = GetRequestTimeZone(_logger);
+ _logger.LogInformation("Creating forgot password request with local date time: " + tz.LocalDateTime.ToString("u"));
try {
- var user = _context.Users.SingleOrDefault(c => c.Username.Equals(request.Username));
+ var user = _database.Users.SingleOrDefault(c => c.Username.Equals(request.Username));
if (user != default) {
- await _passwordResetService.AddRequestAsync(user, tz, cancellationToken);
+ await _passwordResetService.AddRequestAsync(user, tz.TimeZoneInfo, cancellationToken);
return Ok();
}
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs
index a0ad4d0..6f71b2f 100644
--- a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs
@@ -1,34 +1,31 @@
-
namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
-/// <inheritdoc />
public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest<FulfillResetRequestPayload>.WithActionResult
{
- private readonly PasswordResetService _passwordResetService;
+ private readonly PasswordResetService _passwordResetService;
- /// <inheritdoc />
- public FulfillResetRequestRoute(PasswordResetService passwordResetService) {
- _passwordResetService = passwordResetService;
- }
+ public FulfillResetRequestRoute(PasswordResetService passwordResetService) {
+ _passwordResetService = passwordResetService;
+ }
- /// <summary>
- /// Fulfill a password reset request.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpPost("~/_/password-reset-request/fulfill")]
- public override async Task<ActionResult> 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();
- }
+ /// <summary>
+ /// Fulfill a password reset request.
+ /// </summary>
+ /// <param name="request"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpPost("~/_/password-reset-request/fulfill")]
+ public override async Task<ActionResult> 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;
- }
- }
-}
+ throw;
+ }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs
index 917c4f0..687cef6 100644
--- a/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs
+++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs
@@ -1,29 +1,27 @@
namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests;
-/// <inheritdoc />
public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest<Guid>.WithActionResult
{
- private readonly PasswordResetService _passwordResetService;
+ private readonly PasswordResetService _passwordResetService;
- /// <inheritdoc />
- public IsResetRequestValidRoute(PasswordResetService passwordResetService) {
- _passwordResetService = passwordResetService;
- }
+ public IsResetRequestValidRoute(PasswordResetService passwordResetService) {
+ _passwordResetService = passwordResetService;
+ }
- /// <summary>
- /// Check if a given password reset request is still valid.
- /// </summary>
- /// <param name="id"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
- [AllowAnonymous]
- [HttpGet("~/_/password-reset-request/is-valid")]
- public override async Task<ActionResult> HandleAsync(Guid id, CancellationToken cancellationToken = default) {
- var request = await _passwordResetService.GetRequestAsync(id, cancellationToken);
- if (request == default) {
- return NotFound();
- }
+ /// <summary>
+ /// Check if a given password reset request is still valid.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ [AllowAnonymous]
+ [HttpGet("~/_/password-reset-request/is-valid")]
+ public override async Task<ActionResult> HandleAsync(Guid id, CancellationToken cancellationToken = default) {
+ var request = await _passwordResetService.GetRequestAsync(id, cancellationToken);
+ if (request == default) {
+ return NotFound();
+ }
- return Ok(request.IsExpired == false);
- }
-}
+ return Ok(request.IsExpired == false);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs
index 5fb8213..34180d1 100644
--- a/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs
+++ b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs
@@ -2,20 +2,19 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
public class GetApplicationVersionRoute : RouteBaseSync.WithoutRequest.WithActionResult<string>
{
- private readonly IWebHostEnvironment _environment;
+ private readonly IWebHostEnvironment _environment;
- /// <inheritdoc />
- public GetApplicationVersionRoute(IWebHostEnvironment environment) {
- _environment = environment;
- }
+ public GetApplicationVersionRoute(IWebHostEnvironment environment) {
+ _environment = environment;
+ }
- /// <summary>
- /// Get the running api version number.
- /// </summary>
- /// <returns></returns>
- [HttpGet("~/_/version")]
- public override ActionResult<string> Handle() {
- var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt");
- return Ok(System.IO.File.ReadAllText(versionFilePath));
- }
-}
+ /// <summary>
+ /// Get the running api version number.
+ /// </summary>
+ /// <returns></returns>
+ [HttpGet("~/_/version")]
+ public override ActionResult<string> Handle() {
+ var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt");
+ return Ok(System.IO.File.ReadAllText(versionFilePath));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/LogRoute.cs b/code/api/src/Endpoints/Internal/Root/LogRoute.cs
index 48b497a..2c69e94 100644
--- a/code/api/src/Endpoints/Internal/Root/LogRoute.cs
+++ b/code/api/src/Endpoints/Internal/Root/LogRoute.cs
@@ -2,15 +2,15 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
public class LogRoute : RouteBaseSync.WithRequest<string>.WithoutResult
{
- private readonly ILogger<LogRoute> _logger;
+ private readonly ILogger<LogRoute> _logger;
- public LogRoute(ILogger<LogRoute> logger) {
- _logger = logger;
- }
+ public LogRoute(ILogger<LogRoute> logger) {
+ _logger = logger;
+ }
- [AllowAnonymous]
- [HttpPost("~/_/log")]
- public override void Handle([FromBody] string request) {
- _logger.LogInformation(request);
- }
-}
+ [AllowAnonymous]
+ [HttpPost("~/_/log")]
+ public override void Handle([FromBody] string request) {
+ _logger.LogInformation(request);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs
index e0dcca3..7270fd8 100644
--- a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs
+++ b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs
@@ -2,16 +2,16 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
public class ReadConfigurationRoute : RouteBaseSync.WithoutRequest.WithActionResult
{
- private readonly VaultService _vaultService;
+ private readonly VaultService _vaultService;
- public ReadConfigurationRoute(VaultService vaultService) {
- _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");
- }
-}
+ [AllowAnonymous]
+ [HttpGet("~/_/configuration")]
+ public override ActionResult Handle() {
+ var config = _vaultService.GetCurrentAppConfiguration();
+ return Content(JsonSerializer.Serialize(config.GetPublicVersion()), "application/json");
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs
index 4b1beec..a616c00 100644
--- a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs
+++ b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs
@@ -2,14 +2,14 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root;
public class RefreshConfigurationRoute : RouteBaseSync.WithoutRequest.WithoutResult
{
- private readonly VaultService _vaultService;
+ private readonly VaultService _vaultService;
- public RefreshConfigurationRoute(VaultService vaultService) {
- _vaultService = vaultService;
- }
+ public RefreshConfigurationRoute(VaultService vaultService) {
+ _vaultService = vaultService;
+ }
- [HttpGet("~/_/refresh-configuration")]
- public override void Handle() {
- _vaultService.RefreshCurrentAppConfiguration();
- }
-}
+ [HttpGet("~/_/refresh-configuration")]
+ public override void Handle() {
+ _vaultService.RefreshCurrentAppConfiguration();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
index 60b00ff..6bc2fdc 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
@@ -4,13 +4,12 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAccessTokenDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
private readonly AppConfiguration _configuration;
private readonly ILogger<CreateTokenRoute> _logger;
- public CreateTokenRoute(AppDbContext context, VaultService vaultService, ILogger<CreateTokenRoute> logger)
- {
- _context = context;
+ public CreateTokenRoute(MainAppDatabase database, VaultService vaultService, ILogger<CreateTokenRoute> logger) {
+ _database = database;
_configuration = vaultService.GetCurrentAppConfiguration();
_logger = logger;
}
@@ -24,24 +23,19 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce
[HttpPost("~/v{version:apiVersion}/api-tokens/create")]
[ProducesResponseType(200, Type = typeof(string))]
[ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request)
- {
- var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
- if (user == default)
- {
+ public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) {
+ var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
return NotFound(new KnownProblemModel("User does not exist"));
}
var token_entropy = _configuration.APP_AES_KEY;
- if (token_entropy.IsNullOrWhiteSpace())
- {
+ 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(),
+ var accessToken = new ApiAccessToken() {
User = user,
ExpiryDate = request.ExpiryDate.ToUniversalTime(),
AllowCreate = request.AllowCreate,
@@ -50,8 +44,8 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce
AllowUpdate = request.AllowUpdate
};
- _context.AccessTokens.Add(access_token);
- _context.SaveChanges();
- return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy))));
+ _database.AccessTokens.Add(accessToken);
+ _database.SaveChanges();
+ return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(token_entropy))));
}
-}
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
index a90b4c0..116a814 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
@@ -2,32 +2,32 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class DeleteTokenRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly ILogger<DeleteTokenRoute> _logger;
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<DeleteTokenRoute> _logger;
- public DeleteTokenRoute(AppDbContext context, ILogger<DeleteTokenRoute> logger) {
- _context = context;
- _logger = logger;
- }
+ public DeleteTokenRoute(MainAppDatabase database, ILogger<DeleteTokenRoute> logger) {
+ _database = database;
+ _logger = logger;
+ }
- /// <summary>
- /// Delete an api token, rendering it unusable
- /// </summary>
- /// <param name="id">Id of the token to delete</param>
- /// <returns>Nothing</returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")]
- [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();
- }
+ /// <summary>
+ /// Delete an api token, rendering it unusable
+ /// </summary>
+ /// <param name="id">Id of the token to delete</param>
+ /// <returns>Nothing</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public override ActionResult Handle(Guid id) {
+ var token = _database.AccessTokens.SingleOrDefault(c => c.Id == id);
+ if (token == default) {
+ _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received.");
+ return NotFound();
+ }
- _context.AccessTokens.Remove(token);
- _context.SaveChanges();
- return Ok();
- }
-}
+ _database.AccessTokens.Remove(token);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
index 59fd077..19790e4 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
@@ -2,21 +2,21 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult<ActionResult<List<ApiAccessToken.ApiAccessTokenDto>>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public GetTokensRoute(AppDbContext context) {
- _context = context;
- }
+ public GetTokensRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get all tokens, both active and inactive.
- /// </summary>
- /// <returns>A list of tokens</returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [HttpGet("~/v{version:apiVersion}/api-tokens")]
- [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))]
- [ProducesResponseType(204)]
- public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() {
- return Ok(_context.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto));
- }
-}
+ /// <summary>
+ /// Get all tokens, both active and inactive.
+ /// </summary>
+ /// <returns>A list of tokens</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpGet("~/v{version:apiVersion}/api-tokens")]
+ [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))]
+ [ProducesResponseType(204)]
+ public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() {
+ return Ok(_database.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
index fac2b5e..0471637 100644
--- a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
@@ -2,42 +2,43 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class CreateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult<TimeCategory.TimeCategoryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a new time entry category.
- /// </summary>
- /// <param name="categoryTimeCategoryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [HttpPost("~/v{version:apiVersion}/categories/create")]
- [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))]
- public override ActionResult<TimeCategory.TimeCategoryDto> 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);
- }
- }
+ /// <summary>
+ /// Create a new time entry category.
+ /// </summary>
+ /// <param name="categoryTimeCategoryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [HttpPost("~/v{version:apiVersion}/categories/create")]
+ [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))]
+ public override ActionResult<TimeCategory.TimeCategoryDto> Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) {
+ var duplicate = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim());
+ if (duplicate) {
+ var category = _database.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
- };
+ var newCategory = new TimeCategory(LoggedInUser) {
+ Name = categoryTimeCategoryDto.Name.Trim(),
+ Color = categoryTimeCategoryDto.Color
+ };
+ newCategory.SetOwnerIds(LoggedInUser.Id);
- _context.TimeCategories.Add(newCategory);
- _context.SaveChanges();
- categoryTimeCategoryDto.Id = newCategory.Id;
- return Ok(categoryTimeCategoryDto);
- }
-}
+ _database.TimeCategories.Add(newCategory);
+ _database.SaveChanges();
+ categoryTimeCategoryDto.Id = newCategory.Id;
+ return Ok(categoryTimeCategoryDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
index 3d438a0..582ec7d 100644
--- a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
@@ -2,37 +2,37 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class DeleteCategoryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public DeleteCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public DeleteCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry category.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [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);
+ /// <summary>
+ /// Delete a time entry category.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [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 = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == id);
- if (category == default) {
- return NotFound();
- }
+ 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();
- }
-}
+ var entries = _database.TimeEntries
+ .Include(c => c.Category)
+ .Where(c => c.Category.Id == category.Id);
+ _database.TimeEntries.RemoveRange(entries);
+ _database.TimeCategories.Remove(category);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
index a40a832..937f8e3 100644
--- a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
@@ -1,35 +1,33 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
-/// <inheritdoc />
public class GetCategoriesRoute : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeCategory.TimeCategoryDto>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public GetCategoriesRoute(AppDbContext context) {
- _context = context;
- }
+ public GetCategoriesRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a minimal list of time entry categories.
- /// </summary>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))]
- [ProducesResponseType(204)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpGet("~/v{version:apiVersion}/categories")]
- public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() {
- var categories = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .OrderByDescending(c => c.CreatedAt)
- .Select(c => c.AsDto)
- .ToList();
+ /// <summary>
+ /// Get a minimal list of time entry categories.
+ /// </summary>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))]
+ [ProducesResponseType(204)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpGet("~/v{version:apiVersion}/categories")]
+ public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() {
+ var categories = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .OrderByDescending(c => c.CreatedAt)
+ .Select(c => c.AsDto)
+ .ToList();
- if (categories.Count == 0) {
- return NoContent();
- }
+ if (categories.Count == 0) {
+ return NoContent();
+ }
- return Ok(categories);
- }
-}
+ return Ok(categories);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
index ca7dfdf..096132d 100644
--- a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
@@ -2,38 +2,38 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class UpdateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public UpdateCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public UpdateCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry category.
- /// </summary>
- /// <param name="categoryTimeCategoryDto"></param>
- /// <returns></returns>
- [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();
- }
+ /// <summary>
+ /// Update a time entry category.
+ /// </summary>
+ /// <param name="categoryTimeCategoryDto"></param>
+ /// <returns></returns>
+ [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 = _database.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();
- }
+ if (LoggedInUser.Id != category.UserId) {
+ return Forbid();
+ }
- category.Name = categoryTimeCategoryDto.Name;
- category.Color = categoryTimeCategoryDto.Color;
- _context.SaveChanges();
- return Ok();
- }
-}
+ category.Name = categoryTimeCategoryDto.Name;
+ category.Color = categoryTimeCategoryDto.Color;
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
new file mode 100644
index 0000000..eb69f7f
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
@@ -0,0 +1,63 @@
+using Microsoft.Extensions.Localization;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Customers;
+
+public class CreateCustomerRoute : RouteBaseAsync.WithRequest<CreateCustomerPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<CreateCustomerRoute> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateCustomerRoute(MainAppDatabase database, ILogger<CreateCustomerRoute> logger, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _logger = logger;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/customers/create")]
+ public override async Task<ActionResult> HandleAsync(CreateCustomerPayload request, CancellationToken cancellationToken = default) {
+ var errors = new Dictionary<string, string>();
+ if (request.Name.Trim().IsNullOrEmpty()) errors.Add("name", _localizer["Name is a required field"]);
+ if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors);
+ var customer = new Customer(LoggedInUser) {
+ CustomerNumber = request.CustomerNumber,
+ Name = request.Name,
+ Description = request.Description,
+ Address1 = request.Address1,
+ Address2 = request.Address2,
+ Country = request.Country,
+ Currency = request.Currency,
+ Email = request.Email,
+ Phone = request.Phone,
+ PostalCity = request.PostalCity,
+ PostalCode = request.PostalCode,
+ VATNumber = request.VATNumber,
+ ORGNumber = request.ORGNumber,
+ DefaultReference = request.DefaultReference,
+ Website = request.Website
+ };
+ customer.SetOwnerIds(default, LoggedInUser.TenantId);
+ _database.Customers.Add(customer);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateCustomerPayload
+{
+ public string CustomerNumber { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Address1 { get; set; }
+ public string Address2 { get; set; }
+ public string PostalCode { get; set; }
+ public string PostalCity { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Email { get; set; }
+ public string VATNumber { get; set; }
+ public string ORGNumber { get; set; }
+ public string DefaultReference { get; set; }
+ public string Website { get; set; }
+ public string Currency { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
index 854ff59..45d8f32 100644
--- a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
@@ -2,65 +2,66 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class CreateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a time entry.
- /// </summary>
- /// <param name="timeEntryTimeEntryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [ProducesResponseType(200)]
- [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- [HttpPost("~/v{version:apiVersion}/entries/create")]
- public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
- if (timeEntryTimeEntryDto.Stop == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required"));
- }
+ /// <summary>
+ /// Create a time entry.
+ /// </summary>
+ /// <param name="timeEntryTimeEntryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
+ [HttpPost("~/v{version:apiVersion}/entries/create")]
+ public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
+ if (timeEntryTimeEntryDto.Stop == default) {
+ return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required"));
+ }
- if (timeEntryTimeEntryDto.Start == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A start date is required"));
- }
+ if (timeEntryTimeEntryDto.Start == default) {
+ return BadRequest(new KnownProblemModel("Invalid form", "A start date is required"));
+ }
- if (timeEntryTimeEntryDto.Category == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A category is required"));
- }
+ if (timeEntryTimeEntryDto.Category == default) {
+ return BadRequest(new KnownProblemModel("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 KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
- }
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
- var entry = new TimeEntry(LoggedInUser.Id) {
- Category = category,
- Start = timeEntryTimeEntryDto.Start.ToUniversalTime(),
- Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(),
- Description = timeEntryTimeEntryDto.Description,
- };
+ if (category == default) {
+ return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
+ }
- 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 KnownProblemModel("Not found", "Could not find all of the specified labels"));
- }
+ var entry = new TimeEntry(LoggedInUser) {
+ Category = category,
+ Start = timeEntryTimeEntryDto.Start.ToUniversalTime(),
+ Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(),
+ Description = timeEntryTimeEntryDto.Description,
+ };
+ entry.SetOwnerIds(LoggedInUser.Id);
- entry.Labels = labels;
- }
+ if (timeEntryTimeEntryDto.Labels?.Count > 0) {
+ var labels = _database.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 KnownProblemModel("Not found", "Could not find all of the specified labels"));
+ }
- _context.TimeEntries.Add(entry);
- _context.SaveChanges();
- return Ok(entry.AsDto);
- }
-}
+ entry.Labels = labels;
+ }
+
+ _database.TimeEntries.Add(entry);
+ _database.SaveChanges();
+ return Ok(entry.AsDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
index 0850af0..c488ed5 100644
--- a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
@@ -1,35 +1,33 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
-/// <inheritdoc />
public class DeleteEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public DeleteEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public DeleteEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [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();
- }
+ /// <summary>
+ /// Delete a time entry.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [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 = _database.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();
- }
-}
+ _database.TimeEntries.Remove(entry);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
index ec6003c..03a64d2 100644
--- a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
@@ -2,185 +2,185 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class EntryQueryRoute : RouteBaseSync.WithRequest<EntryQueryPayload>.WithActionResult<EntryQueryResponse>
{
- private readonly ILogger<EntryQueryRoute> _logger;
- private readonly AppDbContext _context;
+ private readonly ILogger<EntryQueryRoute> _logger;
+ private readonly MainAppDatabase _database;
- public EntryQueryRoute(ILogger<EntryQueryRoute> logger, AppDbContext context) {
- _logger = logger;
- _context = context;
- }
+ public EntryQueryRoute(ILogger<EntryQueryRoute> logger, MainAppDatabase database) {
+ _logger = logger;
+ _database = database;
+ }
- /// <summary>
- /// Get a list of entries based on a given query.
- /// </summary>
- /// <param name="entryQuery"></param>
- /// <returns></returns>
- /// <exception cref="ArgumentOutOfRangeException"></exception>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpPost("~/v{version:apiVersion}/entries/query")]
- [ProducesResponseType(204)]
- [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(200, Type = typeof(EntryQueryResponse))]
- public override ActionResult<EntryQueryResponse> Handle(EntryQueryPayload entryQuery) {
- var result = new TimeQueryDto();
+ /// <summary>
+ /// Get a list of entries based on a given query.
+ /// </summary>
+ /// <param name="entryQuery"></param>
+ /// <returns></returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpPost("~/v{version:apiVersion}/entries/query")]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(200, Type = typeof(EntryQueryResponse))]
+ public override ActionResult<EntryQueryResponse> 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;
+ 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++;
- }
+ // 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"));
+ _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;
- }
+ var skipCount = 0;
+ if (entryQuery.Page > 1) {
+ skipCount = entryQuery.PageSize * entryQuery.Page;
+ }
- result.Page = entryQuery.Page;
- result.PageSize = entryQuery.PageSize;
+ 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);
+ var baseQuery = _database.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();
+ 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();
- }
+ if (baseTodaysEntriesCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseTodaysEntriesCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / entryQuery.PageSize));
+ result.TotalSize = baseTodaysEntriesCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseTodaysEntriesCount / entryQuery.PageSize));
- var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(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);
+ 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 baseEntriesThisWeek = baseQuery
+ .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date);
- var baseEntriesThisWeekCount = baseEntriesThisWeek.Count();
+ var baseEntriesThisWeekCount = baseEntriesThisWeek.Count();
- if (baseEntriesThisWeekCount == 0) {
- return NoContent();
- }
+ if (baseEntriesThisWeekCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesThisWeekCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesThisWeekCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisWeekCount / entryQuery.PageSize));
- var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(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.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));
+ result.TotalSize = baseEntriesThisMonthCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisMonthCount / entryQuery.PageSize));
- var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(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);
+ 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();
- }
+ var baseEntriesThisYearCount = baseEntriesThisYear.Count();
+ if (baseEntriesThisYearCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesThisYearCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesThisYearCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisYearCount / entryQuery.PageSize));
- var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(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();
+ 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();
- }
+ if (baseEntriesOnThisDateCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesOnThisDateCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesOnThisDateCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesOnThisDateCount / entryQuery.PageSize));
- var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(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 KnownProblemModel("Invalid query", "From date cannot be empty"));
- }
+ result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.DATE_RANGE:
+ if (entryQuery.DateRange.From == default) {
+ return BadRequest(new KnownProblemModel("Invalid query", "From date cannot be empty"));
+ }
- var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc);
+ var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc);
- if (entryQuery.DateRange.To == default) {
- return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty"));
- }
+ if (entryQuery.DateRange.To == default) {
+ return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty"));
+ }
- var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc);
+ var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc);
- if (DateTime.Compare(fromDate, toDate) > 0) {
- return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be less than From date"));
- }
+ if (DateTime.Compare(fromDate, toDate) > 0) {
+ return BadRequest(new KnownProblemModel("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 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();
- }
+ var baseDateRangeEntriesCount = baseDateRangeEntries.Count();
+ if (baseDateRangeEntriesCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseDateRangeEntriesCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / entryQuery.PageSize));
+ result.TotalSize = baseDateRangeEntriesCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseDateRangeEntriesCount / entryQuery.PageSize));
- var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(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");
- }
+ 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;
- }
+ if (result.Results.Any() && result.Page == 0) {
+ result.Page = 1;
+ result.TotalPageCount = 1;
+ }
- return Ok(result);
- }
-}
+ return Ok(result);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
index 87038db..6e064c7 100644
--- a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
@@ -2,33 +2,33 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class GetEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public GetEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public GetEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a spesific time entry.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [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<TimeEntry.TimeEntryDto> 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);
+ /// <summary>
+ /// Get a spesific time entry.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [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<TimeEntry.TimeEntryDto> Handle(Guid id) {
+ var entry = _database.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();
- }
+ if (entry == default) {
+ return NotFound();
+ }
- return Ok(entry);
- }
-}
+ return Ok(entry);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
index 09e3b9c..2254214 100644
--- a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
@@ -2,65 +2,65 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class UpdateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public UpdateEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public UpdateEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry.
- /// </summary>
- /// <param name="timeEntryTimeEntryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
- [HttpPost("~/v{version:apiVersion}/entries/update")]
- [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
- public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
- var entry = _context.TimeEntries
- .Where(c => c.UserId == LoggedInUser.Id)
- .Include(c => c.Labels)
- .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id);
+ /// <summary>
+ /// Update a time entry.
+ /// </summary>
+ /// <param name="timeEntryTimeEntryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
+ [HttpPost("~/v{version:apiVersion}/entries/update")]
+ [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
+ public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
+ var entry = _database.TimeEntries
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Include(c => c.Labels)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id);
- if (entry == default) {
- return NotFound();
- }
+ 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 KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
- }
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
+ if (category == default) {
+ return NotFound(new KnownProblemModel("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;
+ 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<TimeLabel>();
+ if (timeEntryTimeEntryDto.Labels?.Count > 0) {
+ var labels = new List<TimeLabel>();
- foreach (var labelDto in timeEntryTimeEntryDto.Labels) {
- var label = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == labelDto.Id);
+ foreach (var labelDto in timeEntryTimeEntryDto.Labels) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == labelDto.Id);
- if (label == default) {
- continue;
- }
+ if (label == default) {
+ continue;
+ }
- labels.Add(label);
- }
+ labels.Add(label);
+ }
- entry.Labels = labels;
- } else {
- entry.Labels = default;
- }
+ entry.Labels = labels;
+ } else {
+ entry.Labels = default;
+ }
- _context.SaveChanges();
- return Ok(entry.AsDto);
- }
-}
+ _database.SaveChanges();
+ return Ok(entry.AsDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
index 4fe418b..d6106aa 100644
--- a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
@@ -1,44 +1,44 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
public class CreateLabelRoute : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult<TimeLabel.TimeLabelDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateLabelRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateLabelRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a time entry label.
- /// </summary>
- /// <param name="labelTimeLabelDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [HttpPost("~/v{version:apiVersion}/labels/create")]
- public override ActionResult<TimeLabel.TimeLabelDto> 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());
+ /// <summary>
+ /// Create a time entry label.
+ /// </summary>
+ /// <param name="labelTimeLabelDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [HttpPost("~/v{version:apiVersion}/labels/create")]
+ public override ActionResult<TimeLabel.TimeLabelDto> Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
+ var duplicate = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
+ if (duplicate) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
- if (label != default) {
- return Ok(label.AsDto);
- }
- }
+ if (label != default) {
+ return Ok(label.AsDto);
+ }
+ }
- var newLabel = new TimeLabel(LoggedInUser.Id) {
- Name = labelTimeLabelDto.Name.Trim(),
- Color = labelTimeLabelDto.Color
- };
+ var newLabel = new TimeLabel(LoggedInUser) {
+ Name = labelTimeLabelDto.Name.Trim(),
+ Color = labelTimeLabelDto.Color
+ };
+ newLabel.SetOwnerIds(LoggedInUser.Id);
- _context.TimeLabels.Add(newLabel);
- _context.SaveChanges();
- labelTimeLabelDto.Id = newLabel.Id;
- return Ok(labelTimeLabelDto);
- }
-}
+ _database.TimeLabels.Add(newLabel);
+ _database.SaveChanges();
+ labelTimeLabelDto.Id = newLabel.Id;
+ return Ok(labelTimeLabelDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
index d845a6f..1baf4ef 100644
--- a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
@@ -1,35 +1,32 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class DeleteLabelEndpoint : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public DeleteLabelEndpoint(AppDbContext context) {
- _context = context;
- }
+ public DeleteLabelEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry label.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [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);
+ /// <summary>
+ /// Delete a time entry label.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [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 = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == id);
- if (label == default) {
- return NotFound();
- }
+ if (label == default) {
+ return NotFound();
+ }
- _context.TimeLabels.Remove(label);
- _context.SaveChanges();
- return Ok();
- }
-}
+ _database.TimeLabels.Remove(label);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
index c9ccef3..09d453b 100644
--- a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
@@ -1,34 +1,31 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class GetEndpoint : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeLabel.TimeLabelDto>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public GetEndpoint(AppDbContext context) {
- _context = context;
- }
+ public GetEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a minimal list of time entry labels.
- /// </summary>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpGet("~/v{version:apiVersion}/labels")]
- public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() {
- var labels = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .OrderByDescending(c => c.CreatedAt)
- .Select(c => c.AsDto)
- .ToList();
+ /// <summary>
+ /// Get a minimal list of time entry labels.
+ /// </summary>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpGet("~/v{version:apiVersion}/labels")]
+ public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() {
+ var labels = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .OrderByDescending(c => c.CreatedAt)
+ .Select(c => c.AsDto)
+ .ToList();
- if (labels.Count == 0) {
- return NoContent();
- }
+ if (labels.Count == 0) {
+ return NoContent();
+ }
- return Ok(labels);
- }
-}
+ return Ok(labels);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
index 30d72ec..9857b7d 100644
--- a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
@@ -1,38 +1,36 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class UpdateLabelEndpoint : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public UpdateLabelEndpoint(AppDbContext context) {
- _context = context;
- }
+ public UpdateLabelEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry label.
- /// </summary>
- /// <param name="labelTimeLabelDto"></param>
- /// <returns></returns>
- [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();
- }
+ /// <summary>
+ /// Update a time entry label.
+ /// </summary>
+ /// <param name="labelTimeLabelDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
+ [HttpPost("~/v{version:apiVersion}/labels/update")]
+ public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
+ var label = _database.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();
- }
+ if (LoggedInUser.Id != label.UserId) {
+ return Forbid();
+ }
- label.Name = labelTimeLabelDto.Name;
- label.Color = labelTimeLabelDto.Color;
- _context.SaveChanges();
- return Ok();
- }
-}
+ label.Name = labelTimeLabelDto.Name;
+ label.Color = labelTimeLabelDto.Color;
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
new file mode 100644
index 0000000..5c78e27
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
@@ -0,0 +1,80 @@
+using Microsoft.Extensions.Localization;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class CreateProjectRoute : RouteBaseAsync.WithRequest<CreateProjectPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateProjectRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/projects/create")]
+ public override async Task<ActionResult> HandleAsync(CreateProjectPayload request, CancellationToken cancellationToken = default) {
+ var errors = new Dictionary<string, string>();
+
+ if (request.Name.IsNullOrEmpty()) {
+ errors.Add("name", _localizer["Name is a required field"]);
+ }
+
+ var project = new Project(LoggedInUser) {
+ Name = request.Name,
+ Description = request.Description,
+ Start = request.Start,
+ Stop = request.Stop,
+ };
+ project.SetOwnerIds(default, LoggedInUser.TenantId);
+
+ foreach (var customerId in request.CustomerIds) {
+ var customer = _database.Customers.FirstOrDefault(c => c.Id == customerId);
+ if (customer == default) {
+ errors.Add("customer_" + customerId, _localizer["Customer not found"]);
+ continue;
+ }
+
+ project.Customers.Add(customer);
+ }
+
+ foreach (var member in request.Members) {
+ var user = _database.Users.FirstOrDefault(c => c.Id == member.UserId);
+ if (user == default) {
+ errors.Add("members_" + member.UserId, _localizer["User not found"]);
+ continue;
+ }
+
+ project.Members.Add(new ProjectMember() {
+ Project = project,
+ User = user,
+ Role = member.Role
+ });
+ }
+
+ if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors);
+
+ _database.Projects.Add(project);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateProjectResponse
+{ }
+
+public class CreateProjectPayload
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public DateTime Start { get; set; }
+ public DateTime Stop { get; set; }
+ public List<Guid> CustomerIds { get; set; }
+ public List<ProjectMember> Members { get; set; }
+
+ public class ProjectMember
+ {
+ public Guid UserId { get; set; }
+ public ProjectRole Role { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
new file mode 100644
index 0000000..d60f974
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
@@ -0,0 +1,43 @@
+using MR.AspNetCore.Pagination;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class GetProjectsRoute : RouteBaseAsync.WithRequest<GetProjectsQueryParameters>.WithActionResult<KeysetPaginationResult<GetProjectsResponseDto>>
+{
+ private readonly MainAppDatabase _database;
+ private readonly PaginationService _pagination;
+
+ public GetProjectsRoute(MainAppDatabase database, PaginationService pagination) {
+ _database = database;
+ _pagination = pagination;
+ }
+
+ [HttpGet("~/v{version:apiVersion}/projects")]
+ public override async Task<ActionResult<KeysetPaginationResult<GetProjectsResponseDto>>> HandleAsync(GetProjectsQueryParameters request, CancellationToken cancellationToken = default) {
+ var result = await _pagination.KeysetPaginateAsync(
+ _database.Projects.ForTenant(LoggedInUser),
+ b => b.Descending(x => x.CreatedAt),
+ async id => await _database.Projects.FindAsync(id),
+ query => query.Select(p => new GetProjectsResponseDto() {
+ Id = p.Id,
+ Name = p.Name
+ })
+ );
+ return Ok(result);
+ }
+}
+
+public class GetProjectsResponseDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+}
+
+public class GetProjectsQueryParameters
+{
+ [FromQuery]
+ public int Page { get; set; }
+
+ [FromQuery]
+ public int Size { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/_calls.http b/code/api/src/Endpoints/V1/Projects/_calls.http
new file mode 100644
index 0000000..af0eba6
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/_calls.http
@@ -0,0 +1,12 @@
+### Create Project
+GET https://localhost:5001/v1/projects/create
+Accept: application/json
+Content-Type: application/json
+Cookie: ""
+
+{
+ "": ""
+}
+
+### Get Projects
+POST http://localhost
diff --git a/code/api/src/Endpoints/V1/RouteBaseAsync.cs b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
index a75e9da..33b6f5f 100644
--- a/code/api/src/Endpoints/V1/RouteBaseAsync.cs
+++ b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
@@ -3,9 +3,9 @@ namespace IOL.GreatOffice.Api.Endpoints.V1;
/// <summary>
/// A base class for an endpoint that accepts parameters.
/// </summary>
-public static class RouteBaseAsync
+public class RouteBaseAsync
{
- public static class WithRequest<TRequest>
+ public class WithRequest<TRequest>
{
public abstract class WithResult<TResponse> : V1_EndpointBase
{
diff --git a/code/api/src/IOL.GreatOffice.Api.csproj b/code/api/src/IOL.GreatOffice.Api.csproj
index 1a3c97e..a5154db 100644
--- a/code/api/src/IOL.GreatOffice.Api.csproj
+++ b/code/api/src/IOL.GreatOffice.Api.csproj
@@ -21,6 +21,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+ <PackageReference Include="MR.AspNetCore.Pagination" Version="1.0.1" />
+ <PackageReference Include="MR.AspNetCore.Pagination.Swashbuckle" Version="1.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
@@ -38,19 +40,33 @@
<Content Include="..\build_and_push.sh">
<Link>build_and_push.sh</Link>
</Content>
- <Content Include="..\CHANGELOG.md">
- <Link>CHANGELOG.md</Link>
- </Content>
- <Content Include="..\cliff.toml">
- <Link>cliff.toml</Link>
- </Content>
<Content Include="..\Dockerfile">
<Link>Dockerfile</Link>
</Content>
</ItemGroup>
<ItemGroup>
- <Folder Include="Resources" />
+ <EmbeddedResource Update="Resources\SharedResources.en.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SharedResources.en.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\SharedResources.nb.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SharedResources.nb.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="Resources\SharedResources.en.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>SharedResource.en.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\SharedResources.nb.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>SharedResources.nb.resx</DependentUpon>
+ </Compile>
</ItemGroup>
</Project>
diff --git a/code/api/src/Jobs/TokenCleanupJob.cs b/code/api/src/Jobs/TokenCleanupJob.cs
index fce40c9..04ccf23 100644
--- a/code/api/src/Jobs/TokenCleanupJob.cs
+++ b/code/api/src/Jobs/TokenCleanupJob.cs
@@ -5,9 +5,9 @@ namespace IOL.GreatOffice.Api.Jobs;
public class TokenCleanupJob : IJob
{
private readonly ILogger<TokenCleanupJob> _logger;
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _context;
- public TokenCleanupJob(ILogger<TokenCleanupJob> logger, AppDbContext context) {
+ public TokenCleanupJob(ILogger<TokenCleanupJob> logger, MainAppDatabase context) {
_logger = logger;
_context = context;
}
diff --git a/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs b/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs
index b6a01ff..da3c3ec 100644
--- a/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs
+++ b/code/api/src/Migrations/20210517202115_InitialMigration.Designer.cs
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20210517202115_InitialMigration")]
partial class InitialMigration
{
diff --git a/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs
index 368e6b3..28408c7 100644
--- a/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs
+++ b/code/api/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20210522165932_RenameNoteToDescription")]
partial class RenameNoteToDescription
{
diff --git a/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs b/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs
index 59e6112..50f461e 100644
--- a/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs
+++ b/code/api/src/Migrations/20211002113037_V6Migration.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20211002113037_V6Migration")]
partial class V6Migration
{
diff --git a/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs b/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs
index 2b95f9d..6c59262 100644
--- a/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs
+++ b/code/api/src/Migrations/20220225143559_GithubUserMappings.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220225143559_GithubUserMappings")]
partial class GithubUserMappings
{
diff --git a/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs b/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs
index 3d57f1a..2f96cdc 100644
--- a/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs
+++ b/code/api/src/Migrations/20220319135910_RenameCreated.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220319135910_RenameCreated")]
partial class RenameCreated
{
diff --git a/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs b/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs
index f75400e..349c4ad 100644
--- a/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs
+++ b/code/api/src/Migrations/20220319144958_ModifiedAt.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220319144958_ModifiedAt")]
partial class ModifiedAt
{
diff --git a/code/api/src/Migrations/20220319203018_UserBase.Designer.cs b/code/api/src/Migrations/20220319203018_UserBase.Designer.cs
index 6c7a76f..c918d3d 100644
--- a/code/api/src/Migrations/20220319203018_UserBase.Designer.cs
+++ b/code/api/src/Migrations/20220319203018_UserBase.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220319203018_UserBase")]
partial class UserBase
{
diff --git a/code/api/src/Migrations/20220320115601_Update1.Designer.cs b/code/api/src/Migrations/20220320115601_Update1.Designer.cs
index c7463fb..3b6a63a 100644
--- a/code/api/src/Migrations/20220320115601_Update1.Designer.cs
+++ b/code/api/src/Migrations/20220320115601_Update1.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220320115601_Update1")]
partial class Update1
{
diff --git a/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs
index 3a18463..8dae7c8 100644
--- a/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs
+++ b/code/api/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs
@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220320132220_UpdatedForgotPasswordRequests")]
partial class UpdatedForgotPasswordRequests
{
diff --git a/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs b/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs
index 74f9b40..3dc01a7 100644
--- a/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs
+++ b/code/api/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220529190359_ApiAccessTokens")]
partial class ApiAccessTokens
{
diff --git a/code/api/src/Migrations/20220530174741_Tenants.Designer.cs b/code/api/src/Migrations/20220530174741_Tenants.Designer.cs
index 678c52d..aadbec9 100644
--- a/code/api/src/Migrations/20220530174741_Tenants.Designer.cs
+++ b/code/api/src/Migrations/20220530174741_Tenants.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220530174741_Tenants")]
partial class Tenants
{
diff --git a/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs
index 8fd6b40..f87309f 100644
--- a/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs
+++ b/code/api/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220530175322_RemoveUnusedNavs")]
partial class RemoveUnusedNavs
{
diff --git a/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs
index a05b0e4..ddbe9d2 100644
--- a/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs
+++ b/code/api/src/Migrations/20220602214238_NullableOptionalBaseFields.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220602214238_NullableOptionalBaseFields")]
partial class NullableOptionalBaseFields
{
diff --git a/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs b/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs
index 69d4f7e..5c4d3a3 100644
--- a/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs
+++ b/code/api/src/Migrations/20220606232346_FleshOutNewModules.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220606232346_FleshOutNewModules")]
partial class FleshOutNewModules
{
diff --git a/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs b/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs
index b333f23..b89e68d 100644
--- a/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs
+++ b/code/api/src/Migrations/20220616170311_DataProtectionKeys.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220616170311_DataProtectionKeys")]
partial class DataProtectionKeys
{
diff --git a/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs
index 33b5cfd..0ba742c 100644
--- a/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs
+++ b/code/api/src/Migrations/20220819203816_RemoveGithubUsers.Designer.cs
@@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
[Migration("20220819203816_RemoveGithubUsers")]
partial class RemoveGithubUsers
{
diff --git a/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs
new file mode 100644
index 0000000..284ea89
--- /dev/null
+++ b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.Designer.cs
@@ -0,0 +1,1074 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030080515_InitialProjectAndCustomer")]
+ partial class InitialProjectAndCustomer
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_member");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_member_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_member_user_id");
+
+ b.ToTable("project_member", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_member_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_member_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs
new file mode 100644
index 0000000..8a856cb
--- /dev/null
+++ b/code/api/src/Migrations/20221030080515_InitialProjectAndCustomer.cs
@@ -0,0 +1,304 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class InitialProjectAndCustomer : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "customer_groups",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_groups", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "projects",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ start = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ stop = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_projects", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customers",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_number = table.Column<string>(type: "text", nullable: true),
+ name = table.Column<string>(type: "text", nullable: true),
+ description = table.Column<string>(type: "text", nullable: true),
+ address1 = table.Column<string>(type: "text", nullable: true),
+ address2 = table.Column<string>(type: "text", nullable: true),
+ postal_code = table.Column<string>(type: "text", nullable: true),
+ postal_city = table.Column<string>(type: "text", nullable: true),
+ country = table.Column<string>(type: "text", nullable: true),
+ phone = table.Column<string>(type: "text", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ vat_number = table.Column<string>(type: "text", nullable: true),
+ org_number = table.Column<string>(type: "text", nullable: true),
+ default_reference = table.Column<string>(type: "text", nullable: true),
+ website = table.Column<string>(type: "text", nullable: true),
+ currency = table.Column<string>(type: "text", nullable: true),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customers", x => x.id);
+ table.ForeignKey(
+ name: "fk_customers_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_customers_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "project_labels",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ value = table.Column<string>(type: "text", nullable: true),
+ color = table.Column<string>(type: "text", nullable: true),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_project_labels", x => x.id);
+ table.ForeignKey(
+ name: "fk_project_labels_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "project_member",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ project_id = table.Column<Guid>(type: "uuid", nullable: true),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ role = table.Column<int>(type: "integer", nullable: false),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_project_member", x => x.id);
+ table.ForeignKey(
+ name: "fk_project_member_projects_project_id",
+ column: x => x.project_id,
+ principalTable: "projects",
+ principalColumn: "id");
+ table.ForeignKey(
+ name: "fk_project_member_users_user_id",
+ column: x => x.user_id,
+ principalTable: "users",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_contacts",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_id = table.Column<Guid>(type: "uuid", nullable: true),
+ first_name = table.Column<string>(type: "text", nullable: true),
+ last_name = table.Column<string>(type: "text", nullable: true),
+ email = table.Column<string>(type: "text", nullable: true),
+ phone = table.Column<string>(type: "text", nullable: true),
+ work_title = table.Column<string>(type: "text", nullable: true),
+ note = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_contacts", x => x.id);
+ table.ForeignKey(
+ name: "fk_customer_contacts_customers_customer_id",
+ column: x => x.customer_id,
+ principalTable: "customers",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_customer_group",
+ columns: table => new
+ {
+ customers_id = table.Column<Guid>(type: "uuid", nullable: false),
+ groups_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_customer_group", x => new { x.customers_id, x.groups_id });
+ table.ForeignKey(
+ name: "fk_customer_customer_group_customer_groups_groups_id",
+ column: x => x.groups_id,
+ principalTable: "customer_groups",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_customer_customer_group_customers_customers_id",
+ column: x => x.customers_id,
+ principalTable: "customers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "customer_events",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ customer_id = table.Column<Guid>(type: "uuid", nullable: true),
+ title = table.Column<string>(type: "text", nullable: true),
+ note = table.Column<string>(type: "text", nullable: true),
+ created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
+ deleted = table.Column<bool>(type: "boolean", nullable: false),
+ user_id = table.Column<Guid>(type: "uuid", nullable: true),
+ tenant_id = table.Column<Guid>(type: "uuid", nullable: true),
+ modified_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ created_by_id = table.Column<Guid>(type: "uuid", nullable: true),
+ deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_events", x => x.id);
+ table.ForeignKey(
+ name: "fk_customer_events_customers_customer_id",
+ column: x => x.customer_id,
+ principalTable: "customers",
+ principalColumn: "id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_contacts_customer_id",
+ table: "customer_contacts",
+ column: "customer_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_customer_group_groups_id",
+ table: "customer_customer_group",
+ column: "groups_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_events_customer_id",
+ table: "customer_events",
+ column: "customer_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_project_id",
+ table: "customers",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_user_id",
+ table: "customers",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_labels_project_id",
+ table: "project_labels",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_member_project_id",
+ table: "project_member",
+ column: "project_id");
+
+ migrationBuilder.CreateIndex(
+ name: "ix_project_member_user_id",
+ table: "project_member",
+ column: "user_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "customer_contacts");
+
+ migrationBuilder.DropTable(
+ name: "customer_customer_group");
+
+ migrationBuilder.DropTable(
+ name: "customer_events");
+
+ migrationBuilder.DropTable(
+ name: "project_labels");
+
+ migrationBuilder.DropTable(
+ name: "project_member");
+
+ migrationBuilder.DropTable(
+ name: "customer_groups");
+
+ migrationBuilder.DropTable(
+ name: "customers");
+
+ migrationBuilder.DropTable(
+ name: "projects");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs b/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs
new file mode 100644
index 0000000..78f9454
--- /dev/null
+++ b/code/api/src/Migrations/20221030081459_DeletedAt.Designer.cs
@@ -0,0 +1,1126 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030081459_DeletedAt")]
+ partial class DeletedAt
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_member");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_member_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_member_user_id");
+
+ b.ToTable("project_member", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_member_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_member_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030081459_DeletedAt.cs b/code/api/src/Migrations/20221030081459_DeletedAt.cs
new file mode 100644
index 0000000..8132a6b
--- /dev/null
+++ b/code/api/src/Migrations/20221030081459_DeletedAt.cs
@@ -0,0 +1,146 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class DeletedAt : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_labels",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_entries",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "time_categories",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "tenants",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "projects",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "project_member",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "project_labels",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customers",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_groups",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_events",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "customer_contacts",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn<DateTime>(
+ name: "deleted_at",
+ table: "api_access_tokens",
+ type: "timestamp with time zone",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "users");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_entries");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "time_categories");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "tenants");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "projects");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "project_member");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "project_labels");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customers");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_groups");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_events");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "customer_contacts");
+
+ migrationBuilder.DropColumn(
+ name: "deleted_at",
+ table: "api_access_tokens");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs b/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs
new file mode 100644
index 0000000..1527d91
--- /dev/null
+++ b/code/api/src/Migrations/20221030084716_MinorChanges.Designer.cs
@@ -0,0 +1,1126 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030084716_MinorChanges")]
+ partial class MinorChanges
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_customers_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany("Customers")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_customers_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Customers");
+
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030084716_MinorChanges.cs b/code/api/src/Migrations/20221030084716_MinorChanges.cs
new file mode 100644
index 0000000..e708caf
--- /dev/null
+++ b/code/api/src/Migrations/20221030084716_MinorChanges.cs
@@ -0,0 +1,105 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class MinorChanges : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_member_projects_project_id",
+ table: "project_member");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_member_users_user_id",
+ table: "project_member");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_project_member",
+ table: "project_member");
+
+ migrationBuilder.RenameTable(
+ name: "project_member",
+ newName: "project_members");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_member_user_id",
+ table: "project_members",
+ newName: "ix_project_members_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_member_project_id",
+ table: "project_members",
+ newName: "ix_project_members_project_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_project_members",
+ table: "project_members",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_members_projects_project_id",
+ table: "project_members",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_members_users_user_id",
+ table: "project_members",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_members_projects_project_id",
+ table: "project_members");
+
+ migrationBuilder.DropForeignKey(
+ name: "fk_project_members_users_user_id",
+ table: "project_members");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "pk_project_members",
+ table: "project_members");
+
+ migrationBuilder.RenameTable(
+ name: "project_members",
+ newName: "project_member");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_members_user_id",
+ table: "project_member",
+ newName: "ix_project_member_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "ix_project_members_project_id",
+ table: "project_member",
+ newName: "ix_project_member_project_id");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "pk_project_member",
+ table: "project_member",
+ column: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_member_projects_project_id",
+ table: "project_member",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_project_member_users_user_id",
+ table: "project_member",
+ column: "user_id",
+ principalTable: "users",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs b/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs
new file mode 100644
index 0000000..ff1dfb2
--- /dev/null
+++ b/code/api/src/Migrations/20221030090557_MoreMinorChanges.Designer.cs
@@ -0,0 +1,1148 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221030090557_MoreMinorChanges")]
+ partial class MoreMinorChanges
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_forgot_password_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_forgot_password_requests_user_id");
+
+ b.ToTable("forgot_password_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_categories");
+
+ b.ToTable("time_categories", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("CategoryId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<DateTime>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_entries");
+
+ b.HasIndex("CategoryId")
+ .HasDatabaseName("ix_time_entries_category_id");
+
+ b.ToTable("time_entries", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TimeEntryId")
+ .HasColumnType("uuid")
+ .HasColumnName("time_entry_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_time_labels");
+
+ b.HasIndex("TimeEntryId")
+ .HasDatabaseName("ix_time_labels_time_entry_id");
+
+ b.ToTable("time_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.Property<Guid>("TenantsId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenants_id");
+
+ b.Property<Guid>("UsersId")
+ .HasColumnType("uuid")
+ .HasColumnName("users_id");
+
+ b.HasKey("TenantsId", "UsersId")
+ .HasName("pk_tenant_user");
+
+ b.HasIndex("UsersId")
+ .HasDatabaseName("ix_tenant_user_users_id");
+
+ b.ToTable("tenant_user", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_forgot_password_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
+ .WithMany()
+ .HasForeignKey("CategoryId")
+ .HasConstraintName("fk_time_entries_time_categories_category_id");
+
+ b.Navigation("Category");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null)
+ .WithMany("Labels")
+ .HasForeignKey("TimeEntryId")
+ .HasConstraintName("fk_time_labels_time_entries_time_entry_id");
+ });
+
+ modelBuilder.Entity("TenantUser", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_tenants_tenants_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", null)
+ .WithMany()
+ .HasForeignKey("UsersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_tenant_user_users_users_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
+ {
+ b.Navigation("Labels");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs b/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs
new file mode 100644
index 0000000..5ebd664
--- /dev/null
+++ b/code/api/src/Migrations/20221030090557_MoreMinorChanges.cs
@@ -0,0 +1,78 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ public partial class MoreMinorChanges : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "fk_customers_projects_project_id",
+ table: "customers");
+
+ migrationBuilder.DropIndex(
+ name: "ix_customers_project_id",
+ table: "customers");
+
+ migrationBuilder.DropColumn(
+ name: "project_id",
+ table: "customers");
+
+ migrationBuilder.CreateTable(
+ name: "customer_project",
+ columns: table => new
+ {
+ customers_id = table.Column<Guid>(type: "uuid", nullable: false),
+ projects_id = table.Column<Guid>(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_customer_project", x => new { x.customers_id, x.projects_id });
+ table.ForeignKey(
+ name: "fk_customer_project_customers_customers_id",
+ column: x => x.customers_id,
+ principalTable: "customers",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "fk_customer_project_projects_projects_id",
+ column: x => x.projects_id,
+ principalTable: "projects",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customer_project_projects_id",
+ table: "customer_project",
+ column: "projects_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "customer_project");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "project_id",
+ table: "customers",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "ix_customers_project_id",
+ table: "customers",
+ column: "project_id");
+
+ migrationBuilder.AddForeignKey(
+ name: "fk_customers_projects_project_id",
+ table: "customers",
+ column: "project_id",
+ principalTable: "projects",
+ principalColumn: "id");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/AppDbContextModelSnapshot.cs b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
index cc4bf72..7a534ad 100644
--- a/code/api/src/Migrations/AppDbContextModelSnapshot.cs
+++ b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
@@ -1,6 +1,6 @@
// <auto-generated />
using System;
-using IOL.GreatOffice.Api.Data;
+using IOL.GreatOffice.Api.Data.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -10,18 +10,56 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace IOL.GreatOffice.Api.Migrations
{
- [DbContext(typeof(AppDbContext))]
+ [DbContext(typeof(MainAppDatabase))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "6.0.7")
+ .HasAnnotation("ProductVersion", "6.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
{
b.Property<Guid>("Id")
@@ -53,6 +91,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<DateTime>("ExpiryDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiry_date");
@@ -74,6 +116,315 @@ namespace IOL.GreatOffice.Api.Migrations
b.ToTable("api_access_tokens", (string)null);
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
{
b.Property<Guid>("Id")
@@ -98,6 +449,182 @@ namespace IOL.GreatOffice.Api.Migrations
b.ToTable("forgot_password_requests", (string)null);
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedById")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by_id");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
{
b.Property<Guid>("Id")
@@ -121,6 +648,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<Guid?>("DeletedById")
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
@@ -186,6 +717,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<Guid?>("DeletedById")
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
@@ -239,6 +774,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<Guid?>("DeletedById")
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
@@ -303,6 +842,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<Guid?>("DeletedById")
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
@@ -355,6 +898,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("boolean")
.HasColumnName("deleted");
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
b.Property<string>("Email")
.HasColumnType("text")
.HasColumnName("email");
@@ -427,6 +974,40 @@ namespace IOL.GreatOffice.Api.Migrations
b.ToTable("tenant_user", (string)null);
});
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
{
b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
@@ -437,6 +1018,36 @@ namespace IOL.GreatOffice.Api.Migrations
b.Navigation("User");
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.Navigation("Customer");
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b =>
{
b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
@@ -449,6 +1060,33 @@ namespace IOL.GreatOffice.Api.Migrations
b.Navigation("User");
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
{
b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category")
@@ -484,6 +1122,20 @@ namespace IOL.GreatOffice.Api.Migrations
.HasConstraintName("fk_tenant_user_users_users_id");
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b =>
{
b.Navigation("Labels");
diff --git a/code/api/src/Program.cs b/code/api/src/Program.cs
index ebd686f..21c0a7d 100644
--- a/code/api/src/Program.cs
+++ b/code/api/src/Program.cs
@@ -98,7 +98,7 @@ public static class Program
builder.Services
.AddDataProtection()
.ProtectKeysWithCertificate(configuration.CERT1())
- .PersistKeysToDbContext<AppDbContext>();
+ .PersistKeysToDbContext<MainAppDatabase>();
builder.Services.Configure(JsonSettings.Default);
builder.Services.AddQuartz(options => {
@@ -129,7 +129,7 @@ public static class Program
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AppConstants.BASIC_AUTH_SCHEME, default);
- builder.Services.AddDbContext<AppDbContext>(options => {
+ builder.Services.AddDbContext<MainAppDatabase>(options => {
options.UseNpgsql(builder.Configuration.GetAppDatabaseConnectionString(vaultService.GetCurrentAppConfiguration),
npgsqlDbContextOptionsBuilder => {
npgsqlDbContextOptionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
@@ -150,6 +150,7 @@ public static class Program
builder.Services.AddSwaggerGen(options => {
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "IOL.GreatOffice.Api.xml"));
options.UseApiEndpoints();
+ options.ConfigurePagination();
options.OperationFilter<SwaggerDefaultValues>();
options.SwaggerDoc(ApiSpecV1.Document.VersionName, ApiSpecV1.Document.OpenApiInfo);
options.AddSecurityDefinition("Basic",
@@ -175,7 +176,11 @@ public static class Program
}
});
});
-
+ builder.Services.AddPagination(options => {
+ options.DefaultSize = 50;
+ options.MaxSize = 100;
+ options.CanChangeSizeFromQuery = true;
+ });
builder.Services
.AddControllers()
.AddDataAnnotationsLocalization()
diff --git a/code/api/src/Resources/SharedResources.cs b/code/api/src/Resources/SharedResources.cs
new file mode 100644
index 0000000..4b8ec2f
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.cs
@@ -0,0 +1,4 @@
+namespace IOL.GreatOffice.Api;
+
+public class SharedResources
+{ } \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.en.Designer.cs b/code/api/src/Resources/SharedResources.en.Designer.cs
new file mode 100644
index 0000000..4eb786f
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.en.Designer.cs
@@ -0,0 +1,78 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace IOL.GreatOffice.Api.Resources {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SharedResources_en {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SharedResources_en() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("IOL.GreatOffice.Api.Resources.SharedResources_en", typeof(SharedResources_en).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Name_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Name is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Start_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Start is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_form {
+ get {
+ return ResourceManager.GetString("Invalid form", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_validation_errors_occured {
+ get {
+ return ResourceManager.GetString("One or more validation errors occured", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_fields_is_invalid {
+ get {
+ return ResourceManager.GetString("One or more fields is invalid", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/code/api/src/Resources/SharedResources.en.resx b/code/api/src/Resources/SharedResources.en.resx
new file mode 100644
index 0000000..5987e8d
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.en.resx
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Name is a required field</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start is a required field</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Invalid form</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more validation errors occured" xml:space="preserve">
+ <value>One or more validation errors occured</value>
+ <comment>One or more validation errors occured</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <value>One or more fields is invalid</value>
+ <comment>One or more fields is invalid</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.nb.Designer.cs b/code/api/src/Resources/SharedResources.nb.Designer.cs
new file mode 100644
index 0000000..114c86f
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.nb.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace IOL.GreatOffice.Api.Resources {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SharedResources_nb {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SharedResources_nb() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("IOL.GreatOffice.Api.Resources.SharedResources_nb", typeof(SharedResources_nb).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static string Name_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Name is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Start_is_a_required_field {
+ get {
+ return ResourceManager.GetString("Start is a required field", resourceCulture);
+ }
+ }
+
+ internal static string Invalid_form {
+ get {
+ return ResourceManager.GetString("Invalid form", resourceCulture);
+ }
+ }
+
+ internal static string One_or_more_fields_is_invalid {
+ get {
+ return ResourceManager.GetString("One or more fields is invalid", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/code/api/src/Resources/SharedResources.nb.resx b/code/api/src/Resources/SharedResources.nb.resx
new file mode 100644
index 0000000..6c8937b
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.nb.resx
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:element name="root" msdata:IsDataSet="true">
+
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+ </value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
+ PublicKeyToken=b77a5c561934e089
+ </value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Navn er et påkrevd felt</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start er et påkrevd felt</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Ugyldig skjema</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <comment>One or more fields is invalid</comment>
+ <value>En eller flere felt er ugyldige</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Resources/SharedResources.resx b/code/api/src/Resources/SharedResources.resx
new file mode 100644
index 0000000..377994b
--- /dev/null
+++ b/code/api/src/Resources/SharedResources.resx
@@ -0,0 +1,30 @@
+<root>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>1.3</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Name is a required field" xml:space="preserve">
+ <value>Name is a required field</value>
+ <comment>Name is a required field</comment>
+ </data>
+ <data name="Start is a required field" xml:space="preserve">
+ <value>Start is a required field</value>
+ <comment>Start is a required field</comment>
+ </data>
+ <data name="Invalid form" xml:space="preserve">
+ <value>Invalid form</value>
+ <comment>Invalid form</comment>
+ </data>
+ <data name="One or more fields is invalid" xml:space="preserve">
+ <value>One or more fields is invalid</value>
+ <comment>One or more fields is invalid</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs
index c08cb84..b55b48f 100644
--- a/code/api/src/Services/MailService.cs
+++ b/code/api/src/Services/MailService.cs
@@ -41,7 +41,7 @@ public class MailService
new JsonSerializerOptions {
WriteIndented = true
});
-
+
_logger.LogDebug("SmtpClient was instansiated with the following configuration\n" + configurationString);
smtpClient.Send(message);
diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs
index 1b4f147..76eb2fe 100644
--- a/code/api/src/Services/PasswordResetService.cs
+++ b/code/api/src/Services/PasswordResetService.cs
@@ -2,26 +2,25 @@ namespace IOL.GreatOffice.Api.Services;
public class PasswordResetService
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
private readonly MailService _mailService;
private readonly AppConfiguration _configuration;
private readonly ILogger<PasswordResetService> _logger;
-
public PasswordResetService(
- AppDbContext context,
+ MainAppDatabase database,
VaultService vaultService,
ILogger<PasswordResetService> logger,
MailService mailService
) {
- _context = context;
+ _database = database;
_configuration = vaultService.GetCurrentAppConfiguration();
_logger = logger;
_mailService = mailService;
}
public async Task<ForgotPasswordRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) {
- var request = await _context.ForgotPasswordRequests
+ var request = await _database.ForgotPasswordRequests
.Include(c => c.User)
.SingleOrDefaultAsync(c => c.Id == id, cancellationToken);
if (request == default) {
@@ -31,21 +30,21 @@ public class PasswordResetService
_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<bool> 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);
+ var user = _database.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);
+ _database.Users.Update(user);
+ await _database.SaveChangesAsync(cancellationToken);
_logger.LogInformation($"Fullfilled password reset request for user: {request.User.Username}");
await DeleteRequestsForUserAsync(user.Id, cancellationToken);
return true;
@@ -55,8 +54,8 @@ public class PasswordResetService
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);
+ _database.ForgotPasswordRequests.Add(request);
+ await _database.SaveChangesAsync(cancellationToken);
var portalUrl = _configuration.PORTAL_URL;
var emailFromAddress = _configuration.EMAIL_FROM_ADDRESS;
var emailFromDisplayName = _configuration.EMAIL_FROM_DISPLAY_NAME;
@@ -89,27 +88,27 @@ If you did not request a password reset, no action is required.
}
public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) {
- var requestsToRemove = _context.ForgotPasswordRequests.Where(c => c.UserId == userId).ToList();
+ var requestsToRemove = _database.ForgotPasswordRequests.Where(c => c.UserId == userId).ToList();
if (!requestsToRemove.Any()) return;
- _context.ForgotPasswordRequests.RemoveRange(requestsToRemove);
- await _context.SaveChangesAsync(cancellationToken);
+ _database.ForgotPasswordRequests.RemoveRange(requestsToRemove);
+ await _database.SaveChangesAsync(cancellationToken);
_logger.LogInformation($"Deleted {requestsToRemove.Count} password reset requests for user: {userId}.");
}
public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) {
var deleteCount = 0;
- foreach (var request in _context.ForgotPasswordRequests.Where(c => c.IsExpired)) {
+ foreach (var request in _database.ForgotPasswordRequests.Where(c => c.IsExpired)) {
if (!request.IsExpired) {
continue;
}
- _context.ForgotPasswordRequests.Remove(request);
+ _database.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);
+ await _database.SaveChangesAsync(cancellationToken);
_logger.LogInformation($"Deleted {deleteCount} stale password reset requests.");
}
} \ No newline at end of file
diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs
index 6db663a..30231e8 100644
--- a/code/api/src/Services/UserService.cs
+++ b/code/api/src/Services/UserService.cs
@@ -2,49 +2,49 @@ namespace IOL.GreatOffice.Api.Services;
public class UserService
{
- private readonly PasswordResetService _passwordResetService;
+ private readonly PasswordResetService _passwordResetService;
- /// <summary>
- /// Provides methods to perform common operations on user data.
- /// </summary>
- /// <param name="passwordResetService"></param>
- public UserService(PasswordResetService passwordResetService) {
- _passwordResetService = passwordResetService;
- }
+ /// <summary>
+ /// Provides methods to perform common operations on user data.
+ /// </summary>
+ /// <param name="passwordResetService"></param>
+ public UserService(PasswordResetService passwordResetService) {
+ _passwordResetService = passwordResetService;
+ }
- /// <summary>
- /// Log in a user.
- /// </summary>
- /// <param name="httpContext"></param>
- /// <param name="user"></param>
- /// <param name="persist"></param>
- public async Task LogInUser(HttpContext httpContext, User user, bool persist = false) {
- var claims = new List<Claim> {
- new(AppClaims.USER_ID, user.Id.ToString()),
- new(AppClaims.NAME, user.Username),
- };
+ /// <summary>
+ /// Log in a user.
+ /// </summary>
+ /// <param name="httpContext"></param>
+ /// <param name="user"></param>
+ /// <param name="persist"></param>
+ public async Task LogInUser(HttpContext httpContext, User user, bool persist = false) {
+ var claims = new List<Claim> {
+ 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,
- };
+ 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;
- }
+ if (persist) {
+ authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(6);
+ authenticationProperties.IsPersistent = true;
+ }
- await httpContext.SignInAsync(principal, authenticationProperties);
- await _passwordResetService.DeleteRequestsForUserAsync(user.Id);
- }
+ await httpContext.SignInAsync(principal, authenticationProperties);
+ await _passwordResetService.DeleteRequestsForUserAsync(user.Id);
+ }
- /// <summary>
- /// Log out a user.
- /// </summary>
- /// <param name="httpContext"></param>
- public async Task LogOutUser(HttpContext httpContext) {
- await httpContext.SignOutAsync();
- }
-}
+ /// <summary>
+ /// Log out a user.
+ /// </summary>
+ /// <param name="httpContext"></param>
+ public async Task LogOutUser(HttpContext httpContext) {
+ await httpContext.SignOutAsync();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs
index 2f8d46e..3d58608 100644
--- a/code/api/src/Services/VaultService.cs
+++ b/code/api/src/Services/VaultService.cs
@@ -4,155 +4,169 @@ namespace IOL.GreatOffice.Api.Services;
public class VaultService
{
- private readonly HttpClient _client;
- private readonly IMemoryCache _cache;
- private readonly IConfiguration _configuration;
- private readonly ILogger<VaultService> _logger;
- private int CACHE_TTL { get; set; }
+ private readonly HttpClient _client;
+ private readonly IMemoryCache _cache;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger<VaultService> _logger;
+ private int CACHE_TTL { get; set; }
- public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) {
- var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN);
- var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL);
- CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
- if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty");
- if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty");
- client.DefaultRequestHeaders.Add("X-Vault-Token", token);
- client.BaseAddress = new Uri(vaultUrl);
- _client = client;
- _cache = cache;
- _configuration = configuration;
- _logger = logger;
- }
+ public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) {
+ var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN);
+ var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL);
+ CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty");
+ if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty");
+ client.DefaultRequestHeaders.Add("X-Vault-Token", token);
+ client.BaseAddress = new Uri(vaultUrl);
+ _client = client;
+ _cache = cache;
+ _configuration = configuration;
+ _logger = logger;
+ }
- public static object Data { get; set; }
+ public T Get<T>(string path) {
+ var result = _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY,
+ cacheEntry => {
+ cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL);
+ var getSecretResponse = _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path).Result;
+ if (getSecretResponse == null) {
+ return default;
+ }
- public T Get<T>(string path) {
- var result = _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY,
- cacheEntry => {
- cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL);
- var getSecretResponse = _client.GetFromJsonAsync<GetSecretResponse<T>>("/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;
+ }
- 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<T>(string path) {
+ _cache.Remove(AppConstants.VAULT_CACHE_KEY);
+ CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
+ return Get<T>(path);
+ }
- public T Refresh<T>(string path) {
- _cache.Remove(AppConstants.VAULT_CACHE_KEY);
- CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12);
- return Get<T>(path);
- }
+ public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token) {
+ var response = await _client.PostAsJsonAsync("v1/auth/token/renew",
+ new {
+ Token = token
+ });
+ if (response.IsSuccessStatusCode) {
+ return await response.Content.ReadFromJsonAsync<RenewTokenResponse>();
+ }
- public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token) {
- var response = await _client.PostAsJsonAsync("v1/auth/token/renew",
- new {
- Token = token
- });
- if (response.IsSuccessStatusCode) {
- return await response.Content.ReadFromJsonAsync<RenewTokenResponse>();
- }
+ return default;
+ }
- return default;
- }
+ public AppConfiguration GetCurrentAppConfiguration() {
+ var isInFlightMode = true;
+ if (isInFlightMode) {
+ return new AppConfiguration() {
+ DB_HOST = "localhost",
+ DB_PORT = "5432",
+ DB_NAME = "greatoffice_ivar_dev",
+ DB_PASSWORD = "ivar123",
+ DB_USER = "postgres",
+ QUARTZ_DB_HOST = "localhost",
+ QUARTZ_DB_PORT = "5432",
+ QUARTZ_DB_NAME = "greatoffice_quartz_ivar_dev",
+ QUARTZ_DB_PASSWORD = "ivar123",
+ QUARTZ_DB_USER = "postgres",
+ APP_CERT = "MIII2QIBAzCCCJ8GCSqGSIb3DQEHAaCCCJAEggiMMIIIiDCCAz8GCSqGSIb3DQEHBqCCAzAwggMsAgEAMIIDJQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI1JebRQOOJekCAggAgIIC+FTMxILwgOWxknEWvucjaHTtf/KUcSPwc/zWg0RoFzu03o1vZkStztz92L2J+nnNrQti7WEx0C/h5ug67qCQAdzkjNHZE9wn1pI2EqaCwKYwZnOTmyJDLV86xQlH9QYTs4/L1F2qTwpKdBoB2lNyTswYgZ8WNYY65fbFKUUVIaHkReGnma/ERm8F38Ymp7cudqQ4G6sh6E+JFSo2IfcTFb260fWO/iMDU3GryNsaUl4amT4aQfsSrNtf6ODy8Ivh7tJeLbR6bqzMtsKPzavT5ZbA6AP2GYAQSVejNP79lqmtoOs8+dz7HaJdxORBESYi02z+j2rb4ssI+ZPx+YtGvgpr79gVf3qM1/VX4ROzLDUUJGWS2RnRDBBY/d0uMqftndUHSPMFuhfvgBUXdzLqhoEqYNMTP3BpOyBQ7f26soLKrc3zup2WIn8WSnQFLi2D8cWPVPS9iAb0EgQ5cBjuaLz2aX1WVMCSzya7a6Z93rLxUf9s3+PEy75ibhoh/cJvAlCMTfiVAhJOaIroR1K4MKkO23ylyLHv49/2GYIaZ8n0WRO57fM5jDUhOfti+ZzPM6hrSJkRSla+pr8DFlpqOqObksGwxGGTqq6ZvWon19kXesFl5n640uJBu7Viq8IdxGAbX/aRkZNlvja7sOgfiNz3Hxomz7DWwgWLKaNKlFSqFMzsTUye+mUByC1AfEn14/SYyyxRTB99PmItxWFyjo9nOsxH5riz7tPTPxUXzhVb4eDt7PjY+ZsEKTC3a/LFqf3k5MWk+qc4p0Kx1sGaGEAPCCE04mZ7NOdqk6dhoP46FNUEh8CmxDDVaMSdThrvyzv9KrclwQnRMJz7BJWVXUemyQl3aModepXIhvLv90nH1qzYlFDQ0H6rxzCB4f1l//GoWPyYFBxGh6UrkunXWx2fopR87zi2OF3azxqscF/qLVU4FHKzhMrec7eE0/dk3d+0If/AxQ4p7Cso92i/5n0Bsg5DWc4EIWBuldodsjVxxq7dKxinKJkwggVBBgkqhkiG9w0BBwGgggUyBIIFLjCCBSowggUmBgsqhkiG9w0BDAoBAqCCBO4wggTqMBwGCiqGSIb3DQEMAQMwDgQIb6GEBS5DxrkCAggABIIEyHcCXqJMUG8t0PhvDwf0dHZo6SiA2WsLz1hM+KgNBrE8YwuXEZTGYzfHy85cEWNB2WLV5kxgu/vtifCnnlS1bvc2kMKT3dIFER/7hOqRh8pNvzMoeNc4zNkiEB1ZXxlctUKDsQozbLUhnRATwNyeaMkt3B0KQuRaMxGuA9riRISnmGd1K5GTm3VZ0I7e6vDqXCllLzfOQ+aoz8WIOFJ1aoN2E5+bDTtcr18xYJMd+kNOMjMcbg5f9kmNZAk1MBRuiEWtUjMhRySYWk1Km/y5WHRNRShHTae/E4ifmpLuUKsfOjX7T/4RDWg8rYCnxUpLfCln+omQ9t0gFSN+Et7Dw+cyW48Kkrw6StnRz/AeLxo3SU/PAXVazmAa6ZkuNe+uasvTniYM+enw4hgcXPzTu90R40fTGHO1Sp8EV3IbvrtwFu9kjCxtgleLQ139HTtpWXjVZ0o1ikmn2uN4f73gxKIKxmSX4xZZN6IDOze3OOY1aalUIzkbwFAYCV74zEL05dJzo3GOOJfdQsp2GNJPfkcAcuMPMvi+mieBk6XjKDCj95b41hSWDqfuMUgPh3xm3/felVD1HRNO9NF0RscosP02NLi44TcNz4LX9j/E9PHpNFF+W4ba1w7v7h4P5/leQFRP7+H90fPHA2M8UOHZ4AwmwdA5sHYXBoXkVS3snbVzhzkvW5GblFn0l1AFj8AO0HLCwGSumZ1uUEvEA021hmluHbs62iIiOYJbacbcT/TUpO5/2tFMPKr042LmpQFDIuEfrurLTC3r1iXuS6fkWbf2IxdjTrtL61AtPqtFagKSGsyHViO7nPu6yhbhTbmQJ4G0t++b6h18RPS+3muwrnSxgAz6OmbBWybNKOlAyTjd4JO3hfCaQ+K/mO2Q9TnSUOTgeobXXZsOEdltPXFJExQ7+cqkr4gKdPeoTZEcv8jRoS+NHasZIvMPGrwYnvOuSJ09qAwtIcvhGaPkEmTZ6b3wQl0mnTMPCQHXGTXztucB0O33kbn8sClfs6P6dg0GdR6ZnNFacwIpe8T8PmLg5q8bu5FL1eNo4+ijzC64lrZkKeRKKT1vBtZfcGQTvE8TTdQQS5MkKcptfL/3HVE9VopNZlzryJGYj89GMeQ1PfABi1Ovs5gjfro+0xBbtVuAWbP8dM5ugozO1//vjTMZYwXml4nIFkHuGe7R4ZpKRIVjVy7RScelCuQ0yNMGIzx/5Dz3FQXWq1Jii669Oxs/R7iupwo+f6O9XmCJAGXIw5a11Yw9cULptVNc9rPHrauAOeNpE77aSQRKEOJZADvdLB8cXjpXFf2mvzFib69Cuks8QxktAK7Yk3fke1CJpoIb75d8iHkY21epOtqavTppezEd+0uq5RJThH+/nMyZVhRI3tSJ0kVDc1HVX2bTquWcXtniuZNOWYklLxKPfQNho8n0pHRk22UmB8DOxMjnAyt3s7xBNpujU+I7D30lK9N3PH4U+Oyc9pIWc2T7pFILvvToxoE3l2flg6eHnBd6a7ENDVbz1ELwwmt36QQAVQytEngTBYkorbJcQC6e2r/RqoqpP2N4dB7+2ZDMVw97VBraMl7ELaYdf9SOdzuis2engAojSiUUO/gdKGaJGnnldOSi5rvnxs+iMjElMCMGCSqGSIb3DQEJFTEWBBRSLC58imQohokANg6rVjq9KE/MxjAxMCEwCQYFKw4DAhoFAAQUILUGtKvqxRY/ywrrlxKrsuLiNLwECCWv9bVh/bZZAgIIAA=="
+ };
+ }
- public AppConfiguration GetCurrentAppConfiguration() {
- var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
- var result = Get<AppConfiguration>(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;
- }
+ var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
+ var result = Get<AppConfiguration>(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_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;
- }
+ if (overwrites.DB_HOST.HasValue()) {
+ _logger.LogInformation("OVERWRITE_DB_HOST is specified, using it's value: {DB_HOST}", overwrites.DB_HOST);
+ result.DB_HOST = overwrites.DB_HOST;
+ }
- public AppConfiguration RefreshCurrentAppConfiguration() {
- var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
- return Refresh<AppConfiguration>(path);
- }
+ if (overwrites.DB_PORT.HasValue()) {
+ _logger.LogInformation("OVERWRITE_DB_PORT is specified, using it's value: {DB_PORT}", overwrites.DB_PORT);
+ result.DB_PORT = overwrites.DB_PORT;
+ }
- public class RenewTokenResponse
- {
- public Guid RequestId { get; set; }
- public string LeaseId { get; set; }
- public bool Renewable { get; set; }
- public long LeaseDuration { get; set; }
- public object Data { get; set; }
- public object WrapInfo { get; set; }
- public List<string> Warnings { get; set; }
- public Auth Auth { get; set; }
- }
+ if (overwrites.DB_USER.HasValue()) {
+ _logger.LogInformation("OVERWRITE_DB_USER is specified, using it's value: {DB_USER}", overwrites.DB_USER);
+ result.DB_USER = overwrites.DB_USER;
+ }
- public class Auth
- {
- public string ClientToken { get; set; }
- public string Accessor { get; set; }
- public List<string> Policies { get; set; }
- public List<string> TokenPolicies { get; set; }
- public object Metadata { get; set; }
- public long LeaseDuration { get; set; }
- public bool Renewable { get; set; }
- public string EntityId { get; set; }
- public string TokenType { get; set; }
- public bool Orphan { get; set; }
- public object MfaRequirement { get; set; }
- public long NumUses { get; set; }
- }
+ if (overwrites.DB_PASSWORD.HasValue()) {
+ _logger.LogInformation("OVERWRITE_DB_PASSWORD is specified, using it's value: (redacted)");
+ result.DB_PASSWORD = overwrites.DB_PASSWORD;
+ }
- public class GetSecretResponse<T>
- {
- public VaultSecret<T> Data { get; set; }
- }
+ if (overwrites.DB_NAME.HasValue()) {
+ _logger.LogInformation("OVERWRITE_DB_NAME is specified, using it's value: {DB_NAME}", overwrites.DB_NAME);
+ result.DB_NAME = overwrites.DB_NAME;
+ }
- public class VaultSecret<T>
- {
- public T Data { get; set; }
- public VaultSecretMetadata Metadata { get; set; }
- }
+ return result;
+ }
- public class VaultSecretMetadata
- {
- public DateTimeOffset CreatedTime { get; set; }
- public object CustomMetadata { get; set; }
- public string DeletionTime { get; set; }
- public bool Destroyed { get; set; }
- public long Version { get; set; }
- }
-}
+ public AppConfiguration RefreshCurrentAppConfiguration() {
+ var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET);
+ return Refresh<AppConfiguration>(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<string> Warnings { get; set; }
+ public Auth Auth { get; set; }
+ }
+
+ public class Auth
+ {
+ public string ClientToken { get; set; }
+ public string Accessor { get; set; }
+ public List<string> Policies { get; set; }
+ public List<string> TokenPolicies { get; set; }
+ public object Metadata { get; set; }
+ public long LeaseDuration { get; set; }
+ public bool Renewable { get; set; }
+ public string EntityId { get; set; }
+ public string TokenType { get; set; }
+ public bool Orphan { get; set; }
+ public object MfaRequirement { get; set; }
+ public long NumUses { get; set; }
+ }
+
+ public class GetSecretResponse<T>
+ {
+ public VaultSecret<T> Data { get; set; }
+ }
+
+ public class VaultSecret<T>
+ {
+ public T Data { get; set; }
+ public VaultSecretMetadata Metadata { get; set; }
+ }
+
+ public class VaultSecretMetadata
+ {
+ public DateTimeOffset CreatedTime { get; set; }
+ public object CustomMetadata { get; set; }
+ public string DeletionTime { get; set; }
+ public bool Destroyed { get; set; }
+ public long Version { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/BasicAuthenticationHandler.cs b/code/api/src/Utilities/BasicAuthenticationHandler.cs
index 6138193..b0a2d1a 100644
--- a/code/api/src/Utilities/BasicAuthenticationHandler.cs
+++ b/code/api/src/Utilities/BasicAuthenticationHandler.cs
@@ -7,7 +7,7 @@ namespace IOL.GreatOffice.Api.Utilities;
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _context;
private readonly AppConfiguration _configuration;
private readonly ILogger<BasicAuthenticationHandler> _logger;
@@ -16,7 +16,7 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
- AppDbContext context,
+ MainAppDatabase context,
VaultService vaultService
) :
base(options, logger, encoder, clock) {
diff --git a/code/api/src/Utilities/DateTimeExtensions.cs b/code/api/src/Utilities/DateTimeExtensions.cs
new file mode 100644
index 0000000..d25e9a8
--- /dev/null
+++ b/code/api/src/Utilities/DateTimeExtensions.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class DateTimeExtensions
+{
+ public static bool IsNullOrEmpty(this DateTime dateTime) {
+ return (dateTime == default);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/QueryableExtensions.cs b/code/api/src/Utilities/QueryableExtensions.cs
new file mode 100644
index 0000000..bf2bf3b
--- /dev/null
+++ b/code/api/src/Utilities/QueryableExtensions.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class QueryableExtensions
+{
+ public static IQueryable<T> ForTenant<T>(this IQueryable<T> queryable, LoggedInUserModel loggedInUserModel) where T : BaseWithOwner {
+ return queryable.Where(c => c.TenantId == loggedInUserModel.TenantId);
+ }
+
+ public static IQueryable<T> ForUser<T>(this IQueryable<T> queryable, LoggedInUserModel loggedInUserModel) where T : BaseWithOwner {
+ return queryable.Where(c => c.UserId == loggedInUserModel.Id);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Utilities/Validators.cs b/code/api/src/Utilities/Validators.cs
new file mode 100644
index 0000000..fa3a144
--- /dev/null
+++ b/code/api/src/Utilities/Validators.cs
@@ -0,0 +1,12 @@
+using System.Text.RegularExpressions;
+
+namespace IOL.GreatOffice.Api.Utilities;
+
+public static class Validators
+{
+ private static readonly Regex EMAIL_REGEX = new("");
+
+ public static bool IsValidEmail(this string email) {
+ return EMAIL_REGEX.IsMatch(email);
+ }
+} \ No newline at end of file