aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-12-09 05:38:25 +0100
committerivarlovlie <git@ivarlovlie.no>2022-12-09 05:38:25 +0100
commit8da37c77cae0c7f712a775e3996afd9d84b0f9af (patch)
tree98bc4898d9800f91387e075bac0fcf97be9969ab /code/api/src
parent59055b8056bd92ea56e97e86d9ec255daf0d3129 (diff)
downloadgreatoffice-8da37c77cae0c7f712a775e3996afd9d84b0f9af.tar.xz
greatoffice-8da37c77cae0c7f712a775e3996afd9d84b0f9af.zip
feat: !WIP implement email validation
Diffstat (limited to 'code/api/src')
-rw-r--r--code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs1618
-rw-r--r--code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs46
-rw-r--r--code/api/src/Migrations/AppDbContextModelSnapshot.cs25
-rw-r--r--code/api/src/Models/Database/Internal/User.cs1
-rw-r--r--code/api/src/Models/Database/MainAppDatabase.cs3
-rw-r--r--code/api/src/Models/Database/Queues/ValidationEmail.cs8
-rw-r--r--code/api/src/Models/Misc/AppConfiguration.cs5
-rw-r--r--code/api/src/Services/UserService.cs51
-rw-r--r--code/api/src/Services/VaultService.cs1
9 files changed, 1743 insertions, 15 deletions
diff --git a/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs
new file mode 100644
index 0000000..aa2d7f4
--- /dev/null
+++ b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.Designer.cs
@@ -0,0 +1,1618 @@
+// <auto-generated />
+using System;
+using IOL.GreatOffice.Api.Data.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ [DbContext(typeof(MainAppDatabase))]
+ [Migration("20221209043806_ValidationEmailQueue")]
+ partial class ValidationEmailQueue
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("GroupsId")
+ .HasColumnType("uuid")
+ .HasColumnName("groups_id");
+
+ b.HasKey("CustomersId", "GroupsId")
+ .HasName("pk_customer_customer_group");
+
+ b.HasIndex("GroupsId")
+ .HasDatabaseName("ix_customer_customer_group_groups_id");
+
+ b.ToTable("customer_customer_group", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.Property<Guid>("CustomersId")
+ .HasColumnType("uuid")
+ .HasColumnName("customers_id");
+
+ b.Property<Guid>("ProjectsId")
+ .HasColumnType("uuid")
+ .HasColumnName("projects_id");
+
+ b.HasKey("CustomersId", "ProjectsId")
+ .HasName("pk_customer_project");
+
+ b.HasIndex("ProjectsId")
+ .HasDatabaseName("ix_customer_project_projects_id");
+
+ b.ToTable("customer_project", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("AllowCreate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_create");
+
+ b.Property<bool>("AllowDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_delete");
+
+ b.Property<bool>("AllowRead")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_read");
+
+ b.Property<bool>("AllowUpdate")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_update");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime>("ExpiryDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiry_date");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_api_access_tokens");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_api_access_tokens_user_id");
+
+ b.ToTable("api_access_tokens", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Address1")
+ .HasColumnType("text")
+ .HasColumnName("address1");
+
+ b.Property<string>("Address2")
+ .HasColumnType("text")
+ .HasColumnName("address2");
+
+ b.Property<string>("Country")
+ .HasColumnType("text")
+ .HasColumnName("country");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<string>("Currency")
+ .HasColumnType("text")
+ .HasColumnName("currency");
+
+ b.Property<string>("CustomerNumber")
+ .HasColumnType("text")
+ .HasColumnName("customer_number");
+
+ b.Property<string>("DefaultReference")
+ .HasColumnType("text")
+ .HasColumnName("default_reference");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<string>("ORGNumber")
+ .HasColumnType("text")
+ .HasColumnName("org_number");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("uuid")
+ .HasColumnName("owner_id");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<string>("PostalCity")
+ .HasColumnType("text")
+ .HasColumnName("postal_city");
+
+ b.Property<string>("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("VATNumber")
+ .HasColumnType("text")
+ .HasColumnName("vat_number");
+
+ b.Property<string>("Website")
+ .HasColumnType("text")
+ .HasColumnName("website");
+
+ b.HasKey("Id")
+ .HasName("pk_customers");
+
+ b.HasIndex("OwnerId")
+ .HasDatabaseName("ix_customers_owner_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customers_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customers_user_id");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<string>("Phone")
+ .HasColumnType("text")
+ .HasColumnName("phone");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("WorkTitle")
+ .HasColumnType("text")
+ .HasColumnName("work_title");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_contacts");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_contacts_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_contacts_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_contacts_user_id");
+
+ b.ToTable("customer_contacts", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<Guid?>("CustomerId")
+ .HasColumnType("uuid")
+ .HasColumnName("customer_id");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Note")
+ .HasColumnType("text")
+ .HasColumnName("note");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_events");
+
+ b.HasIndex("CustomerId")
+ .HasDatabaseName("ix_customer_events_customer_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_events_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_events_user_id");
+
+ b.ToTable("customer_events", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_customer_groups");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_customer_groups_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_customer_groups_user_id");
+
+ b.ToTable("customer_groups", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_password_reset_requests");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_password_reset_requests_user_id");
+
+ b.ToTable("password_reset_requests", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<DateTime?>("Start")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start");
+
+ b.Property<DateTime?>("Stop")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("stop");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_projects");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_projects_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_projects_user_id");
+
+ b.ToTable("projects", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_project_labels");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_labels_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_project_labels_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_labels_user_id");
+
+ b.ToTable("project_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<int>("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_project_members");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_project_members_project_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_project_members_user_id");
+
+ b.ToTable("project_members", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Queues.ValidationEmail", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("EmailSentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_sent_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_validation_emails");
+
+ b.ToTable("validation_emails", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("ContactEmail")
+ .HasColumnType("text")
+ .HasColumnName("contact_email");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<Guid>("MasterUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("master_user_id");
+
+ b.Property<string>("MasterUserPassword")
+ .HasColumnType("text")
+ .HasColumnName("master_user_password");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("OwningTenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("owning_tenant_id");
+
+ b.Property<string>("Slug")
+ .HasColumnType("text")
+ .HasColumnName("slug");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_tenants");
+
+ b.HasIndex("OwningTenantId")
+ .HasDatabaseName("ix_tenants_owning_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_tenants_user_id");
+
+ b.ToTable("tenants", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<Guid?>("AssignedToId")
+ .HasColumnType("uuid")
+ .HasColumnName("assigned_to_id");
+
+ b.Property<DateTime?>("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("closed_at");
+
+ b.Property<Guid?>("ClosedById")
+ .HasColumnType("uuid")
+ .HasColumnName("closed_by_id");
+
+ b.Property<Guid>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("PublicId")
+ .HasColumnType("text")
+ .HasColumnName("public_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todos");
+
+ b.HasIndex("AssignedToId")
+ .HasDatabaseName("ix_todos_assigned_to_id");
+
+ b.HasIndex("ClosedById")
+ .HasDatabaseName("ix_todos_closed_by_id");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todos_collection_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todos_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todos_user_id");
+
+ b.ToTable("todos", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<string>("Description")
+ .HasColumnType("text")
+ .HasColumnName("description");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("ProjectId")
+ .HasColumnType("uuid")
+ .HasColumnName("project_id");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("integer")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collections");
+
+ b.HasIndex("ProjectId")
+ .HasDatabaseName("ix_todo_collections_project_id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_collections_tenant_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collections_user_id");
+
+ b.ToTable("todo_collections", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<bool>("CanBrowse")
+ .HasColumnType("boolean")
+ .HasColumnName("can_browse");
+
+ b.Property<bool>("CanComment")
+ .HasColumnType("boolean")
+ .HasColumnName("can_comment");
+
+ b.Property<bool>("CanEdit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_edit");
+
+ b.Property<bool>("CanSubmit")
+ .HasColumnType("boolean")
+ .HasColumnName("can_submit");
+
+ b.Property<Guid?>("CollectionId")
+ .HasColumnType("uuid")
+ .HasColumnName("collection_id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_collection_access_controls");
+
+ b.HasIndex("CollectionId")
+ .HasDatabaseName("ix_todo_collection_access_controls_collection_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_collection_access_controls_user_id");
+
+ b.ToTable("todo_collection_access_controls", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<int?>("ClosingStatement")
+ .HasColumnType("integer")
+ .HasColumnName("closing_statement");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property<string>("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_comments");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_comments_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_comments_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_comments_user_id");
+
+ b.ToTable("todo_comments", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<string>("Color")
+ .HasColumnType("text")
+ .HasColumnName("color");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<Guid?>("DeletedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<Guid?>("ModifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("modified_by");
+
+ b.Property<string>("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<Guid?>("TodoId")
+ .HasColumnType("uuid")
+ .HasColumnName("todo_id");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_todo_labels");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_todo_labels_tenant_id");
+
+ b.HasIndex("TodoId")
+ .HasDatabaseName("ix_todo_labels_todo_id");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_todo_labels_user_id");
+
+ b.ToTable("todo_labels", (string)null);
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property<bool>("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property<DateTime?>("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property<string>("Email")
+ .HasColumnType("text")
+ .HasColumnName("email");
+
+ b.Property<DateTime>("EmailLastValidated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_last_validated");
+
+ b.Property<string>("FirstName")
+ .HasColumnType("text")
+ .HasColumnName("first_name");
+
+ b.Property<string>("LastName")
+ .HasColumnType("text")
+ .HasColumnName("last_name");
+
+ b.Property<DateTime?>("ModifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("modified_at");
+
+ b.Property<string>("Password")
+ .HasColumnType("text")
+ .HasColumnName("password");
+
+ b.Property<Guid?>("TenantId")
+ .HasColumnType("uuid")
+ .HasColumnName("tenant_id");
+
+ b.Property<string>("Username")
+ .HasColumnType("text")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("ix_users_tenant_id");
+
+ b.ToTable("users", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+ b.Property<string>("FriendlyName")
+ .HasColumnType("text")
+ .HasColumnName("friendly_name");
+
+ b.Property<string>("Xml")
+ .HasColumnType("text")
+ .HasColumnName("xml");
+
+ b.HasKey("Id")
+ .HasName("pk_data_protection_keys");
+
+ b.ToTable("data_protection_keys", (string)null);
+ });
+
+ modelBuilder.Entity("CustomerCustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.CustomerGroup", null)
+ .WithMany()
+ .HasForeignKey("GroupsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_customer_group_customer_groups_groups_id");
+ });
+
+ modelBuilder.Entity("CustomerProject", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", null)
+ .WithMany()
+ .HasForeignKey("CustomersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_customers_customers_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", null)
+ .WithMany()
+ .HasForeignKey("ProjectsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_customer_project_projects_projects_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_api_access_tokens_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .HasConstraintName("fk_customers_users_owner_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customers_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customers_users_user_id");
+
+ b.Navigation("Owner");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerContact", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Contacts")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_contacts_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_contacts_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_contacts_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerEvent", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Customer", "Customer")
+ .WithMany("Events")
+ .HasForeignKey("CustomerId")
+ .HasConstraintName("fk_customer_events_customers_customer_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_events_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_events_users_user_id");
+
+ b.Navigation("Customer");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.CustomerGroup", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_customer_groups_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_customer_groups_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.PasswordResetRequest", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_password_reset_requests_users_user_id");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_projects_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_projects_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Labels")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_labels_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_project_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ProjectMember", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany("Members")
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_project_members_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_project_members_users_user_id");
+
+ b.Navigation("Project");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("OwningTenantId")
+ .HasConstraintName("fk_tenants_tenants_owning_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany("Tenants")
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_tenants_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "AssignedTo")
+ .WithMany()
+ .HasForeignKey("AssignedToId")
+ .HasConstraintName("fk_todos_users_assigned_to_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ClosedBy")
+ .WithMany()
+ .HasForeignKey("ClosedById")
+ .HasConstraintName("fk_todos_users_closed_by_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany()
+ .HasForeignKey("CollectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_todos_todo_projects_collection_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todos_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todos_users_user_id");
+
+ b.Navigation("AssignedTo");
+
+ b.Navigation("ClosedBy");
+
+ b.Navigation("Collection");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Project", "Project")
+ .WithMany()
+ .HasForeignKey("ProjectId")
+ .HasConstraintName("fk_todo_collections_projects_project_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_collections_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collections_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Project");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollectionAccessControl", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.TodoCollection", "Collection")
+ .WithMany("AccessControls")
+ .HasForeignKey("CollectionId")
+ .HasConstraintName("fk_todo_collection_access_controls_todo_collections_collection");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_collection_access_controls_users_user_id");
+
+ b.Navigation("Collection");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoComment", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_comments_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Comments")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_comments_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_comments_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoLabel", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "OwningTenant")
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_todo_labels_tenants_tenant_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Todo", "Todo")
+ .WithMany("Labels")
+ .HasForeignKey("TodoId")
+ .HasConstraintName("fk_todo_labels_todos_todo_id");
+
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "OwningUser")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .HasConstraintName("fk_todo_labels_users_user_id");
+
+ b.Navigation("OwningTenant");
+
+ b.Navigation("OwningUser");
+
+ b.Navigation("Todo");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", null)
+ .WithMany("Users")
+ .HasForeignKey("TenantId")
+ .HasConstraintName("fk_users_tenants_tenant_id");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Customer", b =>
+ {
+ b.Navigation("Contacts");
+
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Project", b =>
+ {
+ b.Navigation("Labels");
+
+ b.Navigation("Members");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
+ {
+ b.Navigation("Users");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Todo", b =>
+ {
+ b.Navigation("Comments");
+
+ b.Navigation("Labels");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TodoCollection", b =>
+ {
+ b.Navigation("AccessControls");
+ });
+
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b =>
+ {
+ b.Navigation("Tenants");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs
new file mode 100644
index 0000000..3599a37
--- /dev/null
+++ b/code/api/src/Migrations/20221209043806_ValidationEmailQueue.cs
@@ -0,0 +1,46 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace IOL.GreatOffice.Api.Migrations
+{
+ /// <inheritdoc />
+ public partial class ValidationEmailQueue : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<DateTime>(
+ name: "email_last_validated",
+ table: "users",
+ type: "timestamp with time zone",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+
+ migrationBuilder.CreateTable(
+ name: "validation_emails",
+ columns: table => new
+ {
+ id = table.Column<Guid>(type: "uuid", nullable: false),
+ emailsentat = table.Column<DateTime>(name: "email_sent_at", type: "timestamp with time zone", nullable: false),
+ userid = table.Column<Guid>(name: "user_id", type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk_validation_emails", x => x.id);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "validation_emails");
+
+ migrationBuilder.DropColumn(
+ name: "email_last_validated",
+ table: "users");
+ }
+ }
+}
diff --git a/code/api/src/Migrations/AppDbContextModelSnapshot.cs b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
index 0c6ace3..ff6563e 100644
--- a/code/api/src/Migrations/AppDbContextModelSnapshot.cs
+++ b/code/api/src/Migrations/AppDbContextModelSnapshot.cs
@@ -665,6 +665,27 @@ namespace IOL.GreatOffice.Api.Migrations
b.ToTable("project_members", (string)null);
});
+ modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Queues.ValidationEmail", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("EmailSentAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_sent_at");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_validation_emails");
+
+ b.ToTable("validation_emails", (string)null);
+ });
+
modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b =>
{
b.Property<Guid>("Id")
@@ -1136,6 +1157,10 @@ namespace IOL.GreatOffice.Api.Migrations
.HasColumnType("text")
.HasColumnName("email");
+ b.Property<DateTime>("EmailLastValidated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("email_last_validated");
+
b.Property<string>("FirstName")
.HasColumnType("text")
.HasColumnName("first_name");
diff --git a/code/api/src/Models/Database/Internal/User.cs b/code/api/src/Models/Database/Internal/User.cs
index 9db5d35..b046974 100644
--- a/code/api/src/Models/Database/Internal/User.cs
+++ b/code/api/src/Models/Database/Internal/User.cs
@@ -13,6 +13,7 @@ public class User : Base
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
+ public DateTime EmailLastValidated { get; set; }
public ICollection<Tenant> Tenants { get; set; }
public string DisplayName() {
diff --git a/code/api/src/Models/Database/MainAppDatabase.cs b/code/api/src/Models/Database/MainAppDatabase.cs
index eaf7781..33e5dcd 100644
--- a/code/api/src/Models/Database/MainAppDatabase.cs
+++ b/code/api/src/Models/Database/MainAppDatabase.cs
@@ -1,3 +1,4 @@
+using IOL.GreatOffice.Api.Data.Database.Queues;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
namespace IOL.GreatOffice.Api.Data.Database;
@@ -21,6 +22,7 @@ public class MainAppDatabase : DbContext, IDataProtectionKeyContext
public DbSet<TodoCollection> TodoProjects { get; set; }
public DbSet<TodoComment> TodoComments { get; set; }
public DbSet<Todo> Todos { get; set; }
+ public DbSet<ValidationEmail> ValidationEmails { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<User>(e => {
@@ -99,6 +101,7 @@ public class MainAppDatabase : DbContext, IDataProtectionKeyContext
e.HasOne(n => n.Collection);
e.ToTable("todo_collection_access_controls");
});
+ modelBuilder.Entity<ValidationEmail>(e => { e.ToTable("validation_emails"); });
base.OnModelCreating(modelBuilder);
}
diff --git a/code/api/src/Models/Database/Queues/ValidationEmail.cs b/code/api/src/Models/Database/Queues/ValidationEmail.cs
new file mode 100644
index 0000000..8ca8c5d
--- /dev/null
+++ b/code/api/src/Models/Database/Queues/ValidationEmail.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database.Queues;
+
+public class ValidationEmail
+{
+ public Guid Id { get; set; }
+ public DateTime EmailSentAt { get; set; }
+ public Guid UserId { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Models/Misc/AppConfiguration.cs b/code/api/src/Models/Misc/AppConfiguration.cs
index 2a9afc2..31e5726 100644
--- a/code/api/src/Models/Misc/AppConfiguration.cs
+++ b/code/api/src/Models/Misc/AppConfiguration.cs
@@ -80,6 +80,11 @@ public class AppConfiguration
public string CANONICAL_FRONTEND_URL { get; set; }
/// <summary>
+ /// The absolute url to the backend api
+ /// </summary>
+ public string CANONICAL_BACKEND_URL { get; set; }
+
+ /// <summary>
/// A random string used to encrypt/decrypt for general purposes
/// </summary>
public string APP_AES_KEY { get; set; }
diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs
index 30231e8..8b925be 100644
--- a/code/api/src/Services/UserService.cs
+++ b/code/api/src/Services/UserService.cs
@@ -1,23 +1,26 @@
+using IOL.GreatOffice.Api.Data.Database.Queues;
+using Microsoft.Extensions.Localization;
+
namespace IOL.GreatOffice.Api.Services;
public class UserService
{
private readonly PasswordResetService _passwordResetService;
+ private readonly MailService _mailService;
+ private readonly ILogger<UserService> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+ private readonly MainAppDatabase _database;
+ private string EmailValidationUrl;
- /// <summary>
- /// Provides methods to perform common operations on user data.
- /// </summary>
- /// <param name="passwordResetService"></param>
- public UserService(PasswordResetService passwordResetService) {
+ public UserService(PasswordResetService passwordResetService, MailService mailService, IStringLocalizer<SharedResources> localizer, VaultService vaultService, MainAppDatabase database) {
_passwordResetService = passwordResetService;
+ _mailService = mailService;
+ _localizer = localizer;
+ _database = database;
+ var configuration = vaultService.GetCurrentAppConfiguration();
+ EmailValidationUrl = configuration.CANONICAL_BACKEND_URL + "/validate";
}
- /// <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()),
@@ -38,13 +41,31 @@ public class UserService
await httpContext.SignInAsync(principal, authenticationProperties);
await _passwordResetService.DeleteRequestsForUserAsync(user.Id);
+ _logger.LogInformation("Logged in user {0}", user.Id);
}
- /// <summary>
- /// Log out a user.
- /// </summary>
- /// <param name="httpContext"></param>
public async Task LogOutUser(HttpContext httpContext) {
await httpContext.SignOutAsync();
+ _logger.LogInformation("Logged out user {0}", httpContext.User.FindFirst(AppClaims.USER_ID));
+ }
+
+ public async Task SendValidationEmail(User user) {
+ var email = new MailService.PostmarkEmail() {
+ To = user.Username,
+ Subject = _localizer["Greatoffice Email Validation"],
+ TextBody = _localizer["""
+Hello, {0}.
+
+Validate your email address by opening this link in a browser {1}
+""", user.DisplayName(), EmailValidationUrl + "?email=" + user.Username]
+ };
+ var queueItem = new ValidationEmail() {
+ UserId = user.Id,
+ Id = Guid.NewGuid()
+ };
+ await _mailService.SendMail(email);
+ queueItem.EmailSentAt = DateTime.UtcNow;
+ _database.ValidationEmails.Add(queueItem);
+ await _database.SaveChangesAsync();
}
} \ No newline at end of file
diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs
index 4961ceb..3745096 100644
--- a/code/api/src/Services/VaultService.cs
+++ b/code/api/src/Services/VaultService.cs
@@ -72,6 +72,7 @@ public class VaultService
DB_NAME = "greatoffice_ivar_dev",
DB_PASSWORD = "ivar123",
CANONICAL_FRONTEND_URL = "http://localhost:5173",
+ CANONICAL_BACKEND_URL = "http://localhost:5000",
POSTMARK_TOKEN = "b530c311-45c7-43e5-aa48-f2c992886e51",
DB_USER = "postgres",
QUARTZ_DB_HOST = "localhost",