diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-12-21 23:37:23 +0100 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-12-21 23:37:23 +0100 |
| commit | 82ade3c31fb17b662feec59e9e654ceb66edbb7a (patch) | |
| tree | 26443c41c55d2cd2ae46fdd0d663aca84b779ffe | |
| parent | e60703aadca7d423c0fbfb189d5ef439fc1df072 (diff) | |
| download | storage-82ade3c31fb17b662feec59e9e654ceb66edbb7a.tar.xz storage-82ade3c31fb17b662feec59e9e654ceb66edbb7a.zip | |
feat: Add initial schema and start login
45 files changed, 2100 insertions, 34 deletions
@@ -591,3 +591,6 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ +.idea +.vscode +STORAGE_ROOT diff --git a/code/api/.idea/.idea.api.dir/.idea/.gitignore b/code/api/.idea/.idea.api.dir/.idea/.gitignore new file mode 100644 index 0000000..f1978cd --- /dev/null +++ b/code/api/.idea/.idea.api.dir/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.api.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/code/api/.idea/.idea.api.dir/.idea/encodings.xml b/code/api/.idea/.idea.api.dir/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/code/api/.idea/.idea.api.dir/.idea/encodings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" /> +</project>
\ No newline at end of file diff --git a/code/api/.idea/.idea.api.dir/.idea/indexLayout.xml b/code/api/.idea/.idea.api.dir/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/code/api/.idea/.idea.api.dir/.idea/indexLayout.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="UserContentModel"> + <attachedFolders /> + <explicitIncludes /> + <explicitExcludes /> + </component> +</project>
\ No newline at end of file diff --git a/code/api/.idea/.idea.api.dir/.idea/vcs.xml b/code/api/.idea/.idea.api.dir/.idea/vcs.xml new file mode 100644 index 0000000..e343e08 --- /dev/null +++ b/code/api/.idea/.idea.api.dir/.idea/vcs.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CommitMessageInspectionProfile"> + <profile version="1.0"> + <inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" /> + </profile> + </component> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/code/api/Database/AppDatabase.cs b/code/api/Database/AppDatabase.cs new file mode 100644 index 0000000..722a3e0 --- /dev/null +++ b/code/api/Database/AppDatabase.cs @@ -0,0 +1,36 @@ +using File = I2R.Storage.Api.Database.Models.File; + +namespace I2R.Storage.Api.Database; + +public class AppDatabase : DbContext +{ + public AppDatabase(DbContextOptions<AppDatabase> options) : base(options) { } + public DbSet<User> Users { get; set; } + public DbSet<File> Files { get; set; } + public DbSet<Folder> Folders { get; set; } + public DbSet<Permission> Permissions { get; set; } + public DbSet<PermissionGroup> PermissionGroups { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity<User>(e => { e.ToTable("users"); }); + modelBuilder.Entity<File>(e => { + e.HasMany(c => c.Permissions); + e.HasOne(c => c.Folder); + e.ToTable("files"); + }); + modelBuilder.Entity<Folder>(e => { + e.HasMany(c => c.Files); + e.HasMany(c => c.Permissions); + e.ToTable("folders"); + }); + modelBuilder.Entity<PermissionGroup>(e => { + e.HasMany(c => c.Users); + e.ToTable("permission_groups"); + }); + modelBuilder.Entity<Permission>(e => { + e.HasOne(c => c.Group); + e.ToTable("permissions"); + }); + base.OnModelCreating(modelBuilder); + } +}
\ No newline at end of file diff --git a/code/api/Database/Models/File.cs b/code/api/Database/Models/File.cs new file mode 100644 index 0000000..b1f51a5 --- /dev/null +++ b/code/api/Database/Models/File.cs @@ -0,0 +1,13 @@ +namespace I2R.Storage.Api.Database.Models; + +public class File : Base +{ + public string Name { get; set; } + public string MimeType { get; set; } + public long SizeInBytes { get; set; } + public Folder Folder { get; set; } + public Guid FolderId { get; set; } + public bool IsEncrypted { get; set; } + public bool IsBinned { get; set; } + public List<Permission> Permissions { get; set; } +}
\ No newline at end of file diff --git a/code/api/Database/Models/Folder.cs b/code/api/Database/Models/Folder.cs new file mode 100644 index 0000000..ecfed1e --- /dev/null +++ b/code/api/Database/Models/Folder.cs @@ -0,0 +1,10 @@ +namespace I2R.Storage.Api.Database.Models; + +public class Folder : Base +{ + public string Name { get; set; } + public List<File> Files { get; set; } + public List<Permission> Permissions { get; set; } + public bool IsEncrypted { get; set; } + public bool IsBinned { get; set; } +}
\ No newline at end of file diff --git a/code/api/Database/Models/Permission.cs b/code/api/Database/Models/Permission.cs new file mode 100644 index 0000000..3076d0e --- /dev/null +++ b/code/api/Database/Models/Permission.cs @@ -0,0 +1,11 @@ +namespace I2R.Storage.Api.Database.Models; + +public class Permission : Base +{ + public Guid ContentId { get; set; } + public bool IsFile { get; set; } + public bool CanRead { get; set; } + public bool CanWrite { get; set; } + public Guid GroupId { get; set; } + public PermissionGroup Group { get; set; } +}
\ No newline at end of file diff --git a/code/api/Database/Models/PermissionGroup.cs b/code/api/Database/Models/PermissionGroup.cs new file mode 100644 index 0000000..712f0cb --- /dev/null +++ b/code/api/Database/Models/PermissionGroup.cs @@ -0,0 +1,8 @@ +namespace I2R.Storage.Api.Database.Models; + +public class PermissionGroup : Base +{ + public string Name { get; set; } + public string Description { get; set; } + public List<User> Users { get; set; } +}
\ No newline at end of file diff --git a/code/api/Database/Models/User.cs b/code/api/Database/Models/User.cs new file mode 100644 index 0000000..7f7b0e9 --- /dev/null +++ b/code/api/Database/Models/User.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; + +namespace I2R.Storage.Api.Database.Models; + +public class User : Base +{ + public User() { } + + public User(Guid createdBy) : base(createdBy) { } + public string Username { get; set; } + public string Password { get; set; } + public EUserRole Role { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime? LastLoggedOn { get; set; } + + + public IEnumerable<Claim> DefaultClaims() => new List<Claim>() { + new(AppClaims.USER_ID, Id.ToString()), + new(AppClaims.USERNAME, Username), + new(AppClaims.USER_ROLE, UserRole.ToString(Role)) + }; +}
\ No newline at end of file diff --git a/code/api/Database/Models/_Base.cs b/code/api/Database/Models/_Base.cs new file mode 100644 index 0000000..2a05a3a --- /dev/null +++ b/code/api/Database/Models/_Base.cs @@ -0,0 +1,34 @@ +namespace I2R.Storage.Api.Database.Models; + +public class Base +{ + public Base() { + Id = Guid.NewGuid(); + CreatedAt = AppDateTime.UtcNow; + } + + public Base(Guid createdBy) { + Id = Guid.NewGuid(); + CreatedAt = AppDateTime.UtcNow; + CreatedBy = createdBy; + } + + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? LastModifiedAt { get; set; } + public DateTime? LastDeletedAt { get; set; } + public Guid? OwningUserId { get; set; } + public Guid? LastModifiedBy { get; set; } + public Guid? LastDeletedBy { get; set; } + public Guid? CreatedBy { get; set; } + + public void SetDeleted(Guid performingUserId = default) { + LastDeletedAt = AppDateTime.UtcNow; + LastDeletedBy = performingUserId; + } + + public void SetModified(Guid performingUserId = default) { + LastModifiedAt = AppDateTime.UtcNow; + LastModifiedBy = performingUserId; + } +}
\ No newline at end of file diff --git a/code/api/Endpoints/Account/CreateEndpoint.cs b/code/api/Endpoints/Account/CreateEndpoint.cs new file mode 100644 index 0000000..41ffe96 --- /dev/null +++ b/code/api/Endpoints/Account/CreateEndpoint.cs @@ -0,0 +1,52 @@ +namespace I2R.Storage.Api.Endpoints.Account; + +public class CreateEndpoint : Base +{ + private readonly AppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer<SharedResources> _localizer; + + public CreateEndpoint(AppDatabase database, UserService userService, IStringLocalizer<SharedResources> localizer) { + _database = database; + _userService = userService; + _localizer = localizer; + } + + public new class Request + { + public string Username { get; set; } + public string Password { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + } + + public new class Response + { + public Guid Id { get; set; } + public string Username { get; set; } + public EUserRole Role { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/account/create")] + public ActionResult Handle([FromBody] Request request) { + if (!_userService.CanCreateAccount(request.Username)) { + return BadRequest(_localizer["That username is already taken"]); + } + + var user = new User() { + Username = request.Username, + Password = PasswordHelper.HashPassword(request.Password), + LastName = request.LastName, + FirstName = request.FirstName, + Role = EUserRole.LEAST_PRIVILEGED, + }; + _database.Users.Add(user); + _database.SaveChanges(); + return Ok(new Response { + Id = user.Id, + Username = user.Username, + Role = user.Role + }); + } +}
\ No newline at end of file diff --git a/code/api/Endpoints/Account/LoginEndpoint.cs b/code/api/Endpoints/Account/LoginEndpoint.cs new file mode 100644 index 0000000..0ffed0f --- /dev/null +++ b/code/api/Endpoints/Account/LoginEndpoint.cs @@ -0,0 +1,38 @@ +using I2R.Storage.Api.Endpoints._Root; + +namespace I2R.Storage.Api.Endpoints.Account; + +public class LoginEndpoint : Base +{ + private readonly AppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer<SharedResources> _localizer; + + public new class Request + { + public string Username { get; set; } + public string Password { get; set; } + } + + public LoginEndpoint(UserService userService, AppDatabase database, IStringLocalizer<SharedResources> localizer) { + _userService = userService; + _database = database; + _localizer = localizer; + } + + [AllowAnonymous] + [HttpPost("~/account/login")] + public async Task<ActionResult> Handle([FromBody] Request request) { + var user = _database.Users.FirstOrDefault(c => c.Username == request.Username); + if (user == default) { + return BadRequest(_localizer["Invalid username or password"]); + } + + if (!PasswordHelper.Verify(request.Password, user.Password)) { + return BadRequest(_localizer["Invalid username or password"]); + } + + await _userService.LogInUserAsync(HttpContext, user.DefaultClaims()); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/Endpoints/Account/LogoutEndpoint.cs b/code/api/Endpoints/Account/LogoutEndpoint.cs new file mode 100644 index 0000000..064fa9f --- /dev/null +++ b/code/api/Endpoints/Account/LogoutEndpoint.cs @@ -0,0 +1,16 @@ +namespace I2R.Storage.Api.Endpoints.Account; + +public class LogoutEndpoint : Base +{ + private readonly UserService _userService; + + public LogoutEndpoint(UserService userService) { + _userService = userService; + } + + [HttpGet("~/account/logout")] + public async Task<ActionResult> Handle() { + await _userService.LogOutUserAsync(HttpContext); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/Endpoints/Account/create.http b/code/api/Endpoints/Account/create.http new file mode 100644 index 0000000..b29352b --- /dev/null +++ b/code/api/Endpoints/Account/create.http @@ -0,0 +1,10 @@ +POST http://localhost:5068/account/create +Content-Type: application/json;charset=utf-8 +Accept: application/json + +{ +"username": "ivar", +"password": "ivar123", +"firstName": "Ivar", +"lastName": "Løvlie" +}
\ No newline at end of file diff --git a/code/api/Endpoints/Base.cs b/code/api/Endpoints/Base.cs new file mode 100644 index 0000000..211d1f6 --- /dev/null +++ b/code/api/Endpoints/Base.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; + +namespace I2R.Storage.Api.Endpoints; + +[ApiController] +[Authorize] +public class Base : ControllerBase +{ + public class LoggedInUserModel + { + public string Username { get; set; } + public Guid Id { get; set; } + public EUserRole Role { get; set; } + + public class Public + { + public string Id { get; set; } + public string Username { get; set; } + public string Role { get; set; } + } + + public Public ForThePeople(HttpContext httpContext) { + return new Public() { + Id = httpContext.User.FindFirstValue(AppClaims.USER_ID), + Username = httpContext.User.FindFirstValue(AppClaims.USERNAME), + Role = httpContext.User.FindFirstValue(AppClaims.USER_ROLE) + }; + } + } + + public LoggedInUserModel LoggedInUser => new LoggedInUserModel() { + Id = HttpContext.User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + Username = HttpContext.User.FindFirstValue(AppClaims.USERNAME), + Role = UserRole.FromString(HttpContext.User.FindFirstValue(AppClaims.USER_ROLE)) + }; +}
\ No newline at end of file diff --git a/code/api/Endpoints/_Root/SessionEndpoint.cs b/code/api/Endpoints/_Root/SessionEndpoint.cs new file mode 100644 index 0000000..8d6ca56 --- /dev/null +++ b/code/api/Endpoints/_Root/SessionEndpoint.cs @@ -0,0 +1,9 @@ +namespace I2R.Storage.Api.Endpoints._Root; + +public class SessionEndpoint : Base +{ + [HttpGet("~/session")] + public ActionResult<LoggedInUserModel.Public> Handle() { + return LoggedInUser.ForThePeople(HttpContext); + } +}
\ No newline at end of file diff --git a/code/api/Enums/EUserRole.cs b/code/api/Enums/EUserRole.cs new file mode 100644 index 0000000..75a81b3 --- /dev/null +++ b/code/api/Enums/EUserRole.cs @@ -0,0 +1,22 @@ +namespace I2R.Storage.Api.Enums; + +public enum EUserRole +{ + LEAST_PRIVILEGED = 0, + ADMIN = 1, +} + +public static class UserRole +{ + public static EUserRole FromString(string role) => role switch { + "least_privileged" => EUserRole.LEAST_PRIVILEGED, + "admin" => EUserRole.ADMIN, + _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) + }; + + public static string ToString(EUserRole role) => role switch { + EUserRole.LEAST_PRIVILEGED => "least_privileged", + EUserRole.ADMIN => "admin", + _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) + }; +}
\ No newline at end of file diff --git a/code/api/I2R.Storage.Api.csproj b/code/api/I2R.Storage.Api.csproj index ce90fce..b601c37 100644 --- a/code/api/I2R.Storage.Api.csproj +++ b/code/api/I2R.Storage.Api.csproj @@ -1,14 +1,31 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> +<Project Sdk="Microsoft.NET.Sdk.Web"> - <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> - <Nullable>enable</Nullable> - <ImplicitUsings>enable</ImplicitUsings> - </PropertyGroup> + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <UserSecretsId>a85b8d41-ecb0-4e83-b92d-f409b43144fe</UserSecretsId> + </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> - </ItemGroup> + <ItemGroup> + <PackageReference Include="EFCore.NamingConventions" Version="7.0.0" /> + <PackageReference Include="IOL.Helpers" Version="3.1.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Update="Resources\SharedResources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>SharedResources.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + + <ItemGroup> + <Folder Include="STORAGE_ROOT" /> + </ItemGroup> </Project> diff --git a/code/api/Migrations/20221221214429_InitialYay.Designer.cs b/code/api/Migrations/20221221214429_InitialYay.Designer.cs new file mode 100644 index 0000000..e368363 --- /dev/null +++ b/code/api/Migrations/20221221214429_InitialYay.Designer.cs @@ -0,0 +1,418 @@ +// <auto-generated /> +using System; +using I2R.Storage.Api.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace I2R.Storage.Api.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20221221214429_InitialYay")] + partial class InitialYay + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<Guid>("FolderId") + .HasColumnType("uuid") + .HasColumnName("folder_id"); + + b.Property<bool>("IsBinned") + .HasColumnType("boolean") + .HasColumnName("is_binned"); + + b.Property<bool>("IsEncrypted") + .HasColumnType("boolean") + .HasColumnName("is_encrypted"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("MimeType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("mime_type"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.Property<long>("SizeInBytes") + .HasColumnType("bigint") + .HasColumnName("size_in_bytes"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.HasIndex("FolderId") + .HasDatabaseName("ix_files_folder_id"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Folder", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<bool>("IsBinned") + .HasColumnType("boolean") + .HasColumnName("is_binned"); + + b.Property<bool>("IsEncrypted") + .HasColumnType("boolean") + .HasColumnName("is_encrypted"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_folders"); + + b.ToTable("folders", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Permission", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("CanRead") + .HasColumnType("boolean") + .HasColumnName("can_read"); + + b.Property<bool>("CanWrite") + .HasColumnType("boolean") + .HasColumnName("can_write"); + + b.Property<Guid>("ContentId") + .HasColumnType("uuid") + .HasColumnName("content_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<Guid?>("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property<Guid?>("FolderId") + .HasColumnType("uuid") + .HasColumnName("folder_id"); + + b.Property<Guid>("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property<bool>("IsFile") + .HasColumnType("boolean") + .HasColumnName("is_file"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_permissions_file_id"); + + b.HasIndex("FolderId") + .HasDatabaseName("ix_permissions_folder_id"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permissions_group_id"); + + b.ToTable("permissions", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.PermissionGroup", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<string>("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.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<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<string>("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastLoggedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_on"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<Guid?>("PermissionGroupId") + .HasColumnType("uuid") + .HasColumnName("permission_group_id"); + + b.Property<int>("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property<string>("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("PermissionGroupId") + .HasDatabaseName("ix_users_permission_group_id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.Folder", "Folder") + .WithMany("Files") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_files_folders_folder_id"); + + b.Navigation("Folder"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Permission", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.File", null) + .WithMany("Permissions") + .HasForeignKey("FileId") + .HasConstraintName("fk_permissions_files_file_id"); + + b.HasOne("I2R.Storage.Api.Database.Models.Folder", null) + .WithMany("Permissions") + .HasForeignKey("FolderId") + .HasConstraintName("fk_permissions_folders_folder_id"); + + b.HasOne("I2R.Storage.Api.Database.Models.PermissionGroup", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permissions_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.User", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.PermissionGroup", null) + .WithMany("Users") + .HasForeignKey("PermissionGroupId") + .HasConstraintName("fk_users_permission_groups_permission_group_id"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Folder", b => + { + b.Navigation("Files"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.PermissionGroup", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/api/Migrations/20221221214429_InitialYay.cs b/code/api/Migrations/20221221214429_InitialYay.cs new file mode 100644 index 0000000..122e3d9 --- /dev/null +++ b/code/api/Migrations/20221221214429_InitialYay.cs @@ -0,0 +1,201 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace I2R.Storage.Api.Migrations +{ + /// <inheritdoc /> + public partial class InitialYay : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "folders", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: false), + isencrypted = table.Column<bool>(name: "is_encrypted", type: "boolean", nullable: false), + isbinned = table.Column<bool>(name: "is_binned", type: "boolean", nullable: false), + createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false), + lastmodifiedat = table.Column<DateTime>(name: "last_modified_at", type: "timestamp with time zone", nullable: true), + lastdeletedat = table.Column<DateTime>(name: "last_deleted_at", type: "timestamp with time zone", nullable: true), + owninguserid = table.Column<Guid>(name: "owning_user_id", type: "uuid", nullable: true), + lastmodifiedby = table.Column<Guid>(name: "last_modified_by", type: "uuid", nullable: true), + lastdeletedby = table.Column<Guid>(name: "last_deleted_by", type: "uuid", nullable: true), + createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_folders", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "permission_groups", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: false), + description = table.Column<string>(type: "text", nullable: false), + createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false), + lastmodifiedat = table.Column<DateTime>(name: "last_modified_at", type: "timestamp with time zone", nullable: true), + lastdeletedat = table.Column<DateTime>(name: "last_deleted_at", type: "timestamp with time zone", nullable: true), + owninguserid = table.Column<Guid>(name: "owning_user_id", type: "uuid", nullable: true), + lastmodifiedby = table.Column<Guid>(name: "last_modified_by", type: "uuid", nullable: true), + lastdeletedby = table.Column<Guid>(name: "last_deleted_by", type: "uuid", nullable: true), + createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_permission_groups", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "files", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: false), + mimetype = table.Column<string>(name: "mime_type", type: "text", nullable: false), + sizeinbytes = table.Column<long>(name: "size_in_bytes", type: "bigint", nullable: false), + folderid = table.Column<Guid>(name: "folder_id", type: "uuid", nullable: false), + isencrypted = table.Column<bool>(name: "is_encrypted", type: "boolean", nullable: false), + isbinned = table.Column<bool>(name: "is_binned", type: "boolean", nullable: false), + createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false), + lastmodifiedat = table.Column<DateTime>(name: "last_modified_at", type: "timestamp with time zone", nullable: true), + lastdeletedat = table.Column<DateTime>(name: "last_deleted_at", type: "timestamp with time zone", nullable: true), + owninguserid = table.Column<Guid>(name: "owning_user_id", type: "uuid", nullable: true), + lastmodifiedby = table.Column<Guid>(name: "last_modified_by", type: "uuid", nullable: true), + lastdeletedby = table.Column<Guid>(name: "last_deleted_by", type: "uuid", nullable: true), + createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_files", x => x.id); + table.ForeignKey( + name: "fk_files_folders_folder_id", + column: x => x.folderid, + principalTable: "folders", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + username = table.Column<string>(type: "text", nullable: false), + password = table.Column<string>(type: "text", nullable: false), + role = table.Column<int>(type: "integer", nullable: false), + firstname = table.Column<string>(name: "first_name", type: "text", nullable: false), + lastname = table.Column<string>(name: "last_name", type: "text", nullable: false), + lastloggedon = table.Column<DateTime>(name: "last_logged_on", type: "timestamp with time zone", nullable: true), + permissiongroupid = table.Column<Guid>(name: "permission_group_id", type: "uuid", nullable: true), + createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false), + lastmodifiedat = table.Column<DateTime>(name: "last_modified_at", type: "timestamp with time zone", nullable: true), + lastdeletedat = table.Column<DateTime>(name: "last_deleted_at", type: "timestamp with time zone", nullable: true), + owninguserid = table.Column<Guid>(name: "owning_user_id", type: "uuid", nullable: true), + lastmodifiedby = table.Column<Guid>(name: "last_modified_by", type: "uuid", nullable: true), + lastdeletedby = table.Column<Guid>(name: "last_deleted_by", type: "uuid", nullable: true), + createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + table.ForeignKey( + name: "fk_users_permission_groups_permission_group_id", + column: x => x.permissiongroupid, + principalTable: "permission_groups", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "permissions", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + contentid = table.Column<Guid>(name: "content_id", type: "uuid", nullable: false), + isfile = table.Column<bool>(name: "is_file", type: "boolean", nullable: false), + canread = table.Column<bool>(name: "can_read", type: "boolean", nullable: false), + canwrite = table.Column<bool>(name: "can_write", type: "boolean", nullable: false), + groupid = table.Column<Guid>(name: "group_id", type: "uuid", nullable: false), + fileid = table.Column<Guid>(name: "file_id", type: "uuid", nullable: true), + folderid = table.Column<Guid>(name: "folder_id", type: "uuid", nullable: true), + createdat = table.Column<DateTime>(name: "created_at", type: "timestamp with time zone", nullable: false), + lastmodifiedat = table.Column<DateTime>(name: "last_modified_at", type: "timestamp with time zone", nullable: true), + lastdeletedat = table.Column<DateTime>(name: "last_deleted_at", type: "timestamp with time zone", nullable: true), + owninguserid = table.Column<Guid>(name: "owning_user_id", type: "uuid", nullable: true), + lastmodifiedby = table.Column<Guid>(name: "last_modified_by", type: "uuid", nullable: true), + lastdeletedby = table.Column<Guid>(name: "last_deleted_by", type: "uuid", nullable: true), + createdby = table.Column<Guid>(name: "created_by", type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_permissions", x => x.id); + table.ForeignKey( + name: "fk_permissions_files_file_id", + column: x => x.fileid, + principalTable: "files", + principalColumn: "id"); + table.ForeignKey( + name: "fk_permissions_folders_folder_id", + column: x => x.folderid, + principalTable: "folders", + principalColumn: "id"); + table.ForeignKey( + name: "fk_permissions_permission_groups_group_id", + column: x => x.groupid, + principalTable: "permission_groups", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_files_folder_id", + table: "files", + column: "folder_id"); + + migrationBuilder.CreateIndex( + name: "ix_permissions_file_id", + table: "permissions", + column: "file_id"); + + migrationBuilder.CreateIndex( + name: "ix_permissions_folder_id", + table: "permissions", + column: "folder_id"); + + migrationBuilder.CreateIndex( + name: "ix_permissions_group_id", + table: "permissions", + column: "group_id"); + + migrationBuilder.CreateIndex( + name: "ix_users_permission_group_id", + table: "users", + column: "permission_group_id"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "permissions"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "files"); + + migrationBuilder.DropTable( + name: "permission_groups"); + + migrationBuilder.DropTable( + name: "folders"); + } + } +} diff --git a/code/api/Migrations/AppDatabaseModelSnapshot.cs b/code/api/Migrations/AppDatabaseModelSnapshot.cs new file mode 100644 index 0000000..9f0ab71 --- /dev/null +++ b/code/api/Migrations/AppDatabaseModelSnapshot.cs @@ -0,0 +1,415 @@ +// <auto-generated /> +using System; +using I2R.Storage.Api.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace I2R.Storage.Api.Migrations +{ + [DbContext(typeof(AppDatabase))] + partial class AppDatabaseModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<Guid>("FolderId") + .HasColumnType("uuid") + .HasColumnName("folder_id"); + + b.Property<bool>("IsBinned") + .HasColumnType("boolean") + .HasColumnName("is_binned"); + + b.Property<bool>("IsEncrypted") + .HasColumnType("boolean") + .HasColumnName("is_encrypted"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("MimeType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("mime_type"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.Property<long>("SizeInBytes") + .HasColumnType("bigint") + .HasColumnName("size_in_bytes"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.HasIndex("FolderId") + .HasDatabaseName("ix_files_folder_id"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Folder", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<bool>("IsBinned") + .HasColumnType("boolean") + .HasColumnName("is_binned"); + + b.Property<bool>("IsEncrypted") + .HasColumnType("boolean") + .HasColumnName("is_encrypted"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_folders"); + + b.ToTable("folders", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Permission", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("CanRead") + .HasColumnType("boolean") + .HasColumnName("can_read"); + + b.Property<bool>("CanWrite") + .HasColumnType("boolean") + .HasColumnName("can_write"); + + b.Property<Guid>("ContentId") + .HasColumnType("uuid") + .HasColumnName("content_id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<Guid?>("FileId") + .HasColumnType("uuid") + .HasColumnName("file_id"); + + b.Property<Guid?>("FolderId") + .HasColumnType("uuid") + .HasColumnName("folder_id"); + + b.Property<Guid>("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property<bool>("IsFile") + .HasColumnType("boolean") + .HasColumnName("is_file"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_permissions_file_id"); + + b.HasIndex("FolderId") + .HasDatabaseName("ix_permissions_folder_id"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permissions_group_id"); + + b.ToTable("permissions", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.PermissionGroup", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<string>("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.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<Guid?>("CreatedBy") + .HasColumnType("uuid") + .HasColumnName("created_by"); + + b.Property<string>("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property<DateTime?>("LastDeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_deleted_at"); + + b.Property<Guid?>("LastDeletedBy") + .HasColumnType("uuid") + .HasColumnName("last_deleted_by"); + + b.Property<DateTime?>("LastLoggedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_logged_on"); + + b.Property<DateTime?>("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified_at"); + + b.Property<Guid?>("LastModifiedBy") + .HasColumnType("uuid") + .HasColumnName("last_modified_by"); + + b.Property<string>("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property<Guid?>("OwningUserId") + .HasColumnType("uuid") + .HasColumnName("owning_user_id"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property<Guid?>("PermissionGroupId") + .HasColumnType("uuid") + .HasColumnName("permission_group_id"); + + b.Property<int>("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property<string>("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("PermissionGroupId") + .HasDatabaseName("ix_users_permission_group_id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.Folder", "Folder") + .WithMany("Files") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_files_folders_folder_id"); + + b.Navigation("Folder"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Permission", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.File", null) + .WithMany("Permissions") + .HasForeignKey("FileId") + .HasConstraintName("fk_permissions_files_file_id"); + + b.HasOne("I2R.Storage.Api.Database.Models.Folder", null) + .WithMany("Permissions") + .HasForeignKey("FolderId") + .HasConstraintName("fk_permissions_folders_folder_id"); + + b.HasOne("I2R.Storage.Api.Database.Models.PermissionGroup", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permissions_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.User", b => + { + b.HasOne("I2R.Storage.Api.Database.Models.PermissionGroup", null) + .WithMany("Users") + .HasForeignKey("PermissionGroupId") + .HasConstraintName("fk_users_permission_groups_permission_group_id"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.File", b => + { + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.Folder", b => + { + b.Navigation("Files"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("I2R.Storage.Api.Database.Models.PermissionGroup", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/api/Pages/Home.cshtml b/code/api/Pages/Home.cshtml new file mode 100644 index 0000000..457ba2f --- /dev/null +++ b/code/api/Pages/Home.cshtml @@ -0,0 +1,12 @@ +@page +@model I2R.Storage.Api.Pages.Home + +@{ + ViewData["Title"] = "Home"; +} + +<h1>Welcome</h1> + +<pre> +Id: @(HttpContext.User.FindFirst("AppClaims.USER_ID").Value) +</pre>
\ No newline at end of file diff --git a/code/api/Pages/Home.cshtml.cs b/code/api/Pages/Home.cshtml.cs new file mode 100644 index 0000000..0fa9aca --- /dev/null +++ b/code/api/Pages/Home.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace I2R.Storage.Api.Pages; + +public class Home : PageModel +{ + public void OnGet() { + + } +}
\ No newline at end of file diff --git a/code/api/Pages/Index.cshtml b/code/api/Pages/Index.cshtml new file mode 100644 index 0000000..4015b8e --- /dev/null +++ b/code/api/Pages/Index.cshtml @@ -0,0 +1,5 @@ +@page +@model I2R.Storage.Api.Pages.Index +@{ + ViewData["Title"] = "Home"; +} diff --git a/code/api/Pages/Index.cshtml.cs b/code/api/Pages/Index.cshtml.cs new file mode 100644 index 0000000..8129a9b --- /dev/null +++ b/code/api/Pages/Index.cshtml.cs @@ -0,0 +1,8 @@ +namespace I2R.Storage.Api.Pages; + +public class Index : PageModel +{ + public ActionResult OnGet() { + return User.Identity.IsAuthenticated ? Redirect("/home") : Redirect("/login"); + } +}
\ No newline at end of file diff --git a/code/api/Pages/Login.cshtml b/code/api/Pages/Login.cshtml new file mode 100644 index 0000000..d68e96b --- /dev/null +++ b/code/api/Pages/Login.cshtml @@ -0,0 +1,28 @@ +@page +@model I2R.Storage.Api.Pages.Login +@{ + ViewData["Title"] = "Login"; +} + +@section Head { + <link rel="stylesheet" href="~/styles/page-specific/login.css" asp-append-version="true"> +} + +<form id="login-form"> + <fieldset> + <legend>Login</legend> + <div class="error"> + <div class="title"></div> + <div class="subtitle"></div> + </div> + <label for="username">Username</label> + <input type="text" required="required" id="username" name="username"> + <label for="password">Password</label> + <input type="password" required="required" id="password" name="password"> + <input type="submit" value="Login"> + </fieldset> +</form> + +@section Scripts { + <script src="~/scripts/page-specific/login.js" asp-append-version="true"></script> +}
\ No newline at end of file diff --git a/code/api/Pages/Login.cshtml.cs b/code/api/Pages/Login.cshtml.cs new file mode 100644 index 0000000..7d97ff1 --- /dev/null +++ b/code/api/Pages/Login.cshtml.cs @@ -0,0 +1,12 @@ +namespace I2R.Storage.Api.Pages; + +public class Login : PageModel +{ + public ActionResult OnGet() { + if (User.Identity.IsAuthenticated) { + return Redirect("/home"); + } + + return Page(); + } +}
\ No newline at end of file diff --git a/code/api/Pages/_Layout.cshtml b/code/api/Pages/_Layout.cshtml new file mode 100644 index 0000000..022e41e --- /dev/null +++ b/code/api/Pages/_Layout.cshtml @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <link rel="stylesheet" href="~/styles/base.css" asp-append-version="true"> + @await RenderSectionAsync("Head", false) + <title>@ViewData["Title"] - Storage</title> +</head> +<body> +@RenderBody() +<script src="~/scripts/base.js" asp-append-version="true"></script> +@await RenderSectionAsync("Scripts", false) +</body> +</html>
\ No newline at end of file diff --git a/code/api/Pages/_ViewImports.cshtml b/code/api/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..c4934c1 --- /dev/null +++ b/code/api/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using I2R.Storage.Api +@namespace I2R.Storage.Api.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
\ No newline at end of file diff --git a/code/api/Pages/_ViewStart.cshtml b/code/api/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..1af6e49 --- /dev/null +++ b/code/api/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +}
\ No newline at end of file diff --git a/code/api/Program.cs b/code/api/Program.cs index 1b0b1b5..e6281f7 100644 --- a/code/api/Program.cs +++ b/code/api/Program.cs @@ -1,20 +1,51 @@ +global using Microsoft.AspNetCore.Mvc; +global using IOL.Helpers; +global using I2R.Storage.Api.Services.Admin; +global using Microsoft.AspNetCore.Mvc.RazorPages; +global using Microsoft.EntityFrameworkCore; +global using I2R.Storage.Api.Database; +global using I2R.Storage.Api.Utilities; +global using I2R.Storage.Api.Database.Models; +global using I2R.Storage.Api.Resources; +global using I2R.Storage.Api.Enums; +global using Microsoft.Extensions.Localization; +global using I2R.Storage.Api.Statics; +global using Microsoft.AspNetCore.Authorization; +global using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Localization; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(o => { + o.Cookie.Name = "storage_session"; + o.Cookie.HttpOnly = true; + }); +builder.Services.AddAuthorization(o => { + o.AddPolicy("least_privileged", b => { b.RequireRole("least_privileged"); }); + o.AddPolicy("admin", b => { b.RequireRole("admin"); }); +}); +builder.Services.AddLocalization(); +builder.Services.AddRequestLocalization(o => { o.DefaultRequestCulture = new RequestCulture("en"); }); +builder.Services.AddScoped<UserService>(); +builder.Services.AddDbContext<AppDatabase>(o => { + o.UseNpgsql(builder.Configuration.GetAppDbConnectionString(), b => { + b.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + b.EnableRetryOnFailure(3); + }); + o.UseSnakeCaseNamingConvention(); +}); +builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddControllers(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - - +app.UseStaticFiles(); +app.UseStatusCodePages(); +app.UseRequestLocalization(); app.UseAuthorization(); - +app.UseAuthentication(); +app.MapRazorPages(); app.MapControllers(); - -app.Run(); +app.Run();
\ No newline at end of file diff --git a/code/api/Properties/launchSettings.json b/code/api/Properties/launchSettings.json index 778fa45..c5b268a 100644 --- a/code/api/Properties/launchSettings.json +++ b/code/api/Properties/launchSettings.json @@ -5,21 +5,11 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "launchUrl": "swagger", + "launchUrl": "", "applicationUrl": "http://localhost:5068", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7291;http://localhost:5068", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, + } } } diff --git a/code/api/Resources/SharedResources.cs b/code/api/Resources/SharedResources.cs new file mode 100644 index 0000000..36cd2a5 --- /dev/null +++ b/code/api/Resources/SharedResources.cs @@ -0,0 +1,6 @@ +namespace I2R.Storage.Api.Resources; + +public class SharedResources +{ + +}
\ No newline at end of file diff --git a/code/api/Resources/SharedResources.resx b/code/api/Resources/SharedResources.resx new file mode 100644 index 0000000..075a29b --- /dev/null +++ b/code/api/Resources/SharedResources.resx @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> + +<root> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:element name="root" msdata:IsDataSet="true"> + + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>1.3</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="That username is already taken" xml:space="preserve"> + <value>That username is already taken</value> + <comment>That username is already taken</comment> + </data> +</root>
\ No newline at end of file diff --git a/code/api/Services/Admin/UserService.cs b/code/api/Services/Admin/UserService.cs new file mode 100644 index 0000000..94a64ef --- /dev/null +++ b/code/api/Services/Admin/UserService.cs @@ -0,0 +1,53 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace I2R.Storage.Api.Services.Admin; + +public class UserService +{ + private readonly AppDatabase _database; + private readonly ILogger<UserService> _logger; + + public UserService(AppDatabase database, ILogger<UserService> logger) { + _database = database; + _logger = logger; + } + + public bool CanCreateAccount(string username) { + if (username.IsNullOrWhiteSpace()) { + return false; + } + + var normalisedUsername = username.Trim(); + return _database.Users.All(c => c.Username != normalisedUsername); + } + + public async Task LogInUserAsync(HttpContext httpContext, IEnumerable<Claim> claims) { + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var authenticationProperties = new AuthenticationProperties { + AllowRefresh = true, + IssuedUtc = DateTimeOffset.UtcNow, + }; + + await httpContext.SignInAsync(principal, authenticationProperties); + _logger.LogInformation("Logged in user {userId}", principal.FindFirstValue(AppClaims.USER_ID)); + } + + public async Task LogOutUserAsync(HttpContext httpContext, CancellationToken cancellationToken = default) { + await httpContext.SignOutAsync(); + _logger.LogInformation("Logged out user {userId}", httpContext.User.FindFirstValue(AppClaims.USER_ID)); + } + + public async Task MarkUserAsDeleted(Guid userId, Guid actorId) { + var user = _database.Users.FirstOrDefault(c => c.Id == userId); + if (user == default) { + _logger.LogInformation("Tried to delete unknown user {userId}", userId); + return; + } + + user.SetDeleted(actorId); + await _database.SaveChangesAsync(); + } +}
\ No newline at end of file diff --git a/code/api/Statics/AppClaims.cs b/code/api/Statics/AppClaims.cs new file mode 100644 index 0000000..f4f54e3 --- /dev/null +++ b/code/api/Statics/AppClaims.cs @@ -0,0 +1,8 @@ +namespace I2R.Storage.Api.Statics; + +public static class AppClaims +{ + public const string USER_ID = "id"; + public const string USERNAME = "username"; + public const string USER_ROLE = "role"; +}
\ No newline at end of file diff --git a/code/api/Statics/AppEnvVariables.cs b/code/api/Statics/AppEnvVariables.cs new file mode 100644 index 0000000..c10914e --- /dev/null +++ b/code/api/Statics/AppEnvVariables.cs @@ -0,0 +1,6 @@ +namespace I2R.Storage.Api.Statics; + +public class AppEnvVariables +{ + public const string APPDB_CONNECTION_STRING = "APPDB_CONNECTION_STRING"; +}
\ No newline at end of file diff --git a/code/api/Utilities/AppDateTime.cs b/code/api/Utilities/AppDateTime.cs new file mode 100644 index 0000000..cd54a17 --- /dev/null +++ b/code/api/Utilities/AppDateTime.cs @@ -0,0 +1,16 @@ +namespace I2R.Storage.Api.Utilities; + +public class AppDateTime +{ + private static DateTime? dateTime; + + public static DateTime UtcNow => dateTime ?? DateTime.UtcNow; + + public static void Set(DateTime setDateTime) { + dateTime = setDateTime; + } + + public static void Reset() { + dateTime = null; + } +}
\ No newline at end of file diff --git a/code/api/Utilities/ConfigurationHelpers.cs b/code/api/Utilities/ConfigurationHelpers.cs new file mode 100644 index 0000000..dece783 --- /dev/null +++ b/code/api/Utilities/ConfigurationHelpers.cs @@ -0,0 +1,8 @@ +namespace I2R.Storage.Api.Utilities; + +public static class ConfigurationHelpers +{ + public static string GetAppDbConnectionString(this IConfiguration configuration) { + return configuration.GetValue<string>(AppEnvVariables.APPDB_CONNECTION_STRING); + } +}
\ No newline at end of file diff --git a/code/api/wwwroot/scripts/base.js b/code/api/wwwroot/scripts/base.js new file mode 100644 index 0000000..3a55994 --- /dev/null +++ b/code/api/wwwroot/scripts/base.js @@ -0,0 +1,9 @@ +const session = { + _storageKey: "session_data", + get() { + return sessionStorage.getItem(session._storageKey); + }, + set(data) { + sessionStorage.setItem(session._storageKey, JSON.stringify(data)) + } +}
\ No newline at end of file diff --git a/code/api/wwwroot/scripts/page-specific/login.js b/code/api/wwwroot/scripts/page-specific/login.js new file mode 100644 index 0000000..ac43e5d --- /dev/null +++ b/code/api/wwwroot/scripts/page-specific/login.js @@ -0,0 +1,48 @@ +const form = document.getElementById("login-form"); + + +function get_login_payload() { + return { + username: document.querySelector("input[name=username]").value, + password: document.querySelector("input[name=password]").value + } +} + +function show_error(title, subtitle) { + if (!title || !subtitle) { + console.error("parameter title or subtitle is empty"); + return; + } + const errorEl = form.querySelector(".error"); + if (!errorEl) { + console.error("#" + form.id + " does not have an .error element") + return; + } + errorEl.querySelector(".title").innerText = title; + errorEl.querySelector(".subtitle").innerHTML = subtitle; +} + +async function submit_login_form(event) { + event.preventDefault(); + event.stopPropagation(); + const response = await fetch("/account/login", { + method: "post", + body: JSON.stringify(get_login_payload()), + headers: { + "Content-Type": "application/json;charset=utf-8" + } + }) + + if (response.ok) { + const sessionResponse = await fetch("/session"); + session.set(await sessionResponse.json()); + location.href = "/home"; + return; + } +} + +function init() { + form.addEventListener("submit", submit_login_form) +} + +document.addEventListener("DOMContentLoaded", init)
\ No newline at end of file diff --git a/code/api/wwwroot/styles/base.css b/code/api/wwwroot/styles/base.css new file mode 100644 index 0000000..e5d9e42 --- /dev/null +++ b/code/api/wwwroot/styles/base.css @@ -0,0 +1,354 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} + +/* System Fonts as used by GitHub */ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +}
\ No newline at end of file diff --git a/code/api/wwwroot/styles/page-specific/login.css b/code/api/wwwroot/styles/page-specific/login.css new file mode 100644 index 0000000..cefc766 --- /dev/null +++ b/code/api/wwwroot/styles/page-specific/login.css @@ -0,0 +1,6 @@ +#login-form fieldset { + max-width: 23vw; + display: flex; + flex-direction: column; + gap: 5px; +}
\ No newline at end of file |
