aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src/Data
diff options
context:
space:
mode:
Diffstat (limited to 'code/api/src/Data')
-rw-r--r--code/api/src/Data/AppDbContext.cs51
-rw-r--r--code/api/src/Data/Database/ApiAccessToken.cs31
-rw-r--r--code/api/src/Data/Database/Base.cs15
-rw-r--r--code/api/src/Data/Database/BaseWithOwner.cs19
-rw-r--r--code/api/src/Data/Database/Customer.cs6
-rw-r--r--code/api/src/Data/Database/CustomerContact.cs12
-rw-r--r--code/api/src/Data/Database/CustomerEvent.cs7
-rw-r--r--code/api/src/Data/Database/ForgotPasswordRequest.cs23
-rw-r--r--code/api/src/Data/Database/Project.cs7
-rw-r--r--code/api/src/Data/Database/Tenant.cs11
-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.cs13
-rw-r--r--code/api/src/Data/Database/TodoComment.cs7
-rw-r--r--code/api/src/Data/Database/TodoLabel.cs8
-rw-r--r--code/api/src/Data/Database/TodoProject.cs16
-rw-r--r--code/api/src/Data/Database/TodoProjectAccessControl.cs11
-rw-r--r--code/api/src/Data/Database/TodoStatus.cs45
-rw-r--r--code/api/src/Data/Database/User.cs37
-rw-r--r--code/api/src/Data/Dtos/TimeQueryDto.cs34
-rw-r--r--code/api/src/Data/Dtos/UserArchiveDto.cs131
-rw-r--r--code/api/src/Data/Enums/TimeEntryQueryDuration.cs37
-rw-r--r--code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs21
-rw-r--r--code/api/src/Data/Exceptions/UserNotFoundException.cs19
-rw-r--r--code/api/src/Data/Models/ApiSpecDocument.cs9
-rw-r--r--code/api/src/Data/Models/AppPath.cs23
-rw-r--r--code/api/src/Data/Models/LoggedInUserModel.cs7
-rw-r--r--code/api/src/Data/Results/ErrorResult.cs12
-rw-r--r--code/api/src/Data/Static/AppClaims.cs8
-rw-r--r--code/api/src/Data/Static/AppConfiguration.cs58
-rw-r--r--code/api/src/Data/Static/AppConstants.cs12
-rw-r--r--code/api/src/Data/Static/AppDateTime.cs16
-rw-r--r--code/api/src/Data/Static/AppEnvironmentVariables.cs21
-rw-r--r--code/api/src/Data/Static/AppHeaders.cs7
-rw-r--r--code/api/src/Data/Static/AppPaths.cs17
-rw-r--r--code/api/src/Data/Static/JsonSettings.cs11
37 files changed, 869 insertions, 0 deletions
diff --git a/code/api/src/Data/AppDbContext.cs b/code/api/src/Data/AppDbContext.cs
new file mode 100644
index 0000000..c970429
--- /dev/null
+++ b/code/api/src/Data/AppDbContext.cs
@@ -0,0 +1,51 @@
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+
+namespace IOL.GreatOffice.Api.Data;
+
+public class AppDbContext : DbContext, IDataProtectionKeyContext
+{
+ public AppDbContext(DbContextOptions<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/ApiAccessToken.cs
new file mode 100644
index 0000000..9582869
--- /dev/null
+++ b/code/api/src/Data/Database/ApiAccessToken.cs
@@ -0,0 +1,31 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ApiAccessToken : Base
+{
+ public User User { get; set; }
+ public DateTime ExpiryDate { get; set; }
+ public bool AllowRead { get; set; }
+ public bool AllowCreate { get; set; }
+ public bool AllowUpdate { get; set; }
+ public bool AllowDelete { get; set; }
+ public bool HasExpired => ExpiryDate < AppDateTime.UtcNow;
+ public ApiAccessTokenDto AsDto => new(this);
+
+ public class ApiAccessTokenDto
+ {
+ public ApiAccessTokenDto(ApiAccessToken source) {
+ ExpiryDate = source.ExpiryDate;
+ AllowRead = source.AllowRead;
+ AllowCreate = source.AllowCreate;
+ AllowUpdate = source.AllowUpdate;
+ AllowDelete = source.AllowDelete;
+ }
+
+ public DateTime ExpiryDate { get; set; }
+ public bool AllowRead { get; set; }
+ public bool AllowCreate { get; set; }
+ public bool AllowUpdate { get; set; }
+ public bool AllowDelete { get; set; }
+ public bool HasExpired => ExpiryDate < AppDateTime.UtcNow;
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Base.cs b/code/api/src/Data/Database/Base.cs
new file mode 100644
index 0000000..ae9efa2
--- /dev/null
+++ b/code/api/src/Data/Database/Base.cs
@@ -0,0 +1,15 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Base
+{
+ protected Base() {
+ Id = Guid.NewGuid();
+ CreatedAt = AppDateTime.UtcNow;
+ }
+
+ public Guid Id { get; init; }
+ public DateTime CreatedAt { get; init; }
+ public DateTime? ModifiedAt { get; private set; }
+ public bool Deleted { get; set; }
+ public void Modified() => ModifiedAt = AppDateTime.UtcNow;
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/BaseWithOwner.cs b/code/api/src/Data/Database/BaseWithOwner.cs
new file mode 100644
index 0000000..1eb99f4
--- /dev/null
+++ b/code/api/src/Data/Database/BaseWithOwner.cs
@@ -0,0 +1,19 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+/// <summary>
+/// Base class for all entities.
+/// </summary>
+public class BaseWithOwner : Base
+{
+ protected BaseWithOwner() { }
+
+ protected BaseWithOwner(Guid userId) {
+ UserId = userId;
+ }
+
+ public Guid? UserId { get; set; }
+ public Guid? TenantId { get; init; }
+ public Guid? ModifiedById { get; init; }
+ public Guid? CreatedById { get; init; }
+ public Guid? DeletedById { get; init; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/Customer.cs b/code/api/src/Data/Database/Customer.cs
new file mode 100644
index 0000000..c6b06a4
--- /dev/null
+++ b/code/api/src/Data/Database/Customer.cs
@@ -0,0 +1,6 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Customer : BaseWithOwner
+{
+ public string Name { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Database/CustomerContact.cs b/code/api/src/Data/Database/CustomerContact.cs
new file mode 100644
index 0000000..f5a951d
--- /dev/null
+++ b/code/api/src/Data/Database/CustomerContact.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerContact : BaseWithOwner
+{
+ public Customer Customer { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public string Phone { get; set; }
+ public string WorkTitle { get; set; }
+ public string Note { get; set; }
+}
diff --git a/code/api/src/Data/Database/CustomerEvent.cs b/code/api/src/Data/Database/CustomerEvent.cs
new file mode 100644
index 0000000..da3e3ed
--- /dev/null
+++ b/code/api/src/Data/Database/CustomerEvent.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class CustomerEvent : BaseWithOwner
+{
+ public Customer Customer { get; set; }
+ public string Name { get; set; }
+}
diff --git a/code/api/src/Data/Database/ForgotPasswordRequest.cs b/code/api/src/Data/Database/ForgotPasswordRequest.cs
new file mode 100644
index 0000000..1510a35
--- /dev/null
+++ b/code/api/src/Data/Database/ForgotPasswordRequest.cs
@@ -0,0 +1,23 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class ForgotPasswordRequest
+{
+ public ForgotPasswordRequest() { }
+
+ public ForgotPasswordRequest(User user) {
+ CreatedAt = AppDateTime.UtcNow;
+ Id = Guid.NewGuid();
+ User = user;
+ }
+
+ public Guid Id { get; set; }
+ public Guid UserId { get; set; }
+ public User User { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ [NotMapped]
+ public DateTime ExpirationDate => CreatedAt.AddMinutes(15);
+
+ [NotMapped]
+ public bool IsExpired => DateTime.Compare(ExpirationDate, AppDateTime.UtcNow) < 0;
+}
diff --git a/code/api/src/Data/Database/Project.cs b/code/api/src/Data/Database/Project.cs
new file mode 100644
index 0000000..7e694ee
--- /dev/null
+++ b/code/api/src/Data/Database/Project.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Project : BaseWithOwner
+{
+ public string Name { get; set; }
+ public Guid? CustomerId { get; set; }
+}
diff --git a/code/api/src/Data/Database/Tenant.cs b/code/api/src/Data/Database/Tenant.cs
new file mode 100644
index 0000000..b185c7a
--- /dev/null
+++ b/code/api/src/Data/Database/Tenant.cs
@@ -0,0 +1,11 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Tenant : BaseWithOwner
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string ContactEmail { get; set; }
+ public Guid MasterUserId { get; set; }
+ public string MasterUserPassword { get; set; }
+ public ICollection<User> Users { get; set; }
+}
diff --git a/code/api/src/Data/Database/TimeCategory.cs b/code/api/src/Data/Database/TimeCategory.cs
new file mode 100644
index 0000000..69c6957
--- /dev/null
+++ b/code/api/src/Data/Database/TimeCategory.cs
@@ -0,0 +1,31 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeCategory : BaseWithOwner
+{
+ public TimeCategory() { }
+ public TimeCategory(Guid userId) : base(userId) { }
+ public string Name { get; set; }
+ public string Color { get; set; }
+ public TimeCategoryDto AsDto => new(this);
+
+ public class TimeCategoryDto
+ {
+ public TimeCategoryDto() { }
+
+ public TimeCategoryDto(TimeCategory sourceEntry = default) {
+ if (sourceEntry == default) {
+ return;
+ }
+
+ Id = sourceEntry.Id;
+ ModifiedAt = sourceEntry.ModifiedAt;
+ Name = sourceEntry.Name;
+ Color = sourceEntry.Color;
+ }
+
+ public Guid Id { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+ public string Name { get; set; }
+ public string Color { get; set; }
+ }
+}
diff --git a/code/api/src/Data/Database/TimeEntry.cs b/code/api/src/Data/Database/TimeEntry.cs
new file mode 100644
index 0000000..46c62e1
--- /dev/null
+++ b/code/api/src/Data/Database/TimeEntry.cs
@@ -0,0 +1,45 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeEntry : BaseWithOwner
+{
+ public TimeEntry() { }
+ public TimeEntry(Guid userId) : base(userId) { }
+ public DateTime Start { get; set; }
+ public DateTime Stop { get; set; }
+ public string Description { get; set; }
+ public ICollection<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
new file mode 100644
index 0000000..55e20b0
--- /dev/null
+++ b/code/api/src/Data/Database/TimeLabel.cs
@@ -0,0 +1,31 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TimeLabel : BaseWithOwner
+{
+ public TimeLabel() { }
+ public TimeLabel(Guid userId) : base(userId) { }
+ public string Name { get; set; }
+ public string Color { get; set; }
+
+ [NotMapped]
+ public TimeLabelDto AsDto => new(this);
+
+ public class TimeLabelDto
+ {
+ public TimeLabelDto() { }
+
+ public TimeLabelDto(TimeLabel sourceEntry) {
+ Id = sourceEntry.Id;
+ CreatedAt = sourceEntry.CreatedAt;
+ ModifiedAt = sourceEntry.ModifiedAt;
+ Name = sourceEntry.Name;
+ Color = sourceEntry.Color;
+ }
+
+ public Guid Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime? ModifiedAt { get; set; }
+ public string Name { get; set; }
+ public string Color { get; set; }
+ }
+}
diff --git a/code/api/src/Data/Database/Todo.cs b/code/api/src/Data/Database/Todo.cs
new file mode 100644
index 0000000..5fe3c9a
--- /dev/null
+++ b/code/api/src/Data/Database/Todo.cs
@@ -0,0 +1,13 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class Todo : BaseWithOwner
+{
+ public int PublicId { get; set; }
+ public TodoStatus Status { get; set; }
+ public TodoProject Project { get; set; }
+ public Guid? AssignedUserId { get; set; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public ICollection<TodoLabel> Labels { get; set; }
+ public ICollection<TodoComment> Comments { get; set; }
+}
diff --git a/code/api/src/Data/Database/TodoComment.cs b/code/api/src/Data/Database/TodoComment.cs
new file mode 100644
index 0000000..44dcbed
--- /dev/null
+++ b/code/api/src/Data/Database/TodoComment.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoComment : BaseWithOwner
+{
+ public string Value { get; set; }
+ public Todo Todo { get; set; }
+}
diff --git a/code/api/src/Data/Database/TodoLabel.cs b/code/api/src/Data/Database/TodoLabel.cs
new file mode 100644
index 0000000..7753ade
--- /dev/null
+++ b/code/api/src/Data/Database/TodoLabel.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoLabel : BaseWithOwner
+{
+ public string Name { get; set; }
+ public string Color { get; set; }
+ public Todo Todo { get; set; }
+}
diff --git a/code/api/src/Data/Database/TodoProject.cs b/code/api/src/Data/Database/TodoProject.cs
new file mode 100644
index 0000000..0a4a7be
--- /dev/null
+++ b/code/api/src/Data/Database/TodoProject.cs
@@ -0,0 +1,16 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoProject : BaseWithOwner
+{
+ public string Name { get; set; }
+ public TodoVisibility Visibility { get; set; }
+ public Guid? ProjectId { get; set; }
+}
+
+public enum TodoVisibility
+{
+ PRIVATE = 0,
+ UNLISTED = 1,
+ TENANT_WIDE = 2,
+ PUBLIC = 3,
+}
diff --git a/code/api/src/Data/Database/TodoProjectAccessControl.cs b/code/api/src/Data/Database/TodoProjectAccessControl.cs
new file mode 100644
index 0000000..964f831
--- /dev/null
+++ b/code/api/src/Data/Database/TodoProjectAccessControl.cs
@@ -0,0 +1,11 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoProjectAccessControl
+{
+ public TodoProject Project { get; set; }
+ public Guid? UserId { get; set; }
+ public bool Browse { get; set; }
+ public bool Submit { get; set; }
+ public bool Comment { get; set; }
+ public bool Edit { get; set; }
+}
diff --git a/code/api/src/Data/Database/TodoStatus.cs b/code/api/src/Data/Database/TodoStatus.cs
new file mode 100644
index 0000000..416212d
--- /dev/null
+++ b/code/api/src/Data/Database/TodoStatus.cs
@@ -0,0 +1,45 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class TodoStatus : BaseWithOwner
+{
+ public string Name { get; set; }
+ public string Color { get; set; }
+ public Todo Todo { get; set; }
+
+ public static List<TodoStatus> GetDefaultStatusSetForTenant(Guid tenantId) {
+ 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/Database/User.cs b/code/api/src/Data/Database/User.cs
new file mode 100644
index 0000000..9db5d35
--- /dev/null
+++ b/code/api/src/Data/Database/User.cs
@@ -0,0 +1,37 @@
+namespace IOL.GreatOffice.Api.Data.Database;
+
+public class User : Base
+{
+ public User() { }
+
+ public User(string username) {
+ Username = username;
+ }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public ICollection<Tenant> Tenants { get; set; }
+
+ public string DisplayName() {
+ if (FirstName.HasValue() && LastName.HasValue()) return FirstName + " " + LastName;
+ return FirstName.HasValue() ? FirstName : Email;
+ }
+
+ public void HashAndSetPassword(string password) {
+ Password = PasswordHelper.HashPassword(password);
+ }
+
+ public bool VerifyPassword(string password) {
+ return PasswordHelper.Verify(password, Password);
+ }
+
+ public IEnumerable<Claim> DefaultClaims() {
+ return new Claim[] {
+ new(AppClaims.USER_ID, Id.ToString()),
+ new(AppClaims.NAME, Username),
+ };
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Data/Dtos/TimeQueryDto.cs b/code/api/src/Data/Dtos/TimeQueryDto.cs
new file mode 100644
index 0000000..f734cb1
--- /dev/null
+++ b/code/api/src/Data/Dtos/TimeQueryDto.cs
@@ -0,0 +1,34 @@
+
+namespace IOL.GreatOffice.Api.Data.Dtos;
+
+public class TimeQueryDto
+{
+ public TimeQueryDto() {
+ Results = new List<TimeEntry.TimeEntryDto>();
+ }
+
+ /// <summary>
+ /// List of entries.
+ /// </summary>
+ public List<TimeEntry.TimeEntryDto> Results { get; set; }
+
+ /// <summary>
+ /// Curren page.
+ /// </summary>
+ public int Page { get; set; }
+
+ /// <summary>
+ /// Maximum count of entries in a page.
+ /// </summary>
+ public int PageSize { get; set; }
+
+ /// <summary>
+ /// Total count of entries.
+ /// </summary>
+ public int TotalSize { get; set; }
+
+ /// <summary>
+ /// Total count of pages.
+ /// </summary>
+ public int TotalPageCount { get; set; }
+}
diff --git a/code/api/src/Data/Dtos/UserArchiveDto.cs b/code/api/src/Data/Dtos/UserArchiveDto.cs
new file mode 100644
index 0000000..42e0600
--- /dev/null
+++ b/code/api/src/Data/Dtos/UserArchiveDto.cs
@@ -0,0 +1,131 @@
+
+namespace IOL.GreatOffice.Api.Data.Dtos;
+
+/// <summary>
+/// Represents a user archive as it is provided to users.
+/// </summary>
+public class UserArchiveDto
+{
+ /// <inheritdoc cref="UserArchiveDto"/>
+ public UserArchiveDto(User user) {
+ Meta = new MetaDto {
+ GeneratedAt = AppDateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ };
+ User = new UserDto(user);
+ Entries = new List<EntryDto>();
+ }
+
+ /// <summary>
+ /// Metadata for the user archive.
+ /// </summary>
+ public MetaDto Meta { get; }
+
+ /// <summary>
+ /// Relevant user data for the archive.
+ /// </summary>
+ public UserDto User { get; }
+
+ /// <summary>
+ /// List of entries that the user has created.
+ /// </summary>
+ public List<EntryDto> Entries { get; }
+
+ public void CountEntries() {
+ Meta.EntryCount = Entries.Count;
+ }
+
+ /// <summary>
+ /// Represents a time entry in the data archive.
+ /// </summary>
+ public class EntryDto
+ {
+ public string CreatedAt { get; init; }
+
+ [JsonIgnore]
+ public DateTime StartDateTime { get; init; }
+
+ /// <summary>
+ /// ISO 8601 string of the UTC date the time entry started.
+ /// </summary>
+ public string Start => StartDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
+
+ [JsonIgnore]
+ public DateTime StopDateTime { get; init; }
+
+ /// <summary>
+ /// ISO 8601 string of the UTC date the time entry stopped.
+ /// </summary>
+ public string Stop => StopDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
+
+ /// <summary>
+ /// Total amount of minutes elapsed from start to stop on this time entry.
+ /// </summary>
+ public double Minutes => StopDateTime.Subtract(StartDateTime).TotalMinutes;
+
+ public string Description { get; init; }
+
+ /// <summary>
+ /// Archive spesific category for this time entry.
+ /// </summary>
+ public CategoryDto Category { get; init; }
+
+ /// <summary>
+ /// Archive spesific list of labels for this time entry.
+ /// </summary>
+ public List<LabelDto> Labels { get; init; }
+ }
+
+ /// <summary>
+ /// Time entry category as it is written to the user archive.
+ /// </summary>
+ public class CategoryDto
+ {
+ public string Name { get; init; }
+ public string Color { get; init; }
+ }
+
+ /// <summary>
+ /// Time entry label as it is written to the user archive.
+ /// </summary>
+ public class LabelDto
+ {
+ public string Name { get; init; }
+ public string Color { get; init; }
+ }
+
+
+ /// <summary>
+ /// Represents the user who this archive's data is based on.
+ /// </summary>
+ public class UserDto
+ {
+ /// <inheritdoc cref="UserDto"/>
+ public UserDto(User user) {
+ Username = user.Username;
+ CreatedAt = user.CreatedAt;
+ }
+
+ /// <summary>
+ /// UTC date this user was created.
+ /// </summary>
+ public DateTime CreatedAt { get; }
+
+ public string Username { get; }
+ }
+
+ /// <summary>
+ /// Represents the meta object which contains metdata for this archive.
+ /// </summary>
+ public class MetaDto
+ {
+ /// <summary>
+ /// ISO 8601 UTC date string for when this archive was created.
+ /// </summary>
+ public string GeneratedAt { get; init; }
+
+ /// <summary>
+ /// Amount of entries in the archive.
+ /// </summary>
+ public int EntryCount { get; set; }
+ }
+}
diff --git a/code/api/src/Data/Enums/TimeEntryQueryDuration.cs b/code/api/src/Data/Enums/TimeEntryQueryDuration.cs
new file mode 100644
index 0000000..af70ca6
--- /dev/null
+++ b/code/api/src/Data/Enums/TimeEntryQueryDuration.cs
@@ -0,0 +1,37 @@
+namespace IOL.GreatOffice.Api.Data.Enums;
+
+/// <summary>
+/// Specify a duration filter for time entry queries.
+/// </summary>
+public enum TimeEntryQueryDuration
+{
+ /// <summary>
+ /// Only query entries created today.
+ /// </summary>
+ TODAY = 0,
+
+ /// <summary>
+ /// Only query entries created this week.
+ /// </summary>
+ THIS_WEEK = 1,
+
+ /// <summary>
+ /// Only query entries created this month.
+ /// </summary>
+ THIS_MONTH = 2,
+
+ /// <summary>
+ /// Only query entries created this year.
+ /// </summary>
+ THIS_YEAR = 3,
+
+ /// <summary>
+ /// Only query entries created at a spesific date.
+ /// </summary>
+ SPECIFIC_DATE = 4,
+
+ /// <summary>
+ /// Only query entries created between two dates.
+ /// </summary>
+ DATE_RANGE = 5,
+}
diff --git a/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs b/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs
new file mode 100644
index 0000000..02474b4
--- /dev/null
+++ b/code/api/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs
@@ -0,0 +1,21 @@
+namespace IOL.GreatOffice.Api.Data.Exceptions;
+
+[Serializable]
+public class ForgotPasswordRequestNotFoundException : Exception
+{
+ //
+ // For guidelines regarding the creation of new exception types, see
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
+ // and
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
+ //
+
+ public ForgotPasswordRequestNotFoundException() { }
+ public ForgotPasswordRequestNotFoundException(string message) : base(message) { }
+ public ForgotPasswordRequestNotFoundException(string message, Exception inner) : base(message, inner) { }
+
+ protected ForgotPasswordRequestNotFoundException(
+ SerializationInfo info,
+ StreamingContext context
+ ) : base(info, context) { }
+}
diff --git a/code/api/src/Data/Exceptions/UserNotFoundException.cs b/code/api/src/Data/Exceptions/UserNotFoundException.cs
new file mode 100644
index 0000000..06b57a9
--- /dev/null
+++ b/code/api/src/Data/Exceptions/UserNotFoundException.cs
@@ -0,0 +1,19 @@
+namespace IOL.GreatOffice.Api.Data.Exceptions;
+
+[Serializable]
+public class UserNotFoundException : Exception
+{
+ // For guidelines regarding the creation of new exception types, see
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
+ // and
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
+
+ public UserNotFoundException() { }
+ public UserNotFoundException(string message) : base(message) { }
+ public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
+
+ protected UserNotFoundException(
+ SerializationInfo info,
+ StreamingContext context
+ ) : base(info, context) { }
+}
diff --git a/code/api/src/Data/Models/ApiSpecDocument.cs b/code/api/src/Data/Models/ApiSpecDocument.cs
new file mode 100644
index 0000000..1c7d936
--- /dev/null
+++ b/code/api/src/Data/Models/ApiSpecDocument.cs
@@ -0,0 +1,9 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class ApiSpecDocument
+{
+ public string VersionName { get; set; }
+ public string SwaggerPath { get; set; }
+ public ApiVersion Version { get; set; }
+ public OpenApiInfo OpenApiInfo { get; set; }
+}
diff --git a/code/api/src/Data/Models/AppPath.cs b/code/api/src/Data/Models/AppPath.cs
new file mode 100644
index 0000000..e47e48c
--- /dev/null
+++ b/code/api/src/Data/Models/AppPath.cs
@@ -0,0 +1,23 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public sealed record AppPath
+{
+ public string HostPath { get; init; }
+ public string WebPath { get; init; }
+
+ public string GetHostPathForFilename(string filename, string fallback = "") {
+ if (filename.IsNullOrWhiteSpace()) {
+ return fallback;
+ }
+
+ return Path.Combine(HostPath, filename);
+ }
+
+ public string GetWebPathForFilename(string filename, string fallback = "") {
+ if (filename.IsNullOrWhiteSpace()) {
+ return fallback;
+ }
+
+ return Path.Combine(WebPath, filename);
+ }
+}
diff --git a/code/api/src/Data/Models/LoggedInUserModel.cs b/code/api/src/Data/Models/LoggedInUserModel.cs
new file mode 100644
index 0000000..d802b77
--- /dev/null
+++ b/code/api/src/Data/Models/LoggedInUserModel.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Models;
+
+public class LoggedInUserModel
+{
+ public Guid Id { get; set; }
+ public string Username { get; set; }
+}
diff --git a/code/api/src/Data/Results/ErrorResult.cs b/code/api/src/Data/Results/ErrorResult.cs
new file mode 100644
index 0000000..fd2fd6a
--- /dev/null
+++ b/code/api/src/Data/Results/ErrorResult.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Results;
+
+public class ErrorResult
+{
+ public ErrorResult(string title = default, string text = default) {
+ Title = title;
+ Text = text;
+ }
+
+ public string Title { get; set; }
+ public string Text { get; set; }
+}
diff --git a/code/api/src/Data/Static/AppClaims.cs b/code/api/src/Data/Static/AppClaims.cs
new file mode 100644
index 0000000..8b6d3a8
--- /dev/null
+++ b/code/api/src/Data/Static/AppClaims.cs
@@ -0,0 +1,8 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppClaims
+{
+ public const string USER_ID = "user_id";
+ public const string NAME = "name";
+ public const string GITHUB_ACCESS_TOKEN = "";
+}
diff --git a/code/api/src/Data/Static/AppConfiguration.cs b/code/api/src/Data/Static/AppConfiguration.cs
new file mode 100644
index 0000000..4ee7a8e
--- /dev/null
+++ b/code/api/src/Data/Static/AppConfiguration.cs
@@ -0,0 +1,58 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public class AppConfiguration
+{
+ public string DB_HOST { get; set; }
+ public string DB_PORT { get; set; }
+ public string DB_USER { get; set; }
+ public string DB_PASSWORD { get; set; }
+ public string DB_NAME { get; set; }
+ public string QUARTZ_DB_HOST { get; set; }
+ public string QUARTZ_DB_PORT { get; set; }
+ public string QUARTZ_DB_USER { get; set; }
+ public string QUARTZ_DB_PASSWORD { get; set; }
+ public string QUARTZ_DB_NAME { get; set; }
+ public string SEQ_API_KEY { get; set; }
+ public string SEQ_API_URL { get; set; }
+ public string SMTP_HOST { get; set; }
+ public string SMTP_PORT { get; set; }
+ public string SMTP_USER { get; set; }
+ public string SMTP_PASSWORD { get; set; }
+ public string EMAIL_FROM_ADDRESS { get; set; }
+ public string EMAIL_FROM_DISPLAY_NAME { get; set; }
+ public string PORTAL_URL { get; set; }
+ public string GITHUB_CLIENT_ID { get; set; }
+ public string GITHUB_CLIENT_SECRET { get; set; }
+ public string APP_AES_KEY { get; set; }
+ public string APP_CERT { get; set; }
+
+ public X509Certificate2 CERT1() => new (Convert.FromBase64String(APP_CERT));
+
+ public object GetPublicVersion() {
+ return new {
+ DB_HOST,
+ DB_PORT,
+ DB_USER,
+ DB_PASSWORD = DB_PASSWORD.Obfuscate() ?? "",
+ QUARTZ_DB_HOST,
+ QUARTZ_DB_PORT,
+ QUARTZ_DB_USER,
+ QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate() ?? "",
+ SEQ_API_KEY = SEQ_API_KEY.Obfuscate() ?? "",
+ SEQ_API_URL,
+ SMTP_HOST,
+ SMTP_PORT,
+ SMTP_USER = SMTP_USER.Obfuscate() ?? "",
+ SMTP_PASSWORD = SMTP_PASSWORD.Obfuscate() ?? "",
+ EMAIL_FROM_ADDRESS,
+ EMAIL_FROM_DISPLAY_NAME,
+ PORTAL_URL,
+ GITHUB_CLIENT_ID = GITHUB_CLIENT_ID.Obfuscate() ?? "",
+ GITHUB_CLIENT_SECRET = GITHUB_CLIENT_SECRET.Obfuscate() ?? "",
+ APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "",
+ CERT1 = CERT1().PublicKey.Oid.FriendlyName
+ };
+ }
+}
diff --git a/code/api/src/Data/Static/AppConstants.cs b/code/api/src/Data/Static/AppConstants.cs
new file mode 100644
index 0000000..461317b
--- /dev/null
+++ b/code/api/src/Data/Static/AppConstants.cs
@@ -0,0 +1,12 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppConstants
+{
+ public const string API_NAME = "Great Office API";
+ public const string BASIC_AUTH_SCHEME = "BasicAuthenticationScheme";
+ public const string TOKEN_ALLOW_READ = "TOKEN_ALLOW_READ";
+ public const string TOKEN_ALLOW_CREATE = "TOKEN_ALLOW_CREATE";
+ public const string TOKEN_ALLOW_UPDATE = "TOKEN_ALLOW_UPDATE";
+ public const string TOKEN_ALLOW_DELETE = "TOKEN_ALLOW_DELETE";
+ public const string VAULT_CACHE_KEY = "VAULT_CACHE_KEY";
+}
diff --git a/code/api/src/Data/Static/AppDateTime.cs b/code/api/src/Data/Static/AppDateTime.cs
new file mode 100644
index 0000000..880d2a8
--- /dev/null
+++ b/code/api/src/Data/Static/AppDateTime.cs
@@ -0,0 +1,16 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppDateTime
+{
+ private static DateTime? dateTime;
+
+ public static DateTime UtcNow => dateTime ?? DateTime.UtcNow;
+
+ public static void Set(DateTime setDateTime) {
+ dateTime = setDateTime;
+ }
+
+ public static void Reset() {
+ dateTime = null;
+ }
+}
diff --git a/code/api/src/Data/Static/AppEnvironmentVariables.cs b/code/api/src/Data/Static/AppEnvironmentVariables.cs
new file mode 100644
index 0000000..c3f821d
--- /dev/null
+++ b/code/api/src/Data/Static/AppEnvironmentVariables.cs
@@ -0,0 +1,21 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppEnvironmentVariables
+{
+ /// <summary>
+ /// An access token that can be used to access the Hashicorp Vault instance that is available at VAULT_URL
+ /// </summary>
+ public const string VAULT_TOKEN = "VAULT_TOKEN";
+ /// <summary>
+ /// An url pointing to the Hashicorp Vault instance the app should use
+ /// </summary>
+ public const string VAULT_URL = "VAULT_URL";
+ /// <summary>
+ /// The duration of which to keep a local cached version of the configuration
+ /// </summary>
+ public const string VAULT_CACHE_TTL = "VAULT_CACHE_TTL";
+ /// <summary>
+ /// The vault key name for the main configuration json object, described by <see cref="AppConfiguration"/>
+ /// </summary>
+ public const string MAIN_CONFIG_SHEET = "MAIN_CONFIG_SHEET";
+}
diff --git a/code/api/src/Data/Static/AppHeaders.cs b/code/api/src/Data/Static/AppHeaders.cs
new file mode 100644
index 0000000..7912418
--- /dev/null
+++ b/code/api/src/Data/Static/AppHeaders.cs
@@ -0,0 +1,7 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppHeaders
+{
+ public const string BROWSER_TIME_ZONE = "X-TimeZone";
+ public const string VAULT_TOKEN = "X-Vault-Token";
+}
diff --git a/code/api/src/Data/Static/AppPaths.cs b/code/api/src/Data/Static/AppPaths.cs
new file mode 100644
index 0000000..a24f5af
--- /dev/null
+++ b/code/api/src/Data/Static/AppPaths.cs
@@ -0,0 +1,17 @@
+
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class AppPaths
+{
+ public static AppPath AppData => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData")
+ };
+
+ public static AppPath DataProtectionKeys => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "dp-keys")
+ };
+
+ public static AppPath Frontend => new() {
+ HostPath = Path.Combine(Directory.GetCurrentDirectory(), "Frontend")
+ };
+}
diff --git a/code/api/src/Data/Static/JsonSettings.cs b/code/api/src/Data/Static/JsonSettings.cs
new file mode 100644
index 0000000..a163c11
--- /dev/null
+++ b/code/api/src/Data/Static/JsonSettings.cs
@@ -0,0 +1,11 @@
+namespace IOL.GreatOffice.Api.Data.Static;
+
+public static class JsonSettings
+{
+ public static Action<JsonOptions> Default { get; } = options => {
+ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
+ options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
+ options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ };
+}