aboutsummaryrefslogtreecommitdiffstats
path: root/code
diff options
context:
space:
mode:
Diffstat (limited to 'code')
-rw-r--r--code/api/.idea/.idea.api.dir/.idea/.gitignore13
-rw-r--r--code/api/.idea/.idea.api.dir/.idea/encodings.xml4
-rw-r--r--code/api/.idea/.idea.api.dir/.idea/indexLayout.xml8
-rw-r--r--code/api/.idea/.idea.api.dir/.idea/vcs.xml12
-rw-r--r--code/api/Database/AppDatabase.cs36
-rw-r--r--code/api/Database/Models/File.cs13
-rw-r--r--code/api/Database/Models/Folder.cs10
-rw-r--r--code/api/Database/Models/Permission.cs11
-rw-r--r--code/api/Database/Models/PermissionGroup.cs8
-rw-r--r--code/api/Database/Models/User.cs23
-rw-r--r--code/api/Database/Models/_Base.cs34
-rw-r--r--code/api/Endpoints/Account/CreateEndpoint.cs52
-rw-r--r--code/api/Endpoints/Account/LoginEndpoint.cs38
-rw-r--r--code/api/Endpoints/Account/LogoutEndpoint.cs16
-rw-r--r--code/api/Endpoints/Account/create.http10
-rw-r--r--code/api/Endpoints/Base.cs36
-rw-r--r--code/api/Endpoints/_Root/SessionEndpoint.cs9
-rw-r--r--code/api/Enums/EUserRole.cs22
-rw-r--r--code/api/I2R.Storage.Api.csproj37
-rw-r--r--code/api/Migrations/20221221214429_InitialYay.Designer.cs418
-rw-r--r--code/api/Migrations/20221221214429_InitialYay.cs201
-rw-r--r--code/api/Migrations/AppDatabaseModelSnapshot.cs415
-rw-r--r--code/api/Pages/Home.cshtml12
-rw-r--r--code/api/Pages/Home.cshtml.cs10
-rw-r--r--code/api/Pages/Index.cshtml5
-rw-r--r--code/api/Pages/Index.cshtml.cs8
-rw-r--r--code/api/Pages/Login.cshtml28
-rw-r--r--code/api/Pages/Login.cshtml.cs12
-rw-r--r--code/api/Pages/_Layout.cshtml16
-rw-r--r--code/api/Pages/_ViewImports.cshtml3
-rw-r--r--code/api/Pages/_ViewStart.cshtml3
-rw-r--r--code/api/Program.cs55
-rw-r--r--code/api/Properties/launchSettings.json14
-rw-r--r--code/api/Resources/SharedResources.cs6
-rw-r--r--code/api/Resources/SharedResources.resx25
-rw-r--r--code/api/Services/Admin/UserService.cs53
-rw-r--r--code/api/Statics/AppClaims.cs8
-rw-r--r--code/api/Statics/AppEnvVariables.cs6
-rw-r--r--code/api/Utilities/AppDateTime.cs16
-rw-r--r--code/api/Utilities/ConfigurationHelpers.cs8
-rw-r--r--code/api/wwwroot/scripts/base.js9
-rw-r--r--code/api/wwwroot/scripts/page-specific/login.js48
-rw-r--r--code/api/wwwroot/styles/base.css354
-rw-r--r--code/api/wwwroot/styles/page-specific/login.css6
44 files changed, 2097 insertions, 34 deletions
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