diff options
Diffstat (limited to 'server/src')
111 files changed, 9897 insertions, 0 deletions
diff --git a/server/src/Data/AppDbContext.cs b/server/src/Data/AppDbContext.cs new file mode 100644 index 0000000..3f949dd --- /dev/null +++ b/server/src/Data/AppDbContext.cs @@ -0,0 +1,58 @@ +namespace IOL.GreatOffice.Api.Data; + +public class AppDbContext : DbContext +{ + 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<GithubUserMapping> GithubUserMappings { get; set; } + public DbSet<ApiAccessToken> AccessTokens { get; set; } + public DbSet<Tenant> Tenants { 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.HasOne(c => c.User); + e.ToTable("time_categories"); + }); + + modelBuilder.Entity<TimeLabel>(e => { + e.HasOne(c => c.User); + e.ToTable("time_labels"); + }); + + modelBuilder.Entity<TimeEntry>(e => { + e.HasOne(c => c.User); + e.HasOne(c => c.Category); + e.HasMany(c => c.Labels); + e.ToTable("time_entries"); + }); + + modelBuilder.Entity<GithubUserMapping>(e => { + e.HasOne(c => c.User); + e.HasKey(c => c.GithubId); + e.ToTable("github_user_mappings"); + }); + + modelBuilder.Entity<ApiAccessToken>(e => { + e.ToTable("api_access_tokens"); + }); + + modelBuilder.Entity<Tenant>(e => { + e.ToTable("tenants"); + }); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/server/src/Data/Database/ApiAccessToken.cs b/server/src/Data/Database/ApiAccessToken.cs new file mode 100644 index 0000000..3eff5f3 --- /dev/null +++ b/server/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 < DateTime.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 < DateTime.UtcNow; + } +} diff --git a/server/src/Data/Database/Base.cs b/server/src/Data/Database/Base.cs new file mode 100644 index 0000000..2439668 --- /dev/null +++ b/server/src/Data/Database/Base.cs @@ -0,0 +1,14 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class Base +{ + protected Base() { + Id = Guid.NewGuid(); + CreatedAt = DateTime.UtcNow; + } + + public Guid Id { get; init; } + public DateTime CreatedAt { get; init; } + public DateTime? ModifiedAt { get; private set; } + public void Modified() => ModifiedAt = DateTime.UtcNow; +} diff --git a/server/src/Data/Database/BaseWithOwner.cs b/server/src/Data/Database/BaseWithOwner.cs new file mode 100644 index 0000000..eb4438d --- /dev/null +++ b/server/src/Data/Database/BaseWithOwner.cs @@ -0,0 +1,24 @@ +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; init; } + public User User { get; init; } + public Guid TenantId { get; set; } + public Tenant Tenant { get; init; } + public Guid ModifiedById { get; init; } + public User ModifiedBy { get; set; } + public Guid CreatedById { get; init; } + public User CreatedBy { get; set; } + public Guid DeletedById { get; init; } + public User DeletedBy { get; set; } +} diff --git a/server/src/Data/Database/ForgotPasswordRequest.cs b/server/src/Data/Database/ForgotPasswordRequest.cs new file mode 100644 index 0000000..164f09d --- /dev/null +++ b/server/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 = DateTime.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, DateTime.UtcNow) < 0; +} diff --git a/server/src/Data/Database/GithubUserMapping.cs b/server/src/Data/Database/GithubUserMapping.cs new file mode 100644 index 0000000..dbdb2b7 --- /dev/null +++ b/server/src/Data/Database/GithubUserMapping.cs @@ -0,0 +1,9 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class GithubUserMapping +{ + public User User { get; set; } + public string GithubId { get; set; } + public string Email { get; set; } + public string RefreshToken { get; set; } +} diff --git a/server/src/Data/Database/Tenant.cs b/server/src/Data/Database/Tenant.cs new file mode 100644 index 0000000..3028d13 --- /dev/null +++ b/server/src/Data/Database/Tenant.cs @@ -0,0 +1,10 @@ +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; } +} diff --git a/server/src/Data/Database/TimeCategory.cs b/server/src/Data/Database/TimeCategory.cs new file mode 100644 index 0000000..69c6957 --- /dev/null +++ b/server/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/server/src/Data/Database/TimeEntry.cs b/server/src/Data/Database/TimeEntry.cs new file mode 100644 index 0000000..46c62e1 --- /dev/null +++ b/server/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/server/src/Data/Database/TimeLabel.cs b/server/src/Data/Database/TimeLabel.cs new file mode 100644 index 0000000..55e20b0 --- /dev/null +++ b/server/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/server/src/Data/Database/User.cs b/server/src/Data/Database/User.cs new file mode 100644 index 0000000..c5063f6 --- /dev/null +++ b/server/src/Data/Database/User.cs @@ -0,0 +1,29 @@ +namespace IOL.GreatOffice.Api.Data.Database; + +public class User : Base +{ + public User() { } + + public User(string username) { + Username = username; + } + + public string Username { get; set; } + public string Password { get; set; } + + + 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), + }; + } +} diff --git a/server/src/Data/Dtos/TimeQueryDto.cs b/server/src/Data/Dtos/TimeQueryDto.cs new file mode 100644 index 0000000..f734cb1 --- /dev/null +++ b/server/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/server/src/Data/Dtos/UserArchiveDto.cs b/server/src/Data/Dtos/UserArchiveDto.cs new file mode 100644 index 0000000..63b1470 --- /dev/null +++ b/server/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 = DateTime.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/server/src/Data/Enums/TimeEntryQueryDuration.cs b/server/src/Data/Enums/TimeEntryQueryDuration.cs new file mode 100644 index 0000000..af70ca6 --- /dev/null +++ b/server/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/server/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs b/server/src/Data/Exceptions/ForgotPasswordRequestNotFoundException.cs new file mode 100644 index 0000000..02474b4 --- /dev/null +++ b/server/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/server/src/Data/Exceptions/UserNotFoundException.cs b/server/src/Data/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..06b57a9 --- /dev/null +++ b/server/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/server/src/Data/Models/ApiSpecDocument.cs b/server/src/Data/Models/ApiSpecDocument.cs new file mode 100644 index 0000000..1c7d936 --- /dev/null +++ b/server/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/server/src/Data/Models/AppPath.cs b/server/src/Data/Models/AppPath.cs new file mode 100644 index 0000000..e47e48c --- /dev/null +++ b/server/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/server/src/Data/Models/LoggedInUserModel.cs b/server/src/Data/Models/LoggedInUserModel.cs new file mode 100644 index 0000000..4a5bef9 --- /dev/null +++ b/server/src/Data/Models/LoggedInUserModel.cs @@ -0,0 +1,8 @@ +namespace IOL.GreatOffice.Api.Data.Models; + +public class LoggedInUserModel +{ + public LoggedInUserModel() { } + public Guid Id { get; set; } + public string Username { get; set; } +} diff --git a/server/src/Data/Results/ErrorResult.cs b/server/src/Data/Results/ErrorResult.cs new file mode 100644 index 0000000..fd2fd6a --- /dev/null +++ b/server/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/server/src/Data/Static/AppClaims.cs b/server/src/Data/Static/AppClaims.cs new file mode 100644 index 0000000..8b6d3a8 --- /dev/null +++ b/server/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/server/src/Data/Static/AppConstants.cs b/server/src/Data/Static/AppConstants.cs new file mode 100644 index 0000000..61e5cd5 --- /dev/null +++ b/server/src/Data/Static/AppConstants.cs @@ -0,0 +1,11 @@ +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"; +} diff --git a/server/src/Data/Static/AppEnvironmentVariables.cs b/server/src/Data/Static/AppEnvironmentVariables.cs new file mode 100644 index 0000000..a734146 --- /dev/null +++ b/server/src/Data/Static/AppEnvironmentVariables.cs @@ -0,0 +1,27 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class AppEnvironmentVariables +{ + public const string DB_HOST = "DB_HOST"; + public const string DB_PORT = "DB_PORT"; + public const string DB_USER = "DB_USER"; + public const string DB_PASSWORD = "DB_PASSWORD"; + public const string DB_NAME = "DB_NAME"; + public const string QUARTZ_DB_HOST = "QUARTZ_DB_HOST"; + public const string QUARTZ_DB_PORT = "QUARTZ_DB_PORT"; + public const string QUARTZ_DB_USER = "QUARTZ_DB_USER"; + public const string QUARTZ_DB_PASSWORD = "QUARTZ_DB_PASSWORD"; + public const string QUARTZ_DB_NAME = "QUARTZ_DB_NAME"; + public const string SEQ_API_KEY = "SEQ_API_KEY"; + public const string SEQ_API_URL = "SEQ_API_URL"; + public const string SMTP_HOST = "SMTP_HOST"; + public const string SMTP_PORT = "SMTP_PORT"; + public const string SMTP_USER = "SMTP_USER"; + public const string SMTP_PASSWORD = "SMTP_PASSWORD"; + public const string EMAIL_FROM_ADDRESS = "EMAIL_FROM_ADDRESS"; + public const string EMAIL_FROM_DISPLAY_NAME = "EMAIL_FROM_DISPLAY_NAME"; + public const string ACCOUNTS_URL = "ACCOUNTS_URL"; + public const string GITHUB_CLIENT_ID = "GH_CLIENT_ID"; + public const string GITHUB_CLIENT_SECRET = "GH_CLIENT_SECRET"; + public const string APP_AES_KEY = "APP_AES_KEY"; +} diff --git a/server/src/Data/Static/AppHeaders.cs b/server/src/Data/Static/AppHeaders.cs new file mode 100644 index 0000000..41a3085 --- /dev/null +++ b/server/src/Data/Static/AppHeaders.cs @@ -0,0 +1,6 @@ +namespace IOL.GreatOffice.Api.Data.Static; + +public static class AppHeaders +{ + public const string BROWSER_TIME_ZONE = "X-TimeZone"; +} diff --git a/server/src/Data/Static/AppPaths.cs b/server/src/Data/Static/AppPaths.cs new file mode 100644 index 0000000..a24f5af --- /dev/null +++ b/server/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/server/src/Data/Static/JsonSettings.cs b/server/src/Data/Static/JsonSettings.cs new file mode 100644 index 0000000..a163c11 --- /dev/null +++ b/server/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; + }; +} diff --git a/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs b/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs new file mode 100644 index 0000000..dc73e68 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// <summary> +/// Payload for creating new user accounts. +/// </summary> +public class CreateAccountPayload +{ + /// <summary> + /// Username for the new account. + /// </summary> + public string Username { get; set; } + + /// <summary> + /// Password for the new account. + /// </summary> + public string Password { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs new file mode 100644 index 0000000..954fbf5 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -0,0 +1,44 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// <inheritdoc /> +public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountPayload>.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// <inheritdoc /> + public CreateAccountRoute(UserService userService, AppDbContext context) { + _userService = userService; + _context = context; + } + + /// <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 ErrorResult("Invalid form", request.Username + " does not look like a valid email")); + } + + if (request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid form", "The password requires 6 or more characters.")); + } + + var username = request.Username.Trim(); + if (_context.Users.Any(c => c.Username == username)) { + return BadRequest(new ErrorResult("Username is not available", "There is already a user registered with email: " + username)); + } + + var user = new User(username); + user.HashAndSetPassword(request.Password); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs b/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs new file mode 100644 index 0000000..0cd1aa5 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs @@ -0,0 +1,17 @@ +using AspNet.Security.OAuth.GitHub; + +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class CreateGithubSessionRoute : RouteBaseSync.WithRequest<string>.WithActionResult +{ + public CreateGithubSessionRoute(IConfiguration configuration) { } + + [AllowAnonymous] + [HttpGet("~/_/account/create-github-session")] + public override ActionResult Handle(string next) { + return Challenge(new AuthenticationProperties { + RedirectUri = next + }, + GitHubAuthenticationDefaults.AuthenticationScheme); + } +} diff --git a/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs new file mode 100644 index 0000000..13fbdf4 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -0,0 +1,34 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// <inheritdoc /> +public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// <inheritdoc /> + public CreateInitialAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _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(); + } + + var user = new User("admin@ivarlovlie.no"); + user.HashAndSetPassword("ivar123"); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Redirect("/"); + } +} diff --git a/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs b/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs new file mode 100644 index 0000000..2149e15 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs @@ -0,0 +1,49 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// <inheritdoc /> + public DeleteAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _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(); + } + + if (user.Username == "demo@demo.demo") { + await _userService.LogOutUser(HttpContext); + return Ok(); + } + + var githubMappings = _context.TimeCategories.Where(c => c.UserId == user.Id); + var passwordResets = _context.ForgotPasswordRequests.Where(c => c.UserId == user.Id); + var entries = _context.TimeEntries.Where(c => c.UserId == user.Id); + var labels = _context.TimeLabels.Where(c => c.UserId == user.Id); + var categories = _context.TimeCategories.Where(c => c.UserId == user.Id); + + _context.TimeCategories.RemoveRange(githubMappings); + _context.ForgotPasswordRequests.RemoveRange(passwordResets); + _context.TimeEntries.RemoveRange(entries); + _context.TimeLabels.RemoveRange(labels); + _context.TimeCategories.RemoveRange(categories); + _context.Users.Remove(user); + + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogOutUser(HttpContext); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs b/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs new file mode 100644 index 0000000..44f5249 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs @@ -0,0 +1,62 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountArchiveRoute : RouteBaseAsync.WithoutRequest.WithActionResult<UserArchiveDto> +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public GetAccountArchiveRoute(AppDbContext context) { + _context = context; + } + + /// <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(); + } + + var entries = _context.TimeEntries + .AsNoTracking() + .Include(c => c.Labels) + .Include(c => c.Category) + .Where(c => c.User.Id == user.Id) + .ToList(); + + var jsonOptions = new JsonSerializerOptions { + WriteIndented = true + }; + + var dto = new UserArchiveDto(user); + dto.Entries.AddRange(entries.Select(entry => new UserArchiveDto.EntryDto { + CreatedAt = entry.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"), + StartDateTime = entry.Start, + StopDateTime = entry.Stop, + Description = entry.Description, + Labels = entry.Labels + .Select(c => new UserArchiveDto.LabelDto { + Name = c.Name, + Color = c.Color + }) + .ToList(), + Category = new UserArchiveDto.CategoryDto { + Name = entry.Category.Name, + Color = entry.Category.Color + }, + })); + + dto.CountEntries(); + + var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions); + + return File(entriesSerialized, + "application/json", + user.Username + "-time-tracker-archive-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json"); + } +} diff --git a/server/src/Endpoints/Internal/Account/GetRoute.cs b/server/src/Endpoints/Internal/Account/GetRoute.cs new file mode 100644 index 0000000..34a3c97 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/GetRoute.cs @@ -0,0 +1,30 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<LoggedInUserModel> +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public GetAccountRoute(AppDbContext context) { + _context = context; + } + + /// <summary> + /// Get the logged on user's session data. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [HttpGet("~/_/account")] + public override async Task<ActionResult<LoggedInUserModel>> HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user != default) { + return Ok(new LoggedInUserModel { + Id = LoggedInUser.Id, + Username = LoggedInUser.Username + }); + } + + await HttpContext.SignOutAsync(); + return Unauthorized(); + } +} diff --git a/server/src/Endpoints/Internal/Account/LoginPayload.cs b/server/src/Endpoints/Internal/Account/LoginPayload.cs new file mode 100644 index 0000000..807662c --- /dev/null +++ b/server/src/Endpoints/Internal/Account/LoginPayload.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// <summary> +/// Payload for logging in a user. +/// </summary> +public class LoginPayload +{ + /// <summary> + /// Username of the user's account. + /// </summary> + public string Username { get; set; } + + /// <summary> + /// Password of the user's account. + /// </summary> + public string Password { get; set; } + + /// <summary> + /// Specify that the created session should be long lived and continually refreshed. + /// </summary> + public bool Persist { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/LoginRoute.cs b/server/src/Endpoints/Internal/Account/LoginRoute.cs new file mode 100644 index 0000000..5b41c61 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/LoginRoute.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class LoginRoute : RouteBaseAsync + .WithRequest<LoginPayload> + .WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// <inheritdoc /> + public LoginRoute(AppDbContext context, UserService userService) { + _context = context; + _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); + } + + var user = _context.Users.SingleOrDefault(u => u.Username == request.Username); + if (user == default || !user.VerifyPassword(request.Password)) { + return BadRequest(new ErrorResult("Invalid username or password")); + } + + await _userService.LogInUser(HttpContext, user, request.Persist); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/LogoutRoute.cs b/server/src/Endpoints/Internal/Account/LogoutRoute.cs new file mode 100644 index 0000000..4a06f4a --- /dev/null +++ b/server/src/Endpoints/Internal/Account/LogoutRoute.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class LogoutRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly UserService _userService; + + public LogoutRoute(UserService userService) { + _userService = userService; + } + + /// <summary> + /// Logout a user. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/account/logout")] + public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { + await _userService.LogOutUser(HttpContext); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs b/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs new file mode 100644 index 0000000..88a3237 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// <summary> +/// Payload for updating an account. +/// </summary> +public class UpdatePayload +{ + /// <summary> + /// Username to set on the logged on user's account. + /// </summary> + public string Username { get; set; } + + /// <summary> + /// Password to set on the logged on user's account. + /// </summary> + public string Password { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs new file mode 100644 index 0000000..a997dcb --- /dev/null +++ b/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -0,0 +1,51 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdatePayload>.WithActionResult +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public UpdateAccountRoute(AppDbContext context) { + _context = context; + } + + /// <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(); + } + + if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) { + return BadRequest(new ErrorResult("Invalid request", "No data was submitted")); + } + + if (request.Password.HasValue() && request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid request", + "The new password must contain at least 6 characters")); + } + + if (request.Password.HasValue()) { + user.HashAndSetPassword(request.Password); + } + + if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { + return BadRequest(new ErrorResult("Invalid request", + "The new username does not look like a valid email address")); + } + + if (request.Username.HasValue()) { + user.Username = request.Username.Trim(); + } + + await _context.SaveChangesAsync(cancellationToken); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/BaseRoute.cs b/server/src/Endpoints/Internal/BaseRoute.cs new file mode 100644 index 0000000..3e2c6af --- /dev/null +++ b/server/src/Endpoints/Internal/BaseRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +[Authorize] +[ApiController] +[ApiExplorerSettings(IgnoreApi = true)] +[ApiVersionNeutral] +public class BaseRoute : ControllerBase +{ + /// <summary> + /// User data for the currently logged on user. + /// </summary> + protected LoggedInUserModel LoggedInUser => new() { + Username = User.FindFirstValue(AppClaims.NAME), + Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + }; +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs new file mode 100644 index 0000000..3e086f6 --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -0,0 +1,59 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// <inheritdoc /> +public class CreateResetRequestRoute : RouteBaseAsync.WithRequest<string>.WithActionResult +{ + private readonly ILogger<CreateResetRequestRoute> _logger; + private readonly ForgotPasswordService _forgotPasswordService; + private readonly AppDbContext _context; + + /// <inheritdoc /> + public CreateResetRequestRoute(ILogger<CreateResetRequestRoute> logger, ForgotPasswordService forgotPasswordService, AppDbContext context) { + _logger = logger; + _forgotPasswordService = forgotPasswordService; + _context = context; + } + + /// <summary> + /// Create a new password reset request. + /// </summary> + /// <param name="username"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/create")] + public override async Task<ActionResult> HandleAsync(string username, CancellationToken cancellationToken = default) { + if (!username.IsValidEmailAddress()) { + _logger.LogInformation("Username is invalid, not doing request for password change"); + return BadRequest(new ErrorResult("Invalid email address", username + " looks like an invalid email address")); + } + + Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader); + var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC"); + var offset = tz.BaseUtcOffset.Hours; + + // this is fine as long as the client is not connecting from Australia: Lord Howe Island + // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country + if (tz.IsDaylightSavingTime(DateTime.UtcNow)) { + offset++; + } + + _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz); + _logger.LogInformation("Creating forgot password request with date time: " + requestDateTime.ToString("u")); + + try { + var user = _context.Users.SingleOrDefault(c => c.Username.Equals(username)); + if (user != default) { + await _forgotPasswordService.AddRequestAsync(user, tz, cancellationToken); + return Ok(); + } + + _logger.LogInformation("User was not found, not doing request for password change"); + return Ok(); + } catch (Exception e) { + _logger.LogError(e, "ForgotAction failed badly"); + return Ok(); + } + } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs new file mode 100644 index 0000000..f0fb59f --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs @@ -0,0 +1,14 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class FulfillResetRequestPayload +{ + /// <summary> + /// Id of the password reset request to fulfill + /// </summary> + public Guid Id { get; set; } + + /// <summary> + /// New password to set on the relevant account + /// </summary> + public string NewPassword { get; set; } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs new file mode 100644 index 0000000..e33a4fb --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// <inheritdoc /> +public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest<FulfillResetRequestPayload>.WithActionResult +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// <inheritdoc /> + public FulfillResetRequestRoute(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// <summary> + /// Fulfill a password reset request. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpPost("~/_/forgot-password-requests/fulfill")] + public override async Task<ActionResult> HandleAsync(FulfillResetRequestPayload request, CancellationToken cancellationToken = default) { + try { + var fulfilled = await _forgotPasswordService.FullFillRequestAsync(request.Id, request.NewPassword, cancellationToken); + return Ok(fulfilled); + } catch (Exception e) { + if (e is ForgotPasswordRequestNotFoundException or UserNotFoundException) { + return NotFound(); + } + + throw; + } + } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs new file mode 100644 index 0000000..9984094 --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs @@ -0,0 +1,29 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// <inheritdoc /> +public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest<Guid>.WithActionResult +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// <inheritdoc /> + public IsResetRequestValidRoute(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// <summary> + /// Check if a given password reset request is still valid. + /// </summary> + /// <param name="id"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/is-valid")] + public override async Task<ActionResult> HandleAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _forgotPasswordService.GetRequestAsync(id, cancellationToken); + if (request == default) { + return NotFound(); + } + + return Ok(request.IsExpired == false); + } +} diff --git a/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs b/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs new file mode 100644 index 0000000..5fb8213 --- /dev/null +++ b/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs @@ -0,0 +1,21 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class GetApplicationVersionRoute : RouteBaseSync.WithoutRequest.WithActionResult<string> +{ + private readonly IWebHostEnvironment _environment; + + /// <inheritdoc /> + 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)); + } +} diff --git a/server/src/Endpoints/Internal/Root/LogRoute.cs b/server/src/Endpoints/Internal/Root/LogRoute.cs new file mode 100644 index 0000000..48b497a --- /dev/null +++ b/server/src/Endpoints/Internal/Root/LogRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class LogRoute : RouteBaseSync.WithRequest<string>.WithoutResult +{ + private readonly ILogger<LogRoute> _logger; + + public LogRoute(ILogger<LogRoute> logger) { + _logger = logger; + } + + [AllowAnonymous] + [HttpPost("~/_/log")] + public override void Handle([FromBody] string request) { + _logger.LogInformation(request); + } +} diff --git a/server/src/Endpoints/Internal/RouteBaseAsync.cs b/server/src/Endpoints/Internal/RouteBaseAsync.cs new file mode 100644 index 0000000..1bb0af0 --- /dev/null +++ b/server/src/Endpoints/Internal/RouteBaseAsync.cs @@ -0,0 +1,73 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// <summary> +/// A base class for an endpoint that accepts parameters. +/// </summary> +public static class RouteBaseAsync +{ + public static class WithRequest<TRequest> + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract Task<ActionResult<TResponse>> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task<ActionResult> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract Task<ActionResult<TResponse>> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task<ActionResult> HandleAsync( + CancellationToken cancellationToken = default + ); + } + } +} diff --git a/server/src/Endpoints/Internal/RouteBaseSync.cs b/server/src/Endpoints/Internal/RouteBaseSync.cs new file mode 100644 index 0000000..173999d --- /dev/null +++ b/server/src/Endpoints/Internal/RouteBaseSync.cs @@ -0,0 +1,53 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// <summary> +/// A base class for an endpoint that accepts parameters. +/// </summary> +public static class RouteBaseSync +{ + public static class WithRequest<TRequest> + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract ActionResult<TResponse> Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract ActionResult<TResponse> Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + } +} diff --git a/server/src/Endpoints/V1/ApiSpecV1.cs b/server/src/Endpoints/V1/ApiSpecV1.cs new file mode 100644 index 0000000..e4f9cc9 --- /dev/null +++ b/server/src/Endpoints/V1/ApiSpecV1.cs @@ -0,0 +1,18 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1; + +public static class ApiSpecV1 +{ + private const int MAJOR = 1; + private const int MINOR = 0; + public const string VERSION_STRING = "1.0"; + + public static ApiSpecDocument Document => new() { + Version = new ApiVersion(MAJOR, MINOR), + VersionName = VERSION_STRING, + SwaggerPath = $"/swagger/{VERSION_STRING}/swagger.json", + OpenApiInfo = new OpenApiInfo { + Title = AppConstants.API_NAME, + Version = VERSION_STRING + } + }; +} diff --git a/server/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/server/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs new file mode 100644 index 0000000..e8abbf8 --- /dev/null +++ b/server/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs @@ -0,0 +1,52 @@ +using System.Text; + +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAccessTokenDto>.WithActionResult +{ + private readonly AppDbContext _context; + private readonly IConfiguration _configuration; + private readonly ILogger<CreateTokenRoute> _logger; + + public CreateTokenRoute(AppDbContext context, IConfiguration configuration, ILogger<CreateTokenRoute> logger) { + _context = context; + _configuration = configuration; + _logger = logger; + } + + /// <summary> + /// Create a new api token with the provided claims. + /// </summary> + /// <param name="request">The claims to set on the api token</param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpPost("~/v{version:apiVersion}/api-tokens/create")] + [ProducesResponseType(200, Type = typeof(string))] + [ProducesResponseType(404, Type = typeof(ErrorResult))] + public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + return NotFound(new ErrorResult("User does not exist")); + } + + var token_entropy = _configuration.GetValue<string>("TOKEN_ENTROPY"); + if (token_entropy.IsNullOrWhiteSpace()) { + _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); + return NotFound(); + } + + var access_token = new ApiAccessToken() { + Id = Guid.NewGuid(), + User = user, + ExpiryDate = request.ExpiryDate.ToUniversalTime(), + AllowCreate = request.AllowCreate, + AllowRead = request.AllowRead, + AllowDelete = request.AllowDelete, + AllowUpdate = request.AllowUpdate + }; + + _context.AccessTokens.Add(access_token); + _context.SaveChanges(); + return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy)))); + } +} diff --git a/server/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs b/server/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs new file mode 100644 index 0000000..a90b4c0 --- /dev/null +++ b/server/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs @@ -0,0 +1,33 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class DeleteTokenRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult +{ + private readonly AppDbContext _context; + private readonly ILogger<DeleteTokenRoute> _logger; + + public DeleteTokenRoute(AppDbContext context, ILogger<DeleteTokenRoute> logger) { + _context = context; + _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(); + } + + _context.AccessTokens.Remove(token); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs b/server/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs new file mode 100644 index 0000000..59fd077 --- /dev/null +++ b/server/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; + +public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult<ActionResult<List<ApiAccessToken.ApiAccessTokenDto>>> +{ + private readonly AppDbContext _context; + + public GetTokensRoute(AppDbContext context) { + _context = context; + } + + /// <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)); + } +} diff --git a/server/src/Endpoints/V1/BaseRoute.cs b/server/src/Endpoints/V1/BaseRoute.cs new file mode 100644 index 0000000..e7d72ac --- /dev/null +++ b/server/src/Endpoints/V1/BaseRoute.cs @@ -0,0 +1,39 @@ +using System.Net.Http.Headers; + +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// <inheritdoc /> +[ApiVersion(ApiSpecV1.VERSION_STRING)] +[Authorize(AuthenticationSchemes = AuthSchemes)] +[ApiController] +public class BaseRoute : ControllerBase +{ + private const string AuthSchemes = CookieAuthenticationDefaults.AuthenticationScheme + "," + AppConstants.BASIC_AUTH_SCHEME; + + /// <summary> + /// User data for the currently logged on user. + /// </summary> + protected LoggedInUserModel LoggedInUser => new() { + Username = User.FindFirstValue(AppClaims.NAME), + Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + }; + + protected bool IsApiCall() { + if (!Request.Headers.ContainsKey("Authorization")) return false; + try { + var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); + if (authHeader.Parameter == null) return false; + } catch { + return false; + } + + return true; + } + + protected bool HasApiPermission(string permission_key) { + var permission_claim = User.Claims.SingleOrDefault(c => c.Type == permission_key); + return permission_claim is { + Value: "True" + }; + } +} diff --git a/server/src/Endpoints/V1/Categories/CreateCategoryRoute.cs b/server/src/Endpoints/V1/Categories/CreateCategoryRoute.cs new file mode 100644 index 0000000..fac2b5e --- /dev/null +++ b/server/src/Endpoints/V1/Categories/CreateCategoryRoute.cs @@ -0,0 +1,43 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class CreateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult<TimeCategory.TimeCategoryDto> +{ + private readonly AppDbContext _context; + + public CreateCategoryRoute(AppDbContext context) { + _context = context; + } + + /// <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); + } + } + + var newCategory = new TimeCategory(LoggedInUser.Id) { + Name = categoryTimeCategoryDto.Name.Trim(), + Color = categoryTimeCategoryDto.Color + }; + + _context.TimeCategories.Add(newCategory); + _context.SaveChanges(); + categoryTimeCategoryDto.Id = newCategory.Id; + return Ok(categoryTimeCategoryDto); + } +} diff --git a/server/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs b/server/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs new file mode 100644 index 0000000..3d438a0 --- /dev/null +++ b/server/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs @@ -0,0 +1,38 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class DeleteCategoryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult +{ + private readonly AppDbContext _context; + + public DeleteCategoryRoute(AppDbContext context) { + _context = context; + } + + /// <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); + + if (category == default) { + return NotFound(); + } + + var entries = _context.TimeEntries + .Include(c => c.Category) + .Where(c => c.Category.Id == category.Id); + _context.TimeEntries.RemoveRange(entries); + _context.TimeCategories.Remove(category); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/Categories/GetCategoriesRoute.cs b/server/src/Endpoints/V1/Categories/GetCategoriesRoute.cs new file mode 100644 index 0000000..a40a832 --- /dev/null +++ b/server/src/Endpoints/V1/Categories/GetCategoriesRoute.cs @@ -0,0 +1,35 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +/// <inheritdoc /> +public class GetCategoriesRoute : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeCategory.TimeCategoryDto>> +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public GetCategoriesRoute(AppDbContext context) { + _context = context; + } + + /// <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(); + + if (categories.Count == 0) { + return NoContent(); + } + + return Ok(categories); + } +} diff --git a/server/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs b/server/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs new file mode 100644 index 0000000..ca7dfdf --- /dev/null +++ b/server/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs @@ -0,0 +1,39 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; + +public class UpdateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult +{ + private readonly AppDbContext _context; + + public UpdateCategoryRoute(AppDbContext context) { + _context = context; + } + + /// <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(); + } + + if (LoggedInUser.Id != category.UserId) { + return Forbid(); + } + + category.Name = categoryTimeCategoryDto.Name; + category.Color = categoryTimeCategoryDto.Color; + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/Entries/CreateEntryRoute.cs b/server/src/Endpoints/V1/Entries/CreateEntryRoute.cs new file mode 100644 index 0000000..362e430 --- /dev/null +++ b/server/src/Endpoints/V1/Entries/CreateEntryRoute.cs @@ -0,0 +1,65 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class CreateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto> +{ + private readonly AppDbContext _context; + + public CreateEntryRoute(AppDbContext context) { + _context = context; + } + + /// <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(ErrorResult))] + [ProducesResponseType(404, Type = typeof(ErrorResult))] + [HttpPost("~/v{version:apiVersion}/entries/create")] + public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { + if (timeEntryTimeEntryDto.Stop == default) { + return BadRequest(new ErrorResult("Invalid form", "A stop date is required")); + } + + if (timeEntryTimeEntryDto.Start == default) { + return BadRequest(new ErrorResult("Invalid form", "A start date is required")); + } + + if (timeEntryTimeEntryDto.Category == default) { + return BadRequest(new ErrorResult("Invalid form", "A category is required")); + } + + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); + if (category == default) { + return NotFound(new ErrorResult("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } + + var entry = new TimeEntry(LoggedInUser.Id) { + Category = category, + Start = timeEntryTimeEntryDto.Start.ToUniversalTime(), + Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(), + Description = timeEntryTimeEntryDto.Description, + }; + + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id)) + .ToList(); + if (labels.Count != timeEntryTimeEntryDto.Labels.Count) { + return NotFound(new ErrorResult("Not found", "Could not find all of the specified labels")); + } + + entry.Labels = labels; + } + + _context.TimeEntries.Add(entry); + _context.SaveChanges(); + return Ok(entry.AsDto); + } +} diff --git a/server/src/Endpoints/V1/Entries/DeleteEntryRoute.cs b/server/src/Endpoints/V1/Entries/DeleteEntryRoute.cs new file mode 100644 index 0000000..0850af0 --- /dev/null +++ b/server/src/Endpoints/V1/Entries/DeleteEntryRoute.cs @@ -0,0 +1,35 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// <inheritdoc /> +public class DeleteEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public DeleteEntryRoute(AppDbContext context) { + _context = context; + } + + /// <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(); + } + + _context.TimeEntries.Remove(entry); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/Entries/EntryQueryPayload.cs b/server/src/Endpoints/V1/Entries/EntryQueryPayload.cs new file mode 100644 index 0000000..763ac8b --- /dev/null +++ b/server/src/Endpoints/V1/Entries/EntryQueryPayload.cs @@ -0,0 +1,60 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// <summary> +/// Query model for querying time entries. +/// </summary> +public class EntryQueryPayload +{ + /// <summary> + /// Duration to filter with. + /// </summary> + public TimeEntryQueryDuration Duration { get; set; } + + /// <summary> + /// List of categories to filter with. + /// </summary> + public List<TimeCategory.TimeCategoryDto> Categories { get; set; } + + /// <summary> + /// List of labels to filter with. + /// </summary> + public List<TimeLabel.TimeLabelDto> Labels { get; set; } + + /// <summary> + /// Date range to filter with, only respected if Duration is set to TimeEntryQueryDuration.DATE_RANGE. + /// </summary> + /// <see cref="TimeEntryQueryDuration"/> + public QueryDateRange DateRange { get; set; } + + /// <summary> + /// Spesific date to filter with, only respected if Duration is set to TimeEntryQueryDuration.SPECIFIC_DATE. + /// </summary> + /// <see cref="TimeEntryQueryDuration"/> + public DateTime SpecificDate { get; set; } + + /// <summary> + /// Optional page number to show, goes well with PageSize. + /// </summary> + public int Page { get; set; } + + /// <summary> + /// Optional page size to show, goes well with Page. + /// </summary> + public int PageSize { get; set; } + + /// <summary> + /// Represents a date range. + /// </summary> + public class QueryDateRange + { + /// <summary> + /// Range start + /// </summary> + public DateTime From { get; set; } + + /// <summary> + /// Range end + /// </summary> + public DateTime To { get; set; } + } +} diff --git a/server/src/Endpoints/V1/Entries/EntryQueryResponse.cs b/server/src/Endpoints/V1/Entries/EntryQueryResponse.cs new file mode 100644 index 0000000..b1b07a3 --- /dev/null +++ b/server/src/Endpoints/V1/Entries/EntryQueryResponse.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +/// <summary> +/// Response given for a successful query. +/// </summary> +public class EntryQueryResponse +{ + /// <inheritdoc cref="EntryQueryResponse"/> + public EntryQueryResponse() { + Results = new List<TimeEntry.TimeEntryDto>(); + } + + /// <summary> + /// List of entries. + /// </summary> + public List<TimeEntry.TimeEntryDto> Results { get; set; } + + /// <summary> + /// Current page. + /// </summary> + public int Page { get; set; } + + /// <summary> + /// Current page size (amount of entries). + /// </summary> + public int PageSize { get; set; } + + /// <summary> + /// Total amount of entries in query. + /// </summary> + public int TotalSize { get; set; } + + /// <summary> + /// Total amount of page(s) in query. + /// </summary> + public int TotalPageCount { get; set; } +} diff --git a/server/src/Endpoints/V1/Entries/EntryQueryRoute.cs b/server/src/Endpoints/V1/Entries/EntryQueryRoute.cs new file mode 100644 index 0000000..c037b72 --- /dev/null +++ b/server/src/Endpoints/V1/Entries/EntryQueryRoute.cs @@ -0,0 +1,186 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class EntryQueryRoute : RouteBaseSync.WithRequest<EntryQueryPayload>.WithActionResult<EntryQueryResponse> +{ + private readonly ILogger<EntryQueryRoute> _logger; + private readonly AppDbContext _context; + + public EntryQueryRoute(ILogger<EntryQueryRoute> logger, AppDbContext context) { + _logger = logger; + _context = context; + } + + /// <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(ErrorResult))] + [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; + + // 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(DateTime.UtcNow)) { + offsetInHours++; + } + + _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz); + _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u")); + + var skipCount = 0; + if (entryQuery.Page > 1) { + skipCount = entryQuery.PageSize * entryQuery.Page; + } + + result.Page = entryQuery.Page; + result.PageSize = entryQuery.PageSize; + + var baseQuery = _context.TimeEntries + .AsNoTracking() + .Include(c => c.Category) + .Include(c => c.Labels) + .Where(c => c.UserId == LoggedInUser.Id) + .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id)) + .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id))) + .OrderByDescending(c => c.Start); + + switch (entryQuery.Duration) { + case TimeEntryQueryDuration.TODAY: + var baseTodaysEntries = baseQuery + .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, DateTime.UtcNow.Date) == 0); + var baseTodaysEntriesCount = baseTodaysEntries.Count(); + + if (baseTodaysEntriesCount == 0) { + return NoContent(); + } + + result.TotalSize = baseTodaysEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / entryQuery.PageSize)); + + var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_WEEK: + var lastMonday = DateTime.UtcNow.StartOfWeek(DayOfWeek.Monday); + + var baseEntriesThisWeek = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= DateTime.UtcNow.Date); + + var baseEntriesThisWeekCount = baseEntriesThisWeek.Count(); + + if (baseEntriesThisWeekCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisWeekCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / entryQuery.PageSize)); + + var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_MONTH: + var baseEntriesThisMonth = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Month == DateTime.UtcNow.Month + && c.Start.AddHours(offsetInHours).Year == DateTime.UtcNow.Year); + var baseEntriesThisMonthCount = baseEntriesThisMonth.Count(); + if (baseEntriesThisMonthCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisMonthCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisMonthCount / entryQuery.PageSize)); + + var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_YEAR: + var baseEntriesThisYear = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Year == DateTime.UtcNow.Year); + + var baseEntriesThisYearCount = baseEntriesThisYear.Count(); + if (baseEntriesThisYearCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesThisYearCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / entryQuery.PageSize)); + + var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.SPECIFIC_DATE: + var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc); + var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date); + var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count(); + + if (baseEntriesOnThisDateCount == 0) { + return NoContent(); + } + + result.TotalSize = baseEntriesOnThisDateCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / entryQuery.PageSize)); + + var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.DATE_RANGE: + if (entryQuery.DateRange.From == default) { + return BadRequest(new ErrorResult("Invalid query", "From date cannot be empty")); + } + + var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc); + + if (entryQuery.DateRange.To == default) { + return BadRequest(new ErrorResult("Invalid query", "To date cannot be empty")); + } + + var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc); + + if (DateTime.Compare(fromDate, toDate) > 0) { + return BadRequest(new ErrorResult("Invalid query", "To date cannot be less than From date")); + } + + var baseDateRangeEntries = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate); + + var baseDateRangeEntriesCount = baseDateRangeEntries.Count(); + if (baseDateRangeEntriesCount == 0) { + return NoContent(); + } + + result.TotalSize = baseDateRangeEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / entryQuery.PageSize)); + + var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize); + + result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query"); + } + + if (result.Results.Any() && result.Page == 0) { + result.Page = 1; + result.TotalPageCount = 1; + } + + return Ok(result); + } +} diff --git a/server/src/Endpoints/V1/Entries/GetEntryRoute.cs b/server/src/Endpoints/V1/Entries/GetEntryRoute.cs new file mode 100644 index 0000000..87038db --- /dev/null +++ b/server/src/Endpoints/V1/Entries/GetEntryRoute.cs @@ -0,0 +1,34 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class GetEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult<TimeEntry.TimeEntryDto> +{ + private readonly AppDbContext _context; + + public GetEntryRoute(AppDbContext context) { + _context = context; + } + + /// <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); + + if (entry == default) { + return NotFound(); + } + + return Ok(entry); + } +} diff --git a/server/src/Endpoints/V1/Entries/UpdateEntryRoute.cs b/server/src/Endpoints/V1/Entries/UpdateEntryRoute.cs new file mode 100644 index 0000000..ac233e0 --- /dev/null +++ b/server/src/Endpoints/V1/Entries/UpdateEntryRoute.cs @@ -0,0 +1,66 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; + +public class UpdateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto> +{ + private readonly AppDbContext _context; + + public UpdateEntryRoute(AppDbContext context) { + _context = context; + } + + /// <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(ErrorResult))] + [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); + + if (entry == default) { + return NotFound(); + } + + var category = _context.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); + if (category == default) { + return NotFound(new ErrorResult("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } + + entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime(); + entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(); + entry.Description = timeEntryTimeEntryDto.Description; + entry.Category = category; + + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = new List<TimeLabel>(); + + foreach (var labelDto in timeEntryTimeEntryDto.Labels) { + var label = _context.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == labelDto.Id); + + if (label == default) { + continue; + } + + labels.Add(label); + } + + entry.Labels = labels; + } else { + entry.Labels = default; + } + + _context.SaveChanges(); + return Ok(entry.AsDto); + } +} diff --git a/server/src/Endpoints/V1/Labels/CreateLabelRoute.cs b/server/src/Endpoints/V1/Labels/CreateLabelRoute.cs new file mode 100644 index 0000000..31ef7d0 --- /dev/null +++ b/server/src/Endpoints/V1/Labels/CreateLabelRoute.cs @@ -0,0 +1,46 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// <inheritdoc /> +public class CreateLabelRoute : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult<TimeLabel.TimeLabelDto> +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public CreateLabelRoute(AppDbContext context) { + _context = context; + } + + /// <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()); + + if (label != default) { + return Ok(label.AsDto); + } + } + + var newLabel = new TimeLabel(LoggedInUser.Id) { + Name = labelTimeLabelDto.Name.Trim(), + Color = labelTimeLabelDto.Color + }; + + _context.TimeLabels.Add(newLabel); + _context.SaveChanges(); + labelTimeLabelDto.Id = newLabel.Id; + return Ok(labelTimeLabelDto); + } +} diff --git a/server/src/Endpoints/V1/Labels/DeleteLabelRoute.cs b/server/src/Endpoints/V1/Labels/DeleteLabelRoute.cs new file mode 100644 index 0000000..d845a6f --- /dev/null +++ b/server/src/Endpoints/V1/Labels/DeleteLabelRoute.cs @@ -0,0 +1,35 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// <inheritdoc /> +public class DeleteLabelEndpoint : RouteBaseSync.WithRequest<Guid>.WithActionResult +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public DeleteLabelEndpoint(AppDbContext context) { + _context = context; + } + + /// <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); + + if (label == default) { + return NotFound(); + } + + _context.TimeLabels.Remove(label); + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/Labels/GetLabelRoute.cs b/server/src/Endpoints/V1/Labels/GetLabelRoute.cs new file mode 100644 index 0000000..c9ccef3 --- /dev/null +++ b/server/src/Endpoints/V1/Labels/GetLabelRoute.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// <inheritdoc /> +public class GetEndpoint : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeLabel.TimeLabelDto>> +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public GetEndpoint(AppDbContext context) { + _context = context; + } + + /// <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(); + + if (labels.Count == 0) { + return NoContent(); + } + + return Ok(labels); + } +} diff --git a/server/src/Endpoints/V1/Labels/UpdateLabelRoute.cs b/server/src/Endpoints/V1/Labels/UpdateLabelRoute.cs new file mode 100644 index 0000000..0868671 --- /dev/null +++ b/server/src/Endpoints/V1/Labels/UpdateLabelRoute.cs @@ -0,0 +1,38 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; + +/// <inheritdoc /> +public class UpdateLabelEndpoint : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult +{ + private readonly AppDbContext _context; + + /// <inheritdoc /> + public UpdateLabelEndpoint(AppDbContext context) { + _context = context; + } + + /// <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(); + } + + if (LoggedInUser.Id != label.User.Id) { + return Forbid(); + } + + label.Name = labelTimeLabelDto.Name; + label.Color = labelTimeLabelDto.Color; + _context.SaveChanges(); + return Ok(); + } +} diff --git a/server/src/Endpoints/V1/RouteBaseAsync.cs b/server/src/Endpoints/V1/RouteBaseAsync.cs new file mode 100644 index 0000000..1d179f7 --- /dev/null +++ b/server/src/Endpoints/V1/RouteBaseAsync.cs @@ -0,0 +1,73 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// <summary> +/// A base class for an endpoint that accepts parameters. +/// </summary> +public static class RouteBaseAsync +{ + public static class WithRequest<TRequest> + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract Task<ActionResult<TResponse>> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task<ActionResult> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract Task<ActionResult<TResponse>> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task<ActionResult> HandleAsync( + CancellationToken cancellationToken = default + ); + } + } +} diff --git a/server/src/Endpoints/V1/RouteBaseSync.cs b/server/src/Endpoints/V1/RouteBaseSync.cs new file mode 100644 index 0000000..cb27c14 --- /dev/null +++ b/server/src/Endpoints/V1/RouteBaseSync.cs @@ -0,0 +1,53 @@ +namespace IOL.GreatOffice.Api.Endpoints.V1; + +/// <summary> +/// A base class for an endpoint that accepts parameters. +/// </summary> +public static class RouteBaseSync +{ + public static class WithRequest<TRequest> + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract ActionResult<TResponse> Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult<TResponse> : BaseRoute + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(); + } + + public abstract class WithActionResult<TResponse> : BaseRoute + { + public abstract ActionResult<TResponse> Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + } +} diff --git a/server/src/IOL.GreatOffice.Api.csproj b/server/src/IOL.GreatOffice.Api.csproj new file mode 100644 index 0000000..0bd2c48 --- /dev/null +++ b/server/src/IOL.GreatOffice.Api.csproj @@ -0,0 +1,48 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <RuntimeIdentifier>linux-x64</RuntimeIdentifier> + <UserSecretsId>ed5ff3e5-46e2-4d7e-8272-7081f5abfee4</UserSecretsId> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <ImplicitUsings>true</ImplicitUsings> + <Nullable>disable</Nullable> + <NoWarn>CS1591</NoWarn> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.6" /> + <PackageReference Include="Duende.IdentityServer" Version="6.1.0" /> + <PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="6.1.0" /> + <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> + <PackageReference Include="IOL.Helpers" Version="3.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" /> + <PackageReference Include="Quartz.Extensions.Hosting" Version="3.4.0" /> + <PackageReference Include="Serilog.AspNetCore" Version="5.0.0" /> + <PackageReference Include="Serilog.Expressions" Version="3.4.0" /> + <PackageReference Include="Serilog.Sinks.Seq" Version="5.1.1" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" /> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.3.1" /> + <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.3.1" /> + </ItemGroup> + + <ItemGroup Condition="'$(Configuration)' == 'Release'"> + <Content Remove="AppData" /> + </ItemGroup> + + <ItemGroup> + <Content Include="..\..\README.md"> + <Link>README.md</Link> + </Content> + </ItemGroup> + + <ItemGroup> + <Folder Include="wwwroot" /> + </ItemGroup> +</Project> diff --git a/server/src/Jobs/JobRegister.cs b/server/src/Jobs/JobRegister.cs new file mode 100644 index 0000000..72c2cc7 --- /dev/null +++ b/server/src/Jobs/JobRegister.cs @@ -0,0 +1,18 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public static class JobRegister +{ + public static readonly JobKey TokenCleanupKey = new("TokenCleanupJob"); + + public static IServiceCollectionQuartzConfigurator RegisterJobs(this IServiceCollectionQuartzConfigurator configurator) { + configurator.AddJob<TokenCleanupJob>(TokenCleanupKey); + configurator.AddTrigger(options => { + options.ForJob(TokenCleanupKey) + .WithIdentity(TokenCleanupKey.Name + "-trigger") + .WithCronSchedule(CronScheduleBuilder.DailyAtHourAndMinute(1, 0)); + }); + return configurator; + } +} diff --git a/server/src/Jobs/TokenCleanupJob.cs b/server/src/Jobs/TokenCleanupJob.cs new file mode 100644 index 0000000..22b60d1 --- /dev/null +++ b/server/src/Jobs/TokenCleanupJob.cs @@ -0,0 +1,21 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public class TokenCleanupJob : IJob +{ + private readonly ILogger<TokenCleanupJob> _logger; + private readonly AppDbContext _context; + + public TokenCleanupJob(ILogger<TokenCleanupJob> logger, AppDbContext context) { + _logger = logger; + _context = context; + } + + public Task Execute(IJobExecutionContext context) { + var staleTokens = _context.AccessTokens.Where(c => c.HasExpired); + _logger.LogInformation("Removing {0} stale tokens", staleTokens.Count()); + _context.AccessTokens.RemoveRange(); + return Task.CompletedTask; + } +} diff --git a/server/src/Migrations/20210517202115_InitialMigration.Designer.cs b/server/src/Migrations/20210517202115_InitialMigration.Designer.cs new file mode 100644 index 0000000..b6a01ff --- /dev/null +++ b/server/src/Migrations/20210517202115_InitialMigration.Designer.cs @@ -0,0 +1,238 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20210517202115_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.6") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users"); + + b.HasData( + new + { + Id = new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"), + Created = new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868), + Password = "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==", + Username = "admin@ivarlovlie.no" + }); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20210517202115_InitialMigration.cs b/server/src/Migrations/20210517202115_InitialMigration.cs new file mode 100644 index 0000000..8bfaf61 --- /dev/null +++ b/server/src/Migrations/20210517202115_InitialMigration.cs @@ -0,0 +1,162 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "time_categories", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: true), + color = table.Column<string>(type: "text", nullable: true), + user_id = table.Column<Guid>(type: "uuid", nullable: false), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_time_categories", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "time_labels", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: true), + color = table.Column<string>(type: "text", nullable: true), + user_id = table.Column<Guid>(type: "uuid", nullable: false), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_time_labels", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + username = table.Column<string>(type: "text", nullable: true), + password = table.Column<string>(type: "text", nullable: true), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "time_entries", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + start = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + stop = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + note = table.Column<string>(type: "text", nullable: true), + user_id = table.Column<Guid>(type: "uuid", nullable: false), + category_id = table.Column<Guid>(type: "uuid", nullable: true), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_time_entries", x => x.id); + table.ForeignKey( + name: "fk_time_entries_time_categories_category_id", + column: x => x.category_id, + principalTable: "time_categories", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "forgot_password_requests", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + user_id = table.Column<Guid>(type: "uuid", nullable: true), + created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_forgot_password_requests", x => x.id); + table.ForeignKey( + name: "fk_forgot_password_requests_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "time_entry_time_label", + columns: table => new + { + entries_id = table.Column<Guid>(type: "uuid", nullable: false), + labels_id = table.Column<Guid>(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_time_entry_time_label", x => new { x.entries_id, x.labels_id }); + table.ForeignKey( + name: "fk_time_entry_time_label_time_entries_entries_id", + column: x => x.entries_id, + principalTable: "time_entries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_time_entry_time_label_time_labels_labels_id", + column: x => x.labels_id, + principalTable: "time_labels", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "users", + columns: new[] { "id", "created", "password", "username" }, + values: new object[] { new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"), new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868), "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==", "admin@ivarlovlie.no" }); + + migrationBuilder.CreateIndex( + name: "ix_forgot_password_requests_user_id", + table: "forgot_password_requests", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_category_id", + table: "time_entries", + column: "category_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entry_time_label_labels_id", + table: "time_entry_time_label", + column: "labels_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "forgot_password_requests"); + + migrationBuilder.DropTable( + name: "time_entry_time_label"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "time_entries"); + + migrationBuilder.DropTable( + name: "time_labels"); + + migrationBuilder.DropTable( + name: "time_categories"); + } + } +} diff --git a/server/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs b/server/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs new file mode 100644 index 0000000..368e6b3 --- /dev/null +++ b/server/src/Migrations/20210522165932_RenameNoteToDescription.Designer.cs @@ -0,0 +1,229 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20210522165932_RenameNoteToDescription")] + partial class RenameNoteToDescription + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.6") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp without time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp without time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp without time zone") + .HasColumnName("created"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20210522165932_RenameNoteToDescription.cs b/server/src/Migrations/20210522165932_RenameNoteToDescription.cs new file mode 100644 index 0000000..e5bae54 --- /dev/null +++ b/server/src/Migrations/20210522165932_RenameNoteToDescription.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class RenameNoteToDescription : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "users", + keyColumn: "id", + keyValue: new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db")); + + migrationBuilder.RenameColumn( + name: "note", + table: "time_entries", + newName: "description"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "description", + table: "time_entries", + newName: "note"); + + migrationBuilder.InsertData( + table: "users", + columns: new[] { "id", "created", "password", "username" }, + values: new object[] { new Guid("784938f0-cc0e-46ec-afa6-fc60b47b28db"), new DateTime(2021, 5, 17, 20, 21, 14, 827, DateTimeKind.Utc).AddTicks(4868), "AAAAAAEAACcQAAAAEJdtrX3pEeIbcgY+BDAr56gvfbc420ag1TllA0cK6Q6Gw3+gGDIQtYIZnisW3dmqaQ==", "admin@ivarlovlie.no" }); + } + } +} diff --git a/server/src/Migrations/20211002113037_V6Migration.Designer.cs b/server/src/Migrations/20211002113037_V6Migration.Designer.cs new file mode 100644 index 0000000..59e6112 --- /dev/null +++ b/server/src/Migrations/20211002113037_V6Migration.Designer.cs @@ -0,0 +1,233 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20211002113037_V6Migration")] + partial class V6Migration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.0-rc.1.21452.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20211002113037_V6Migration.cs b/server/src/Migrations/20211002113037_V6Migration.cs new file mode 100644 index 0000000..c7ac971 --- /dev/null +++ b/server/src/Migrations/20211002113037_V6Migration.cs @@ -0,0 +1,130 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class V6Migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("SET TimeZone='UTC'"); + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "users", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_labels", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "stop", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "start", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_entries", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_categories", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "forgot_password_requests", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("SET TimeZone='UTC'"); + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "users", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_labels", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "stop", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "start", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_entries", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "time_categories", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn<DateTime>( + name: "created", + table: "forgot_password_requests", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + } +} diff --git a/server/src/Migrations/20220225143559_GithubUserMappings.Designer.cs b/server/src/Migrations/20220225143559_GithubUserMappings.Designer.cs new file mode 100644 index 0000000..2b95f9d --- /dev/null +++ b/server/src/Migrations/20220225143559_GithubUserMappings.Designer.cs @@ -0,0 +1,270 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220225143559_GithubUserMappings")] + partial class GithubUserMappings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220225143559_GithubUserMappings.cs b/server/src/Migrations/20220225143559_GithubUserMappings.cs new file mode 100644 index 0000000..fc30c7a --- /dev/null +++ b/server/src/Migrations/20220225143559_GithubUserMappings.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class GithubUserMappings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "github_user_mappings", + columns: table => new + { + github_id = table.Column<string>(type: "text", nullable: false), + user_id = table.Column<Guid>(type: "uuid", nullable: true), + email = table.Column<string>(type: "text", nullable: true), + refresh_token = table.Column<string>(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_github_user_mappings", x => x.github_id); + table.ForeignKey( + name: "fk_github_user_mappings_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_github_user_mappings_user_id", + table: "github_user_mappings", + column: "user_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "github_user_mappings"); + } + } +} diff --git a/server/src/Migrations/20220319135910_RenameCreated.Designer.cs b/server/src/Migrations/20220319135910_RenameCreated.Designer.cs new file mode 100644 index 0000000..3d57f1a --- /dev/null +++ b/server/src/Migrations/20220319135910_RenameCreated.Designer.cs @@ -0,0 +1,270 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220319135910_RenameCreated")] + partial class RenameCreated + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220319135910_RenameCreated.cs b/server/src/Migrations/20220319135910_RenameCreated.cs new file mode 100644 index 0000000..6571e50 --- /dev/null +++ b/server/src/Migrations/20220319135910_RenameCreated.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class RenameCreated : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "created", + table: "users", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "created", + table: "time_labels", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "created", + table: "time_entries", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "created", + table: "time_categories", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "created", + table: "forgot_password_requests", + newName: "created_at"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "created_at", + table: "users", + newName: "created"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "time_labels", + newName: "created"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "time_entries", + newName: "created"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "time_categories", + newName: "created"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "forgot_password_requests", + newName: "created"); + } + } +} diff --git a/server/src/Migrations/20220319144958_ModifiedAt.Designer.cs b/server/src/Migrations/20220319144958_ModifiedAt.Designer.cs new file mode 100644 index 0000000..f75400e --- /dev/null +++ b/server/src/Migrations/20220319144958_ModifiedAt.Designer.cs @@ -0,0 +1,290 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220319144958_ModifiedAt")] + partial class ModifiedAt + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220319144958_ModifiedAt.cs b/server/src/Migrations/20220319144958_ModifiedAt.cs new file mode 100644 index 0000000..028473d --- /dev/null +++ b/server/src/Migrations/20220319144958_ModifiedAt.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class ModifiedAt : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "users", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "time_labels", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "time_entries", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "time_categories", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "forgot_password_requests", + type: "timestamp with time zone", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "modified_at", + table: "users"); + + migrationBuilder.DropColumn( + name: "modified_at", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "modified_at", + table: "time_entries"); + + migrationBuilder.DropColumn( + name: "modified_at", + table: "time_categories"); + + migrationBuilder.DropColumn( + name: "modified_at", + table: "forgot_password_requests"); + } + } +} diff --git a/server/src/Migrations/20220319203018_UserBase.Designer.cs b/server/src/Migrations/20220319203018_UserBase.Designer.cs new file mode 100644 index 0000000..6c7a76f --- /dev/null +++ b/server/src/Migrations/20220319203018_UserBase.Designer.cs @@ -0,0 +1,322 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220319203018_UserBase")] + partial class UserBase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.Property<string>("password") + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220319203018_UserBase.cs b/server/src/Migrations/20220319203018_UserBase.cs new file mode 100644 index 0000000..14d3f4b --- /dev/null +++ b/server/src/Migrations/20220319203018_UserBase.cs @@ -0,0 +1,140 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class UserBase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "modified_at", + table: "forgot_password_requests"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_user_id", + table: "time_labels", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_user_id", + table: "time_entries", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_categories_user_id", + table: "time_categories", + column: "user_id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_user_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_entries_user_id", + table: "time_entries"); + + migrationBuilder.DropIndex( + name: "ix_time_categories_user_id", + table: "time_categories"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddColumn<DateTime>( + name: "modified_at", + table: "forgot_password_requests", + type: "timestamp with time zone", + nullable: true); + } + } +} diff --git a/server/src/Migrations/20220320115601_Update1.Designer.cs b/server/src/Migrations/20220320115601_Update1.Designer.cs new file mode 100644 index 0000000..c7463fb --- /dev/null +++ b/server/src/Migrations/20220320115601_Update1.Designer.cs @@ -0,0 +1,342 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220320115601_Update1")] + partial class Update1 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.Property<string>("password") + .HasColumnType("text") + .HasColumnName("password"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Categories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany("Entries") + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Entries") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Labels") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Navigation("Categories"); + + b.Navigation("Entries"); + + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220320115601_Update1.cs b/server/src/Migrations/20220320115601_Update1.cs new file mode 100644 index 0000000..8b06fb7 --- /dev/null +++ b/server/src/Migrations/20220320115601_Update1.cs @@ -0,0 +1,139 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class Update1 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories", + column: "user_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries", + column: "user_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels", + column: "user_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_labels", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_entries", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "time_categories", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_user_id", + table: "time_categories", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_user_id", + table: "time_entries", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_user_id", + table: "time_labels", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + } + } +} diff --git a/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs b/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs new file mode 100644 index 0000000..3a18463 --- /dev/null +++ b/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.Designer.cs @@ -0,0 +1,344 @@ +// <auto-generated /> + + +#nullable disable + +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220320132220_UpdatedForgotPasswordRequests")] + partial class UpdatedForgotPasswordRequests + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Categories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeCategory", "Category") + .WithMany("Entries") + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Entries") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.User", "User") + .WithMany("Labels") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.TimeCategory", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Data.Database.User", b => + { + b.Navigation("Categories"); + + b.Navigation("Entries"); + + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs b/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs new file mode 100644 index 0000000..df7a195 --- /dev/null +++ b/server/src/Migrations/20220320132220_UpdatedForgotPasswordRequests.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class UpdatedForgotPasswordRequests : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_forgot_password_requests_users_user_id", + table: "forgot_password_requests"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "forgot_password_requests", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "fk_forgot_password_requests_users_user_id", + table: "forgot_password_requests", + column: "user_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_forgot_password_requests_users_user_id", + table: "forgot_password_requests"); + + migrationBuilder.AlterColumn<Guid>( + name: "user_id", + table: "forgot_password_requests", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "fk_forgot_password_requests_users_user_id", + table: "forgot_password_requests", + column: "user_id", + principalTable: "users", + principalColumn: "id"); + } + } +} diff --git a/server/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs b/server/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs new file mode 100644 index 0000000..74f9b40 --- /dev/null +++ b/server/src/Migrations/20220529190359_ApiAccessTokens.Designer.cs @@ -0,0 +1,401 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220529190359_ApiAccessTokens")] + partial class ApiAccessTokens + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_api_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_api_access_tokens_user_id"); + + b.ToTable("api_access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_api_access_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany("Categories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") + .WithMany("Entries") + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany("Entries") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany("Labels") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Navigation("Entries"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Navigation("Categories"); + + b.Navigation("Entries"); + + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220529190359_ApiAccessTokens.cs b/server/src/Migrations/20220529190359_ApiAccessTokens.cs new file mode 100644 index 0000000..dc44bee --- /dev/null +++ b/server/src/Migrations/20220529190359_ApiAccessTokens.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class ApiAccessTokens : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "api_access_tokens", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + user_id = table.Column<Guid>(type: "uuid", nullable: true), + expiry_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + allow_read = table.Column<bool>(type: "boolean", nullable: false), + allow_create = table.Column<bool>(type: "boolean", nullable: false), + allow_update = table.Column<bool>(type: "boolean", nullable: false), + allow_delete = table.Column<bool>(type: "boolean", nullable: false), + created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_api_access_tokens", x => x.id); + table.ForeignKey( + name: "fk_api_access_tokens_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_api_access_tokens_user_id", + table: "api_access_tokens", + column: "user_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api_access_tokens"); + } + } +} diff --git a/server/src/Migrations/20220530174741_Tenants.Designer.cs b/server/src/Migrations/20220530174741_Tenants.Designer.cs new file mode 100644 index 0000000..678c52d --- /dev/null +++ b/server/src/Migrations/20220530174741_Tenants.Designer.cs @@ -0,0 +1,710 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220530174741_Tenants")] + partial class Tenants + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_api_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_api_access_tokens_user_id"); + + b.ToTable("api_access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<Guid>("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property<string>("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tenants_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tenants_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_tenants_modified_by_id"); + + b.HasIndex("TenantId1") + .HasDatabaseName("ix_tenants_tenant_id1"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenants_user_id"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_categories_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_categories_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_categories_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_categories_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_entries_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_entries_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_entries_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_entries_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_labels_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_labels_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_labels_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_labels_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.Property<Guid>("EntriesId") + .HasColumnType("uuid") + .HasColumnName("entries_id"); + + b.Property<Guid>("LabelsId") + .HasColumnType("uuid") + .HasColumnName("labels_id"); + + b.HasKey("EntriesId", "LabelsId") + .HasName("pk_time_entry_time_label"); + + b.HasIndex("LabelsId") + .HasDatabaseName("ix_time_entry_time_label_labels_id"); + + b.ToTable("time_entry_time_label", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_api_access_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId1") + .HasConstraintName("fk_tenants_tenants_tenant_id1"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") + .WithMany("Entries") + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TimeEntryTimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null) + .WithMany() + .HasForeignKey("EntriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_entries_entries_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeLabel", null) + .WithMany() + .HasForeignKey("LabelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entry_time_label_time_labels_labels_id"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Navigation("Entries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220530174741_Tenants.cs b/server/src/Migrations/20220530174741_Tenants.cs new file mode 100644 index 0000000..ea02ddd --- /dev/null +++ b/server/src/Migrations/20220530174741_Tenants.cs @@ -0,0 +1,481 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class Tenants : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<Guid>( + name: "created_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "deleted_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "modified_by_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "tenant_id", + table: "time_labels", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "created_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "deleted_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "modified_by_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "tenant_id", + table: "time_entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "created_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "deleted_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "modified_by_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn<Guid>( + name: "tenant_id", + table: "time_categories", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateTable( + name: "tenants", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: true), + description = table.Column<string>(type: "text", nullable: true), + contact_email = table.Column<string>(type: "text", nullable: true), + master_user_id = table.Column<Guid>(type: "uuid", nullable: false), + master_user_password = table.Column<string>(type: "text", nullable: true), + created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + modified_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), + user_id = table.Column<Guid>(type: "uuid", nullable: false), + tenant_id = table.Column<Guid>(type: "uuid", nullable: false), + tenant_id1 = table.Column<Guid>(type: "uuid", nullable: true), + modified_by_id = table.Column<Guid>(type: "uuid", nullable: false), + created_by_id = table.Column<Guid>(type: "uuid", nullable: false), + deleted_by_id = table.Column<Guid>(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tenants", x => x.id); + table.ForeignKey( + name: "fk_tenants_tenants_tenant_id1", + column: x => x.tenant_id1, + principalTable: "tenants", + principalColumn: "id"); + table.ForeignKey( + name: "fk_tenants_users_created_by_id", + column: x => x.created_by_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tenants_users_deleted_by_id", + column: x => x.deleted_by_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tenants_users_modified_by_id", + column: x => x.modified_by_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_tenants_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_created_by_id", + table: "time_labels", + column: "created_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_deleted_by_id", + table: "time_labels", + column: "deleted_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_modified_by_id", + table: "time_labels", + column: "modified_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_tenant_id", + table: "time_labels", + column: "tenant_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_created_by_id", + table: "time_entries", + column: "created_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_deleted_by_id", + table: "time_entries", + column: "deleted_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_modified_by_id", + table: "time_entries", + column: "modified_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_entries_tenant_id", + table: "time_entries", + column: "tenant_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_categories_created_by_id", + table: "time_categories", + column: "created_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_categories_deleted_by_id", + table: "time_categories", + column: "deleted_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_categories_modified_by_id", + table: "time_categories", + column: "modified_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_time_categories_tenant_id", + table: "time_categories", + column: "tenant_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_created_by_id", + table: "tenants", + column: "created_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_deleted_by_id", + table: "tenants", + column: "deleted_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_modified_by_id", + table: "tenants", + column: "modified_by_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_tenant_id1", + table: "tenants", + column: "tenant_id1"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_user_id", + table: "tenants", + column: "user_id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_tenants_tenant_id", + table: "time_categories", + column: "tenant_id", + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_created_by_id", + table: "time_categories", + column: "created_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_deleted_by_id", + table: "time_categories", + column: "deleted_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_categories_users_modified_by_id", + table: "time_categories", + column: "modified_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_tenants_tenant_id", + table: "time_entries", + column: "tenant_id", + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_created_by_id", + table: "time_entries", + column: "created_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_deleted_by_id", + table: "time_entries", + column: "deleted_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_entries_users_modified_by_id", + table: "time_entries", + column: "modified_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_tenants_tenant_id", + table: "time_labels", + column: "tenant_id", + principalTable: "tenants", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_created_by_id", + table: "time_labels", + column: "created_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_deleted_by_id", + table: "time_labels", + column: "deleted_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_users_modified_by_id", + table: "time_labels", + column: "modified_by_id", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_time_categories_tenants_tenant_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_created_by_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_deleted_by_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_categories_users_modified_by_id", + table: "time_categories"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_tenants_tenant_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_created_by_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_deleted_by_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_entries_users_modified_by_id", + table: "time_entries"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_tenants_tenant_id", + table: "time_labels"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_created_by_id", + table: "time_labels"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_deleted_by_id", + table: "time_labels"); + + migrationBuilder.DropForeignKey( + name: "fk_time_labels_users_modified_by_id", + table: "time_labels"); + + migrationBuilder.DropTable( + name: "tenants"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_created_by_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_deleted_by_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_modified_by_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_tenant_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_entries_created_by_id", + table: "time_entries"); + + migrationBuilder.DropIndex( + name: "ix_time_entries_deleted_by_id", + table: "time_entries"); + + migrationBuilder.DropIndex( + name: "ix_time_entries_modified_by_id", + table: "time_entries"); + + migrationBuilder.DropIndex( + name: "ix_time_entries_tenant_id", + table: "time_entries"); + + migrationBuilder.DropIndex( + name: "ix_time_categories_created_by_id", + table: "time_categories"); + + migrationBuilder.DropIndex( + name: "ix_time_categories_deleted_by_id", + table: "time_categories"); + + migrationBuilder.DropIndex( + name: "ix_time_categories_modified_by_id", + table: "time_categories"); + + migrationBuilder.DropIndex( + name: "ix_time_categories_tenant_id", + table: "time_categories"); + + migrationBuilder.DropColumn( + name: "created_by_id", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "deleted_by_id", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "modified_by_id", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "tenant_id", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "created_by_id", + table: "time_entries"); + + migrationBuilder.DropColumn( + name: "deleted_by_id", + table: "time_entries"); + + migrationBuilder.DropColumn( + name: "modified_by_id", + table: "time_entries"); + + migrationBuilder.DropColumn( + name: "tenant_id", + table: "time_entries"); + + migrationBuilder.DropColumn( + name: "created_by_id", + table: "time_categories"); + + migrationBuilder.DropColumn( + name: "deleted_by_id", + table: "time_categories"); + + migrationBuilder.DropColumn( + name: "modified_by_id", + table: "time_categories"); + + migrationBuilder.DropColumn( + name: "tenant_id", + table: "time_categories"); + } + } +} diff --git a/server/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs b/server/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs new file mode 100644 index 0000000..8fd6b40 --- /dev/null +++ b/server/src/Migrations/20220530175322_RemoveUnusedNavs.Designer.cs @@ -0,0 +1,686 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220530175322_RemoveUnusedNavs")] + partial class RemoveUnusedNavs + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_api_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_api_access_tokens_user_id"); + + b.ToTable("api_access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<Guid>("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property<string>("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tenants_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tenants_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_tenants_modified_by_id"); + + b.HasIndex("TenantId1") + .HasDatabaseName("ix_tenants_tenant_id1"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenants_user_id"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_categories_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_categories_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_categories_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_categories_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_entries_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_entries_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_entries_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_entries_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_labels_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_labels_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_labels_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_labels_tenant_id"); + + b.HasIndex("TimeEntryId") + .HasDatabaseName("ix_time_labels_time_entry_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_api_access_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId1") + .HasConstraintName("fk_tenants_tenants_tenant_id1"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null) + .WithMany("Labels") + .HasForeignKey("TimeEntryId") + .HasConstraintName("fk_time_labels_time_entries_time_entry_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Migrations/20220530175322_RemoveUnusedNavs.cs b/server/src/Migrations/20220530175322_RemoveUnusedNavs.cs new file mode 100644 index 0000000..36b3cf1 --- /dev/null +++ b/server/src/Migrations/20220530175322_RemoveUnusedNavs.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + public partial class RemoveUnusedNavs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "time_entry_time_label"); + + migrationBuilder.AddColumn<Guid>( + name: "time_entry_id", + table: "time_labels", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "ix_time_labels_time_entry_id", + table: "time_labels", + column: "time_entry_id"); + + migrationBuilder.AddForeignKey( + name: "fk_time_labels_time_entries_time_entry_id", + table: "time_labels", + column: "time_entry_id", + principalTable: "time_entries", + principalColumn: "id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_time_labels_time_entries_time_entry_id", + table: "time_labels"); + + migrationBuilder.DropIndex( + name: "ix_time_labels_time_entry_id", + table: "time_labels"); + + migrationBuilder.DropColumn( + name: "time_entry_id", + table: "time_labels"); + + migrationBuilder.CreateTable( + name: "time_entry_time_label", + columns: table => new + { + entries_id = table.Column<Guid>(type: "uuid", nullable: false), + labels_id = table.Column<Guid>(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_time_entry_time_label", x => new { x.entries_id, x.labels_id }); + table.ForeignKey( + name: "fk_time_entry_time_label_time_entries_entries_id", + column: x => x.entries_id, + principalTable: "time_entries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_time_entry_time_label_time_labels_labels_id", + column: x => x.labels_id, + principalTable: "time_labels", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_time_entry_time_label_labels_id", + table: "time_entry_time_label", + column: "labels_id"); + } + } +} diff --git a/server/src/Migrations/AppDbContextModelSnapshot.cs b/server/src/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..e040cfb --- /dev/null +++ b/server/src/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,684 @@ +// <auto-generated /> +using System; +using IOL.GreatOffice.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace IOL.GreatOffice.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("AllowCreate") + .HasColumnType("boolean") + .HasColumnName("allow_create"); + + b.Property<bool>("AllowDelete") + .HasColumnType("boolean") + .HasColumnName("allow_delete"); + + b.Property<bool>("AllowRead") + .HasColumnType("boolean") + .HasColumnName("allow_read"); + + b.Property<bool>("AllowUpdate") + .HasColumnType("boolean") + .HasColumnName("allow_update"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime>("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_api_access_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_api_access_tokens_user_id"); + + b.ToTable("api_access_tokens", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_forgot_password_requests"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_forgot_password_requests_user_id"); + + b.ToTable("forgot_password_requests", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.Property<string>("GithubId") + .HasColumnType("text") + .HasColumnName("github_id"); + + b.Property<string>("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b.Property<string>("RefreshToken") + .HasColumnType("text") + .HasColumnName("refresh_token"); + + b.Property<Guid?>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("GithubId") + .HasName("pk_github_user_mappings"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_github_user_mappings_user_id"); + + b.ToTable("github_user_mappings", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("ContactEmail") + .HasColumnType("text") + .HasColumnName("contact_email"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<Guid>("MasterUserId") + .HasColumnType("uuid") + .HasColumnName("master_user_id"); + + b.Property<string>("MasterUserPassword") + .HasColumnType("text") + .HasColumnName("master_user_password"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TenantId1") + .HasColumnType("uuid") + .HasColumnName("tenant_id1"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tenants_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tenants_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_tenants_modified_by_id"); + + b.HasIndex("TenantId1") + .HasDatabaseName("ix_tenants_tenant_id1"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tenants_user_id"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_categories"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_categories_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_categories_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_categories_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_categories_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_categories_user_id"); + + b.ToTable("time_categories", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<string>("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<DateTime>("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("start"); + + b.Property<DateTime>("Stop") + .HasColumnType("timestamp with time zone") + .HasColumnName("stop"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_entries"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_time_entries_category_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_entries_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_entries_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_entries_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_entries_tenant_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_entries_user_id"); + + b.ToTable("time_entries", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Color") + .HasColumnType("text") + .HasColumnName("color"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid>("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property<Guid>("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<Guid>("ModifiedById") + .HasColumnType("uuid") + .HasColumnName("modified_by_id"); + + b.Property<string>("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property<Guid?>("TimeEntryId") + .HasColumnType("uuid") + .HasColumnName("time_entry_id"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_time_labels"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_time_labels_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_time_labels_deleted_by_id"); + + b.HasIndex("ModifiedById") + .HasDatabaseName("ix_time_labels_modified_by_id"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_time_labels_tenant_id"); + + b.HasIndex("TimeEntryId") + .HasDatabaseName("ix_time_labels_time_entry_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_time_labels_user_id"); + + b.ToTable("time_labels", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<DateTime?>("ModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified_at"); + + b.Property<string>("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<string>("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ApiAccessToken", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_api_access_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.ForgotPasswordRequest", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_forgot_password_requests_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.GithubUserMapping", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_github_user_mappings_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.Tenant", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId1") + .HasConstraintName("fk_tenants_tenants_tenant_id1"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenants_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeCategory", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_categories_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_time_entries_time_categories_category_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_entries_users_user_id"); + + b.Navigation("Category"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeLabel", b => + { + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_created_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_deleted_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "ModifiedBy") + .WithMany() + .HasForeignKey("ModifiedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_modified_by_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_tenants_tenant_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.TimeEntry", null) + .WithMany("Labels") + .HasForeignKey("TimeEntryId") + .HasConstraintName("fk_time_labels_time_entries_time_entry_id"); + + b.HasOne("IOL.GreatOffice.Api.Data.Database.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_time_labels_users_user_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("ModifiedBy"); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IOL.GreatOffice.Api.Data.Database.TimeEntry", b => + { + b.Navigation("Labels"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Program.cs b/server/src/Program.cs new file mode 100644 index 0000000..b449117 --- /dev/null +++ b/server/src/Program.cs @@ -0,0 +1,261 @@ +global using System; +global using System.Linq; +global using System.IO; +global using System.Net.Mail; +global using System.Net; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Collections.Generic; +global using System.Runtime.Serialization; +global using System.ComponentModel.DataAnnotations.Schema; +global using System.Security.Claims; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using IOL.GreatOffice.Api.Data.Database; +global using IOL.GreatOffice.Api.Data.Exceptions; +global using IOL.GreatOffice.Api.Data.Dtos; +global using IOL.GreatOffice.Api.Data.Enums; +global using IOL.GreatOffice.Api.Data.Models; +global using IOL.GreatOffice.Api.Data.Results; +global using IOL.Helpers; +global using Microsoft.OpenApi.Models; +global using Microsoft.AspNetCore.Authentication.Cookies; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.DataProtection; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Authentication; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Serilog; +global using IOL.GreatOffice.Api.Data; +global using IOL.GreatOffice.Api.Data.Static; +global using IOL.GreatOffice.Api.Services; +global using IOL.GreatOffice.Api.Utilities; +using System.Diagnostics; +using System.Reflection; +using IOL.GreatOffice.Api.Endpoints.V1; +using IOL.GreatOffice.Api.Jobs; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc.Versioning; +using Quartz; + +namespace IOL.GreatOffice.Api; + +public static class Program +{ + public static WebApplicationBuilder CreateAppBuilder(string[] args) { + var builder = WebApplication.CreateBuilder(args); + + var seqUrl = builder.Configuration.GetValue<string>(AppEnvironmentVariables.SEQ_API_URL); + var seqKey = builder.Configuration.GetValue<string>(AppEnvironmentVariables.SEQ_API_KEY); + var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .ReadFrom.Configuration(builder.Configuration) + .WriteTo.Console(); + + if (!builder.Environment.IsDevelopment() && seqUrl.HasValue() && seqKey.HasValue()) { + logger.WriteTo.Seq(seqUrl, apiKey: seqKey); + } + + Log.Logger = logger.CreateLogger(); + Log.Information("Starting web host" + + JsonSerializer.Serialize(new { + DateTime.UtcNow, + PID = Environment.ProcessId, + DB_HOST = builder.Configuration.GetValue<string>(AppEnvironmentVariables.DB_HOST), + DB_PORT = builder.Configuration.GetValue<string>(AppEnvironmentVariables.DB_PORT), + DB_USER = builder.Configuration.GetValue<string>(AppEnvironmentVariables.DB_USER), + DB_NAME = builder.Configuration.GetValue<string>(AppEnvironmentVariables.DB_NAME), + DB_PASS = builder.Configuration.GetValue<string>(AppEnvironmentVariables.DB_PASSWORD).Obfuscate() ?? "!!!Empty!!!", + QUARTZ_DB_HOST = builder.Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_HOST), + QUARTZ_DB_PORT = builder.Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_PORT), + QUARTZ_DB_USER = builder.Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_USER), + QUARTZ_DB_NAME = builder.Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_NAME), + QUARTZ_DB_PASS = builder.Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_PASSWORD).Obfuscate() + ?? "!!!Empty!!!", + }, + new JsonSerializerOptions() { + WriteIndented = true + })); + + builder.Host.UseSerilog(Log.Logger); + builder.WebHost.ConfigureKestrel(kestrel => { + kestrel.AddServerHeader = false; + }); + + if (builder.Environment.IsDevelopment()) { + builder.Services.AddCors(); + } + + if (builder.Environment.IsProduction()) { + builder.Services.Configure<ForwardedHeadersOptions>(options => { + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; + }); + } + + builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(AppPaths.DataProtectionKeys.HostPath)); + + builder.Services.Configure(JsonSettings.Default); + builder.Services.AddQuartz(options => { + options.UsePersistentStore(o => { + o.UsePostgres(builder.Configuration.GetQuartzDatabaseConnectionString()); + o.UseSerializer<QuartzJsonSerializer>(); + }); + options.UseMicrosoftDependencyInjectionJobFactory(); + options.RegisterJobs(); + }); + builder.Services.AddQuartzHostedService(options => { + options.WaitForJobsToComplete = true; + }); + + builder.Services.AddAuthentication(options => { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie(options => { + options.Cookie.Name = "go_session"; + options.Cookie.SameSite = SameSiteMode.Strict; + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = builder.Environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always; + options.Cookie.IsEssential = true; + options.SlidingExpiration = true; + options.Events.OnRedirectToAccessDenied = + options.Events.OnRedirectToLogin = c => { + c.Response.StatusCode = StatusCodes.Status401Unauthorized; + return Task.FromResult<object>(null); + }; + }) + .AddGitHub(options => { + options.ClientSecret = builder.Configuration.GetValue<string>(AppEnvironmentVariables.GITHUB_CLIENT_SECRET); + options.ClientId = builder.Configuration.GetValue<string>(AppEnvironmentVariables.GITHUB_CLIENT_ID); + options.SaveTokens = true; + options.CorrelationCookie.Name = "gh_correlation"; + options.CorrelationCookie.SameSite = SameSiteMode.Lax; + options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; + options.CorrelationCookie.HttpOnly = true; + options.Events.OnCreatingTicket = context => GithubAuthenticationHelpers.HandleGithubTicketCreation(context, builder.Configuration); + }) + .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AppConstants.BASIC_AUTH_SCHEME, default); + + + builder.Services.AddDbContext<AppDbContext>(options => { + options.UseNpgsql(builder.Configuration.GetAppDatabaseConnectionString(), + npgsqlDbContextOptionsBuilder => { + npgsqlDbContextOptionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + npgsqlDbContextOptionsBuilder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), default); + }) + .UseSnakeCaseNamingConvention(); + if (builder.Environment.IsDevelopment()) { + options.EnableSensitiveDataLogging(); + } + }); + + builder.Services.AddApiVersioning(options => { + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = false; + }); + builder.Services.AddVersionedApiExplorer(options => { + options.SubstituteApiVersionInUrl = true; + }); + builder.Services.AddSwaggerGen(options => { + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, Assembly.GetExecutingAssembly().GetName().Name + ".xml")); + options.UseApiEndpoints(); + options.OperationFilter<SwaggerDefaultValues>(); + options.SwaggerDoc(ApiSpecV1.Document.VersionName, ApiSpecV1.Document.OpenApiInfo); + options.AddSecurityDefinition("Basic", + new OpenApiSecurityScheme { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Basic", + BearerFormat = "Basic", + In = ParameterLocation.Header, + Description = + "Enter your token in the text input below.\r\n\r\nExample: \"Basic 12345abcdef\"", + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement { + { + new OpenApiSecurityScheme { + Reference = new OpenApiReference { + Type = ReferenceType.SecurityScheme, + Id = "Basic" + } + }, + Array.Empty<string>() + } + }); + }); + + builder.Services.AddScoped<MailService>(); + builder.Services.AddScoped<ForgotPasswordService>(); + builder.Services.AddScoped<UserService>(); + builder.Services.AddLogging(); + builder.Services.AddHttpClient(); + builder.Services + .AddControllers() + .AddJsonOptions(JsonSettings.Default); + + + return builder; + } + + public static WebApplication CreateWebApplication(WebApplicationBuilder builder) { + var app = builder.Build(); + + if (app.Environment.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + app.UseCors(cors => { + cors.AllowAnyMethod(); + cors.AllowAnyHeader(); + cors.WithOrigins("http://localhost:3000", "http://localhost:3002", "http://localhost:3001"); + cors.AllowCredentials(); + }); + } + + if (app.Environment.IsProduction()) { + app.UseForwardedHeaders(); + } + + app.UseDefaultFiles() + .UseStaticFiles() + .UseRouting() + .UseSerilogRequestLogging() + .UseStatusCodePages() + .UseAuthentication() + .UseAuthorization() + .UseSwagger() + .UseSwaggerUI(options => { + options.SwaggerEndpoint(ApiSpecV1.Document.SwaggerPath, ApiSpecV1.Document.VersionName); + options.DocumentTitle = AppConstants.API_NAME; + }) + .UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + return app; + } + + public static int Main(string[] args) { + try { + CreateWebApplication(CreateAppBuilder(args)).Run(); + return 0; + } catch (Exception ex) { + // This is subject to change in future .net versions, see https://github.com/dotnet/runtime/issues/60600. + if (ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) { + throw; + } + + Log.Fatal(ex, "Unhandled exception"); + return 1; + } finally { + Log.Information("Shut down complete, flusing logs..."); + Log.CloseAndFlush(); + } + } +} diff --git a/server/src/Properties/launchSettings.json b/server/src/Properties/launchSettings.json new file mode 100644 index 0000000..6403d71 --- /dev/null +++ b/server/src/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IOL.GreatOffice": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "applicationUrl": "https://0.0.0.0:5001;http://0.0.0.0:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/server/src/Services/ForgotPasswordService.cs b/server/src/Services/ForgotPasswordService.cs new file mode 100644 index 0000000..de38b29 --- /dev/null +++ b/server/src/Services/ForgotPasswordService.cs @@ -0,0 +1,115 @@ +namespace IOL.GreatOffice.Api.Services; + +public class ForgotPasswordService +{ + private readonly AppDbContext _context; + private readonly MailService _mailService; + private readonly IConfiguration _configuration; + private readonly ILogger<ForgotPasswordService> _logger; + + + public ForgotPasswordService( + AppDbContext context, + IConfiguration configuration, + ILogger<ForgotPasswordService> logger, + MailService mailService + ) { + _context = context; + _configuration = configuration; + _logger = logger; + _mailService = mailService; + } + + public async Task<ForgotPasswordRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _context.ForgotPasswordRequests + .Include(c => c.User) + .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); + if (request == default) { + return default; + } + + _logger.LogInformation($"Found forgot password request for user: {request.User.Username}, expires at {request.ExpirationDate} (in {request.ExpirationDate.Subtract(DateTime.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); + if (user == default) { + throw new UserNotFoundException("User with id: " + request.User.Id + " was not found"); + } + + user.HashAndSetPassword(newPassword); + _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + _logger.LogInformation($"Fullfilled forgot password request for user: {request.User.Username}"); + await DeleteRequestsForUserAsync(user.Id, cancellationToken); + return true; + } + + + public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) { + await DeleteRequestsForUserAsync(user.Id, cancellationToken); + var request = new ForgotPasswordRequest(user); + _context.ForgotPasswordRequests.Add(request); + await _context.SaveChangesAsync(cancellationToken); + var accountsUrl = _configuration.GetValue<string>(AppEnvironmentVariables.ACCOUNTS_URL); + var emailFromAddress = _configuration.GetValue<string>(AppEnvironmentVariables.EMAIL_FROM_ADDRESS); + var emailFromDisplayName = _configuration.GetValue<string>(AppEnvironmentVariables.EMAIL_FROM_DISPLAY_NAME); + var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id); + var message = new MailMessage { + From = new MailAddress(emailFromAddress, emailFromDisplayName), + To = { + new MailAddress(user.Username) + }, + Subject = "Time Tracker - Forgot password request", + Body = @$" +Hi {user.Username} + +Go to the following link to set a new password. + +{accountsUrl}/#/reset-password?id={request.Id} + +The link expires at {zonedExpirationDate:yyyy-MM-dd hh:mm}. +If you did not request a password reset, no action is required. +" + }; + +#pragma warning disable 4014 + Task.Run(() => { +#pragma warning restore 4014 + _mailService.SendMail(message); + _logger.LogInformation($"Added forgot password request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(DateTime.UtcNow)}."); + }, + cancellationToken); + } + + public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) { + var requestsToRemove = _context.ForgotPasswordRequests.Where(c => c.UserId == userId).ToList(); + if (!requestsToRemove.Any()) return; + _context.ForgotPasswordRequests.RemoveRange(requestsToRemove); + await _context.SaveChangesAsync(cancellationToken); + _logger.LogInformation($"Deleted {requestsToRemove.Count} forgot password requests for user: {userId}."); + } + + + public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) { + var deleteCount = 0; + foreach (var request in _context.ForgotPasswordRequests) { + if (!request.IsExpired) { + continue; + } + + _context.ForgotPasswordRequests.Remove(request); + deleteCount++; + _logger.LogInformation($"Marking forgot password request with id: {request.Id} for deletion, expiration date was {request.ExpirationDate}."); + } + + await _context.SaveChangesAsync(cancellationToken); + _logger.LogInformation($"Deleted {deleteCount} stale forgot password requests."); + } +} diff --git a/server/src/Services/MailService.cs b/server/src/Services/MailService.cs new file mode 100644 index 0000000..b271de4 --- /dev/null +++ b/server/src/Services/MailService.cs @@ -0,0 +1,52 @@ +namespace IOL.GreatOffice.Api.Services; + +public class MailService +{ + private readonly ILogger<MailService> _logger; + private static string _emailHost; + private static int _emailPort; + private static string _emailUser; + private static string _emailPassword; + + /// <summary> + /// Provides methods to send email. + /// </summary> + /// <param name="configuration"></param> + /// <param name="logger"></param> + public MailService(IConfiguration configuration, ILogger<MailService> logger) { + _logger = logger; + _emailHost = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_HOST); + _emailPort = configuration.GetValue<int>(AppEnvironmentVariables.SMTP_PORT); + _emailUser = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_USER); + _emailPassword = configuration.GetValue<string>(AppEnvironmentVariables.SMTP_PASSWORD); + } + + /// <summary> + /// Send an email. + /// </summary> + /// <param name="message"></param> + public void SendMail(MailMessage message) { + using var smtpClient = new SmtpClient { + Host = _emailHost, + EnableSsl = _emailPort == 587, + Port = _emailPort, + Credentials = new NetworkCredential { + UserName = _emailUser, + Password = _emailPassword, + } + }; + var configurationString = JsonSerializer.Serialize(new { + Host = smtpClient.Host, + EnableSsl = smtpClient.EnableSsl, + Port = smtpClient.Port, + UserName = _emailUser.HasValue() ? "**REDACTED**" : "**MISSING**", + Password = _emailPassword.HasValue() ? "**REDACTED**" : "**MISSING**", + }, + new JsonSerializerOptions { + WriteIndented = true + }); + _logger.LogDebug("SmtpClient was instansiated with the following configuration\n" + configurationString); + + smtpClient.Send(message); + } +} diff --git a/server/src/Services/UserService.cs b/server/src/Services/UserService.cs new file mode 100644 index 0000000..9b531de --- /dev/null +++ b/server/src/Services/UserService.cs @@ -0,0 +1,50 @@ +namespace IOL.GreatOffice.Api.Services; + +public class UserService +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// <summary> + /// Provides methods to perform common operations on user data. + /// </summary> + /// <param name="forgotPasswordService"></param> + public UserService(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// <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, + }; + + if (persist) { + authenticationProperties.ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(6); + authenticationProperties.IsPersistent = true; + } + + await httpContext.SignInAsync(principal, authenticationProperties); + await _forgotPasswordService.DeleteRequestsForUserAsync(user.Id); + } + + /// <summary> + /// Log out a user. + /// </summary> + /// <param name="httpContext"></param> + public async Task LogOutUser(HttpContext httpContext) { + await httpContext.SignOutAsync(); + } +} diff --git a/server/src/Utilities/BasicAuthenticationAttribute.cs b/server/src/Utilities/BasicAuthenticationAttribute.cs new file mode 100644 index 0000000..0bfd007 --- /dev/null +++ b/server/src/Utilities/BasicAuthenticationAttribute.cs @@ -0,0 +1,39 @@ +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace IOL.GreatOffice.Api.Utilities; + +public class BasicAuthenticationAttribute : TypeFilterAttribute +{ + public BasicAuthenticationAttribute(string claimPermission) : base(typeof(BasicAuthenticationFilter)) { + Arguments = new object[] { + new Claim(claimPermission, "True") + }; + } +} + +public class BasicAuthenticationFilter : IAuthorizationFilter +{ + private readonly Claim _claim; + + public BasicAuthenticationFilter(Claim claim) { + _claim = claim; + } + + public void OnAuthorization(AuthorizationFilterContext context) { + if (!context.HttpContext.Request.Headers.ContainsKey("Authorization")) return; + try { + var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]); + if (authHeader.Parameter is null) { + context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME); + } + + var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value); + if (!hasClaim) { + context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME); + } + } catch { + // ignore + } + } +} diff --git a/server/src/Utilities/BasicAuthenticationHandler.cs b/server/src/Utilities/BasicAuthenticationHandler.cs new file mode 100644 index 0000000..2b9d9ef --- /dev/null +++ b/server/src/Utilities/BasicAuthenticationHandler.cs @@ -0,0 +1,79 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Encodings.Web; +using Microsoft.Extensions.Options; + +namespace IOL.GreatOffice.Api.Utilities; + +public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> +{ + private readonly AppDbContext _context; + private readonly IConfiguration _configuration; + private readonly ILogger<BasicAuthenticationHandler> _logger; + + public BasicAuthenticationHandler( + IOptionsMonitor<AuthenticationSchemeOptions> options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + AppDbContext context, + IConfiguration configuration + ) : + base(options, logger, encoder, clock) { + _context = context; + _configuration = configuration; + _logger = logger.CreateLogger<BasicAuthenticationHandler>(); + } + + protected override Task<AuthenticateResult> HandleAuthenticateAsync() { + var endpoint = Context.GetEndpoint(); + if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null) + return Task.FromResult(AuthenticateResult.NoResult()); + + if (!Request.Headers.ContainsKey("Authorization")) + return Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header")); + + try { + var token_entropy = _configuration.GetValue<string>("TOKEN_ENTROPY"); + if (token_entropy.IsNullOrWhiteSpace()) { + _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } + + var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); + if (authHeader.Parameter == null) return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + var credentialBytes = Convert.FromBase64String(authHeader.Parameter); + var decrypted_string = Encoding.UTF8.GetString(credentialBytes).DecryptWithAes(token_entropy); + var token_is_guid = Guid.TryParse(decrypted_string, out var token_id); + + if (!token_is_guid) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } + + var token = _context.AccessTokens.Include(c => c.User).SingleOrDefault(c => c.Id == token_id); + if (token == default) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Not Found")); + } + + if (token.HasExpired) { + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Expired")); + } + + var permissions = new List<Claim>() { + new(AppConstants.TOKEN_ALLOW_READ, token.AllowRead.ToString()), + new(AppConstants.TOKEN_ALLOW_UPDATE, token.AllowUpdate.ToString()), + new(AppConstants.TOKEN_ALLOW_CREATE, token.AllowCreate.ToString()), + new(AppConstants.TOKEN_ALLOW_DELETE, token.AllowDelete.ToString()), + }; + var claims = token.User.DefaultClaims().Concat(permissions); + var identity = new ClaimsIdentity(claims, AppConstants.BASIC_AUTH_SCHEME); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, AppConstants.BASIC_AUTH_SCHEME); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } catch (Exception e) { + _logger.LogError(e, $"An exception occured when challenging {AppConstants.BASIC_AUTH_SCHEME}"); + return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); + } + } +} diff --git a/server/src/Utilities/ConfigurationExtensions.cs b/server/src/Utilities/ConfigurationExtensions.cs new file mode 100644 index 0000000..772059a --- /dev/null +++ b/server/src/Utilities/ConfigurationExtensions.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Utilities; + +public static class ConfigurationExtensions +{ + public static string GetAppDatabaseConnectionString(this IConfiguration configuration) { + var host = configuration.GetValue<string>(AppEnvironmentVariables.DB_HOST); + var port = configuration.GetValue<string>(AppEnvironmentVariables.DB_PORT); + var database = configuration.GetValue<string>(AppEnvironmentVariables.DB_NAME); + var user = configuration.GetValue<string>(AppEnvironmentVariables.DB_USER); + var password = configuration.GetValue<string>(AppEnvironmentVariables.DB_PASSWORD); + + if (configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Development") { + return $"Server={host};Port={port};Database={database};User Id={user};Password={password};Include Error Detail=true"; + } + + return $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + public static string GetQuartzDatabaseConnectionString(this IConfiguration Configuration) { + var host = Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_HOST); + var port = Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_PORT); + var database = Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_NAME); + var user = Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_USER); + var password = Configuration.GetValue<string>(AppEnvironmentVariables.QUARTZ_DB_PASSWORD); + return $"Server={host};Port={port};Database={database};User Id={user};Password={password}"; + } + + public static string GetVersion(this IConfiguration configuration) { + var versionFilePath = Path.Combine(AppPaths.AppData.HostPath, "version.txt"); + if (File.Exists(versionFilePath)) { + var versionText = File.ReadAllText(versionFilePath); + return versionText + "-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT"); + } + + return "unknown-" + configuration.GetValue<string>("ASPNETCORE_ENVIRONMENT"); + } +} diff --git a/server/src/Utilities/GithubAuthenticationHelpers.cs b/server/src/Utilities/GithubAuthenticationHelpers.cs new file mode 100644 index 0000000..cf0cabb --- /dev/null +++ b/server/src/Utilities/GithubAuthenticationHelpers.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authentication.OAuth; +using Npgsql; + +namespace IOL.GreatOffice.Api.Utilities; + +public static class GithubAuthenticationHelpers +{ + public static async Task HandleGithubTicketCreation(OAuthCreatingTicketContext context, IConfiguration configuration) { + var githubId = context.Identity?.FindFirst(p => p.Type == ClaimTypes.NameIdentifier)?.Value; + var githubUsername = context.Identity?.FindFirst(p => p.Type == ClaimTypes.Name)?.Value; + var githubEmail = context.Identity?.FindFirst(p => p.Type == ClaimTypes.Email)?.Value; + + if (githubId.IsNullOrWhiteSpace() || githubUsername.IsNullOrWhiteSpace() || context.Identity == default) { + return; + } + + var claims = context.Identity.Claims.ToList(); + foreach (var claim in claims) { + context.Identity.RemoveClaim(claim); + } + + var connstring = configuration.GetAppDatabaseConnectionString(); + var connection = new NpgsqlConnection(connstring); + + Log.Information($"Getting user mappings for github user: {githubId}"); + var getMappedUserQuery = @$"SELECT u.id,u.username FROM github_user_mappings INNER JOIN users u on u.id = github_user_mappings.user_id WHERE github_id='{githubId}'"; + await connection.OpenAsync(); + await using var getMappedUserCommand = new NpgsqlCommand(getMappedUserQuery, connection); + await using var reader = await getMappedUserCommand.ExecuteReaderAsync(); + var handled = false; + while (await reader.ReadAsync()) { + try { + var userId = reader.GetGuid(0); + var username = reader.GetString(1); + context.Identity.AddClaim(new Claim(AppClaims.USER_ID, userId.ToString())); + context.Identity.AddClaim(new Claim(AppClaims.NAME, username)); + if (context.AccessToken != default) { + context.Identity.AddClaim(new Claim(AppClaims.GITHUB_ACCESS_TOKEN, context.AccessToken ?? "")); + } + + Log.Information($"Found mapping for github id {githubId} mapped to user id {userId}"); + handled = true; + } catch (Exception e) { + Log.Error(e, "An exception occured when handling github user mappings"); + handled = false; + } + } + + await connection.CloseAsync(); + + if (!handled) { + var userId = Guid.NewGuid(); + + var insertUserQuery = $@"INSERT INTO users VALUES ('{userId}', '{githubUsername}', '', '{DateTime.UtcNow}')"; + await connection.OpenAsync(); + await using var insertUserCommand = new NpgsqlCommand(insertUserQuery, connection); + await insertUserCommand.ExecuteNonQueryAsync(); + await connection.CloseAsync(); + + var refreshTokenEncryptionKey = configuration.GetValue<string>(AppEnvironmentVariables.APP_AES_KEY); + string insertMappingQuery; + + if (context.RefreshToken.HasValue() && refreshTokenEncryptionKey.HasValue()) { + var encryptedRefreshToken = context.RefreshToken.EncryptWithAes(refreshTokenEncryptionKey); + insertMappingQuery = $@"INSERT INTO github_user_mappings VALUES ('{githubId}', '{userId}', '{githubEmail}', '{encryptedRefreshToken}')"; + } else { + insertMappingQuery = $@"INSERT INTO github_user_mappings VALUES ('{githubId}', '{userId}', '{githubEmail}', '')"; + } + + await connection.OpenAsync(); + await using var insertMappingCommand = new NpgsqlCommand(insertMappingQuery, connection); + await insertMappingCommand.ExecuteNonQueryAsync(); + await connection.CloseAsync(); + + context.Identity.AddClaim(new Claim(AppClaims.USER_ID, userId.ToString())); + context.Identity.AddClaim(new Claim(AppClaims.NAME, githubUsername)); + if (context.AccessToken != default) { + context.Identity.AddClaim(new Claim(AppClaims.GITHUB_ACCESS_TOKEN, context.AccessToken ?? "")); + } + + Log.Information($"Created mapping for github id {githubId} mapped to user id {userId}"); + } + } +} diff --git a/server/src/Utilities/QuartzJsonSerializer.cs b/server/src/Utilities/QuartzJsonSerializer.cs new file mode 100644 index 0000000..164a189 --- /dev/null +++ b/server/src/Utilities/QuartzJsonSerializer.cs @@ -0,0 +1,16 @@ +using Quartz.Spi; + +namespace IOL.GreatOffice.Api.Utilities; + +public class QuartzJsonSerializer : IObjectSerializer +{ + public void Initialize() { } + + public byte[] Serialize<T>(T obj) where T : class { + return JsonSerializer.SerializeToUtf8Bytes(obj); + } + + public T DeSerialize<T>(byte[] data) where T : class { + return JsonSerializer.Deserialize<T>(data); + } +} diff --git a/server/src/Utilities/SwaggerDefaultValues.cs b/server/src/Utilities/SwaggerDefaultValues.cs new file mode 100644 index 0000000..4b5c764 --- /dev/null +++ b/server/src/Utilities/SwaggerDefaultValues.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace IOL.GreatOffice.Api.Utilities; + +/// <summary> +/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. +/// </summary> +/// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>. +/// Once they are fixed and published, this class can be removed.</remarks> +public class SwaggerDefaultValues : IOperationFilter +{ + /// <summary> + /// Applies the filter to the specified operation using the given context. + /// </summary> + /// <param name="operation">The operation to apply the filter to.</param> + /// <param name="context">The current operation filter context.</param> + public void Apply(OpenApiOperation operation, OperationFilterContext context) { + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077 + foreach (var responseType in context.ApiDescription.SupportedResponseTypes) { + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387 + var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(); + var response = operation.Responses[responseKey]; + + foreach (var contentType in response.Content.Keys) { + if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType)) { + response.Content.Remove(contentType); + } + } + } + + if (operation.Parameters == null) { + return; + } + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 + foreach (var parameter in operation.Parameters) { + var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + if (parameter.Description == null) { + parameter.Description = description.ModelMetadata.Description; + } + + if (parameter.Schema.Default == null && description.DefaultValue != null) { + // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 + var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); + } + + parameter.Required |= description.IsRequired; + } + } +} diff --git a/server/src/Utilities/SwaggerGenOptionsExtensions.cs b/server/src/Utilities/SwaggerGenOptionsExtensions.cs new file mode 100644 index 0000000..a2dcf7a --- /dev/null +++ b/server/src/Utilities/SwaggerGenOptionsExtensions.cs @@ -0,0 +1,43 @@ +#nullable enable +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Swashbuckle.AspNetCore.SwaggerGen; +using BaseRoute = IOL.GreatOffice.Api.Endpoints.V1.BaseRoute; + +namespace IOL.GreatOffice.Api.Utilities; + +public static class SwaggerGenOptionsExtensions +{ + /// <summary> + /// Updates Swagger document to support ApiEndpoints.<br/><br/> + /// For controllers inherited from <see cref="BaseRoute"/>:<br/> + /// - Replaces action Tag with <c>[namespace]</c><br/> + /// </summary> + public static void UseApiEndpoints(this SwaggerGenOptions options) { + options.TagActionsBy(EndpointNamespaceOrDefault); + } + + private static IList<string?> EndpointNamespaceOrDefault(ApiDescription api) { + if (api.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) { + throw new InvalidOperationException($"Unable to determine tag for endpoint: {api.ActionDescriptor.DisplayName}"); + } + + if (actionDescriptor.ControllerTypeInfo.GetBaseTypesAndThis().Any(t => t == typeof(BaseRoute))) { + return new[] { + actionDescriptor.ControllerTypeInfo.Namespace?.Split('.').Last() + }; + } + + return new[] { + actionDescriptor.ControllerName + }; + } + + public static IEnumerable<Type> GetBaseTypesAndThis(this Type type) { + Type? current = type; + while (current != null) { + yield return current; + current = current.BaseType; + } + } +} diff --git a/server/src/appsettings.json b/server/src/appsettings.json new file mode 100644 index 0000000..8727fd7 --- /dev/null +++ b/server/src/appsettings.json @@ -0,0 +1,22 @@ +{ + "AllowedHosts": "*", + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Filter": [ + { + "Name": "ByExcluding", + "Args": { + "expression": "@mt = 'An unhandled exception has occurred while executing the request.'" + } + } + ] + } +} diff --git a/server/src/wwwroot/version.txt b/server/src/wwwroot/version.txt new file mode 100644 index 0000000..863c2f5 --- /dev/null +++ b/server/src/wwwroot/version.txt @@ -0,0 +1 @@ +v35-server-dev |
