Auth DB schema migration, ef design, auth
This commit is contained in:
parent
a0d5df1c52
commit
5e4bfaa95e
22 changed files with 1106 additions and 38 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
||||||
**/obj
|
**/obj
|
||||||
**/bin
|
**/bin
|
||||||
|
**.db
|
||||||
|
**.db-wal
|
||||||
|
**.db-shm
|
||||||
|
|
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Core Server",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/src/Interlinked.User/bin/Debug/net7.0/Interlinked.User.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Interlinked.User",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"serverReadyAction": {
|
||||||
|
"action": "openExternally",
|
||||||
|
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
},
|
||||||
|
"pipeTransport": {
|
||||||
|
"pipeCwd": "${workspaceFolder}",
|
||||||
|
"pipeProgram": "/usr/bin/bash",
|
||||||
|
"pipeArgs": ["-c"],
|
||||||
|
"debuggerPath": "/usr/bin/netcoredbg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/Interlinked.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/Interlinked.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/Interlinked.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -14,4 +14,8 @@
|
||||||
<PackageReference Include="Syncfusion.Blazor.Maps" Version="23.1.38" />
|
<PackageReference Include="Syncfusion.Blazor.Maps" Version="23.1.38" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
77
src/Interlinked.Core/Pages/Login.razor
Normal file
77
src/Interlinked.Core/Pages/Login.razor
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
@page "/login"
|
||||||
|
@using System.ComponentModel.DataAnnotations;
|
||||||
|
@using System.Net
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
|
||||||
|
<PageTitle>Login</PageTitle>
|
||||||
|
|
||||||
|
<h1>Log in</h1>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@Model" OnValidSubmit="@Submit">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-mail</label>
|
||||||
|
<InputText @bind-Value="Model!.Email" id="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwd">Password</label>
|
||||||
|
<InputText type="password" @bind-Value="Model!.Password" id="pwd" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ValidationMsg))
|
||||||
|
{
|
||||||
|
<p class="text-danger">@ValidationMsg</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public record LoginModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
[Required]
|
||||||
|
public string Password { get; set; } = default!;
|
||||||
|
};
|
||||||
|
public LoginModel Model { get; set; } = new();
|
||||||
|
public string ValidationMsg = string.Empty;
|
||||||
|
|
||||||
|
protected override void OnInitialized() => Model ??= new();
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/login", Model);
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Successful login <3";
|
||||||
|
var user = await resp.Content.ReadFromJsonAsync<UserModel>();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
ValidationMsg = "Unknown authentication error";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usr.User = user;
|
||||||
|
this.StateHasChanged();
|
||||||
|
nav.NavigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Invalid username or password.";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Authorization error...";
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
17
src/Interlinked.Core/Pages/Logout.razor
Normal file
17
src/Interlinked.Core/Pages/Logout.razor
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@page "/logout"
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@using System.Net
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var resp = await req.GetAsync("/api/auth/logout");
|
||||||
|
if (resp.StatusCode == HttpStatusCode.NoContent)
|
||||||
|
{
|
||||||
|
usr.User = null!;
|
||||||
|
}
|
||||||
|
nav.NavigateTo("/login");
|
||||||
|
}
|
||||||
|
}
|
88
src/Interlinked.Core/Pages/Register.razor
Normal file
88
src/Interlinked.Core/Pages/Register.razor
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
@page "/register"
|
||||||
|
@using System.ComponentModel.DataAnnotations;
|
||||||
|
@using System.Net
|
||||||
|
@inject HttpClient req
|
||||||
|
@inject UserManager usr
|
||||||
|
@inject NavigationManager nav
|
||||||
|
|
||||||
|
<PageTitle>Register</PageTitle>
|
||||||
|
|
||||||
|
<h1>Register</h1>
|
||||||
|
|
||||||
|
<EditForm class="form" Model="@Model" OnValidSubmit="@Submit">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<InputText @bind-Value="Model!.Username" id="username" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full name</label>
|
||||||
|
<InputText @bind-Value="Model!.Name" id="name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-mail</label>
|
||||||
|
<InputText @bind-Value="Model!.Email" id="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pwd">Password</label>
|
||||||
|
<InputText type="password" @bind-Value="Model!.Password" id="pwd" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ValidationMsg))
|
||||||
|
{
|
||||||
|
<p class="text-danger">@ValidationMsg</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public record LoginModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Username { get; set; } = default!;
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; } = default!;
|
||||||
|
[Required]
|
||||||
|
public string Password { get; set; } = default!;
|
||||||
|
};
|
||||||
|
public LoginModel Model { get; set; } = new();
|
||||||
|
public string ValidationMsg = string.Empty;
|
||||||
|
|
||||||
|
protected override void OnInitialized() => Model ??= new();
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
var resp = await req.PostAsJsonAsync("/api/auth/login", Model);
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Successful login <3"; var user = await resp.Content.ReadFromJsonAsync<UserModel>();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
ValidationMsg = "Unknown authentication error";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usr.User = user;
|
||||||
|
this.StateHasChanged();
|
||||||
|
nav.NavigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Invalid username or password.";
|
||||||
|
this.StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = new();
|
||||||
|
ValidationMsg = "Authorization error...";
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ using Interlinked.Core;
|
||||||
using Syncfusion.Blazor;
|
using Syncfusion.Blazor;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Interlinked.Core.Services;
|
||||||
|
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
|
@ -21,6 +22,13 @@ internal class Program
|
||||||
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
|
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
|
||||||
new[] { "application/octet-stream" });
|
new[] { "application/octet-stream" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<UserManager, UserManager>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.Services.GetRequiredService<UserManager>().InitAsync();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
src/Interlinked.Core/Services/UserManager.cs
Normal file
32
src/Interlinked.Core/Services/UserManager.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
|
||||||
|
namespace Interlinked.Core.Services;
|
||||||
|
|
||||||
|
public class UserManager
|
||||||
|
{
|
||||||
|
public UserManager(IServiceProvider serv)
|
||||||
|
{
|
||||||
|
_serv = serv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IServiceProvider _serv;
|
||||||
|
|
||||||
|
public async Task InitAsync()
|
||||||
|
{
|
||||||
|
using var scope = _serv.CreateAsyncScope();
|
||||||
|
var resp = await scope.ServiceProvider.GetRequiredService<HttpClient>().GetAsync("/api/auth/userdata");
|
||||||
|
if (resp.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
User = (await resp.Content.ReadFromJsonAsync<UserModel>())!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserModel User { get; set; } = default!;
|
||||||
|
public bool IsAuthorized => User != null;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@inject UserManager usr
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
@ -7,7 +8,15 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="top-row px-4">
|
<div class="top-row px-4">
|
||||||
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
|
@if (!usr.IsAuthorized)
|
||||||
|
{
|
||||||
|
<a href="/login">Log in</a>
|
||||||
|
<a href="/register">Register</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/logout">Log out (@usr.User.Username)</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="content px-4">
|
<article class="content px-4">
|
||||||
|
|
|
@ -8,3 +8,5 @@
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Interlinked.Core
|
@using Interlinked.Core
|
||||||
@using Interlinked.Core.Shared
|
@using Interlinked.Core.Shared
|
||||||
|
@using Interlinked.Core.Services
|
||||||
|
@using Interlinked.Shared.Model
|
||||||
|
|
|
@ -10,7 +10,7 @@ public class AuthSession
|
||||||
public required byte[] Key { get; set; }
|
public required byte[] Key { get; set; }
|
||||||
public required byte[] Secret { get; set; }
|
public required byte[] Secret { get; set; }
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
public IPAddress LastAddress { get; set; } = default!;
|
public IPAddress? LastAddress { get; set; } = default!;
|
||||||
public UserModel User { get; set; } = default!;
|
public UserModel User { get; set; } = default!;
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Interlinked.Shared.Model;
|
using Interlinked.Shared.Model;
|
||||||
|
using Interlinked.User.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -10,20 +11,26 @@ namespace Interlinked.User.Controllers;
|
||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
public AuthController(StoreContext db)
|
public AuthController(StoreContext db, AuthManager auth)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_auth = auth;
|
||||||
}
|
}
|
||||||
private readonly StoreContext _db;
|
private readonly StoreContext _db;
|
||||||
|
private readonly AuthManager _auth;
|
||||||
|
|
||||||
|
public record RegisterRequest(string email, string username, string? name, string password);
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<IActionResult> Register([FromBody] string email, [FromBody] string username, [FromBody] string? name, [FromBody] string password)
|
public async Task<IActionResult> Register([FromBody] RegisterRequest req)
|
||||||
{
|
{
|
||||||
email = email.ToLower().Trim();
|
var now = DateTime.UtcNow;
|
||||||
username = username.Trim();
|
|
||||||
|
|
||||||
if (_db.Users.AsNoTracking().Any(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase)))
|
string email = req.email.ToLower().Trim();
|
||||||
|
string username = req.username.Trim();
|
||||||
|
|
||||||
|
if (_db.Users.AsNoTracking().Any(u => u.Username == username))
|
||||||
{
|
{
|
||||||
return Conflict("Username taken.");
|
return Conflict("Username taken.");
|
||||||
}
|
}
|
||||||
|
@ -35,46 +42,70 @@ public class AuthController : ControllerBase
|
||||||
|
|
||||||
byte[] salt = new byte[16];
|
byte[] salt = new byte[16];
|
||||||
RandomNumberGenerator.Fill(salt);
|
RandomNumberGenerator.Fill(salt);
|
||||||
byte[] hash = UserModel.HashPassword(password, salt.ToArray());
|
byte[] hash = UserModel.HashPassword(req.password, salt.ToArray());
|
||||||
|
|
||||||
UserModel u = new()
|
UserModel u = new()
|
||||||
{
|
{
|
||||||
Email = email,
|
Email = email,
|
||||||
Username = username,
|
Username = username,
|
||||||
Salt = salt,
|
Salt = salt,
|
||||||
Name = name,
|
Name = req.name,
|
||||||
Hash = hash
|
Hash = hash,
|
||||||
|
CreatedTimestamp = now
|
||||||
};
|
};
|
||||||
|
|
||||||
//throw new NotImplementedException();
|
//throw new NotImplementedException();
|
||||||
await _db.Users.AddAsync(u);
|
await _db.Users.AddAsync(u);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _auth.CreateSession(u);
|
||||||
|
|
||||||
return Ok(u);
|
return Ok(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record LoginRequest(string email, string password);
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<IActionResult> Login([FromBody] string email, [FromBody] string password)
|
public async Task<IActionResult> Login([FromBody] LoginRequest req)
|
||||||
{
|
{
|
||||||
email = email.ToLower().Trim();
|
if ((await _auth.RetrieveCookie()) is not null)
|
||||||
UserModel? u = _db.Users.AsNoTracking().FirstOrDefault(u => email.Equals(u.Email));
|
{
|
||||||
|
return this.Conflict("Already authenticated...");
|
||||||
|
}
|
||||||
|
string email = req.email.ToLower().Trim();
|
||||||
|
UserModel? u = _db.Users.AsNoTracking().FirstOrDefault(u => email == u.Email);
|
||||||
|
|
||||||
if (u is null || !u.CheckHash(password))
|
if (u is null || !u.CheckHash(req.password))
|
||||||
{
|
{
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _auth.CreateSession(u);
|
||||||
//Create auth session
|
return Ok(u);
|
||||||
throw new NotImplementedException();
|
}
|
||||||
|
|
||||||
|
[HttpGet("userdata")]
|
||||||
|
public async Task<IActionResult> GetUserData()
|
||||||
|
{
|
||||||
|
if (await _auth.RetrieveCookie() is not AuthSession sess)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(await _db.Users.AsNoTracking().SingleAsync(s => s.Id == sess.UserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("logout")]
|
[HttpGet("logout")]
|
||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (await _auth.RetrieveCookie() is AuthSession sess)
|
||||||
|
{
|
||||||
|
await _auth.RevokeSession(sess);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
18
src/Interlinked.User/Controllers/SystemController.cs
Normal file
18
src/Interlinked.User/Controllers/SystemController.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/")]
|
||||||
|
public class SystemController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("version")]
|
||||||
|
public async Task<IActionResult> Version()
|
||||||
|
{
|
||||||
|
return Ok("1.0E");
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,16 +7,23 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
||||||
|
<ProjectReference Include="..\Interlinked.Core\Interlinked.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
222
src/Interlinked.User/Migrations/20231001074047_Initial schema.Designer.cs
generated
Normal file
222
src/Interlinked.User/Migrations/20231001074047_Initial schema.Designer.cs
generated
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StoreContext))]
|
||||||
|
[Migration("20231001074047_Initial schema")]
|
||||||
|
partial class Initialschema
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Key")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("LastAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Secret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Identifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Identifier");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CountryCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTimestamp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DayOfBirth")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Salt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ProjectsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TagsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ProjectsId", "TagsIdentifier");
|
||||||
|
|
||||||
|
b.HasIndex("TagsIdentifier");
|
||||||
|
|
||||||
|
b.ToTable("ProjectTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InterestsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InterestsIdentifier", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("TagUserModel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "User")
|
||||||
|
.WithMany("AuthSessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Project", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InterestsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthSessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
src/Interlinked.User/Migrations/20231001074047_Initial schema.cs
Normal file
180
src/Interlinked.User/Migrations/20231001074047_Initial schema.cs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initialschema : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Identifier = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tags", x => x.Identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Username = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false, collation: "NOCASE"),
|
||||||
|
Email = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 96, nullable: true),
|
||||||
|
Salt = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
Hash = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
City = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CountryCode = table.Column<string>(type: "TEXT", maxLength: 2, nullable: false),
|
||||||
|
Gender = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
DayOfBirth = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
CreatedTimestamp = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AuthSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
Secret = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||||
|
UserId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
LastAddress = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AuthSessions", x => x.Key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AuthSessions_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Projects",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Title = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
OwnerId = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Projects_Users_OwnerId",
|
||||||
|
column: x => x.OwnerId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TagUserModel",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
InterestsIdentifier = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
UsersId = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TagUserModel", x => new { x.InterestsIdentifier, x.UsersId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TagUserModel_Tags_InterestsIdentifier",
|
||||||
|
column: x => x.InterestsIdentifier,
|
||||||
|
principalTable: "Tags",
|
||||||
|
principalColumn: "Identifier",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TagUserModel_Users_UsersId",
|
||||||
|
column: x => x.UsersId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ProjectTag",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ProjectsId = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
TagsIdentifier = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ProjectTag", x => new { x.ProjectsId, x.TagsIdentifier });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectTag_Projects_ProjectsId",
|
||||||
|
column: x => x.ProjectsId,
|
||||||
|
principalTable: "Projects",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectTag_Tags_TagsIdentifier",
|
||||||
|
column: x => x.TagsIdentifier,
|
||||||
|
principalTable: "Tags",
|
||||||
|
principalColumn: "Identifier",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuthSessions_UserId",
|
||||||
|
table: "AuthSessions",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Projects_OwnerId",
|
||||||
|
table: "Projects",
|
||||||
|
column: "OwnerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ProjectTag_TagsIdentifier",
|
||||||
|
table: "ProjectTag",
|
||||||
|
column: "TagsIdentifier");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TagUserModel_UsersId",
|
||||||
|
table: "TagUserModel",
|
||||||
|
column: "UsersId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AuthSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ProjectTag");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TagUserModel");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
219
src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs
Normal file
219
src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Interlinked.User;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Interlinked.User.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(StoreContext))]
|
||||||
|
partial class StoreContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Key")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("LastAddress")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Secret")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AuthSessions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Projects");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Identifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Identifier");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CountryCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTimestamp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DayOfBirth")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Gender")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Salt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.UseCollation("NOCASE");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ProjectsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("TagsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ProjectsId", "TagsIdentifier");
|
||||||
|
|
||||||
|
b.HasIndex("TagsIdentifier");
|
||||||
|
|
||||||
|
b.ToTable("ProjectTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InterestsIdentifier")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("InterestsIdentifier", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("TagUserModel");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.AuthSession", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "User")
|
||||||
|
.WithMany("AuthSessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ProjectTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Project", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TagUserModel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Interlinked.Shared.Model.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InterestsIdentifier")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Interlinked.Shared.Model.UserModel", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthSessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Interlinked.User/Models/UserDomain.cs
Normal file
31
src/Interlinked.User/Models/UserDomain.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Interlinked.Shared.Model;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Interlinked.User.Models;
|
||||||
|
|
||||||
|
public class UserDomain : IdentityUser<long>
|
||||||
|
{
|
||||||
|
[EmailAddress]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
[MaxLength(96)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Salt { get; set; } = default!;
|
||||||
|
[JsonIgnore]
|
||||||
|
public byte[] Hash { get; set; } = default!;
|
||||||
|
public string? City { get; set; }
|
||||||
|
[StringLength(2)]
|
||||||
|
public string CountryCode { get; set; } = "XX";
|
||||||
|
public string? Gender { get; set; } = "cat";
|
||||||
|
public DateTime? DayOfBirth { get; set; }
|
||||||
|
public ICollection<Tag> Interests { get; set; } = default!;
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public DateTime CreatedTimestamp { get; set; }
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
using Interlinked.Shared.Model;
|
using Interlinked.Shared.Model;
|
||||||
using Interlinked.User;
|
using Interlinked.User;
|
||||||
|
using Interlinked.User.Models;
|
||||||
using Interlinked.User.Services;
|
using Interlinked.User.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddDbContext<StoreContext>();
|
builder.Services.AddDbContext<StoreContext>();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddDefaultIdentity<UserModel>(o => o.SignIn.RequireConfirmedAccount = true)
|
builder.Services.AddDefaultIdentity<UserDomain>(o => o.SignIn.RequireConfirmedAccount = true)
|
||||||
.AddEntityFrameworkStores<StoreContext>();
|
.AddEntityFrameworkStores<StoreContext>();
|
||||||
builder.Services.ConfigureApplicationCookie(options =>
|
builder.Services.ConfigureApplicationCookie(options =>
|
||||||
{
|
{
|
||||||
|
@ -20,14 +22,34 @@ builder.Services.ConfigureApplicationCookie(options =>
|
||||||
options.AccessDeniedPath = "/auth/account/uwuless";
|
options.AccessDeniedPath = "/auth/account/uwuless";
|
||||||
options.SlidingExpiration = true;
|
options.SlidingExpiration = true;
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddScoped<AuthManager, AuthManager>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<StoreContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseWebAssemblyDebugging();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseBlazorFrameworkFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapFallbackToFile("index.html");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
@ -11,8 +11,8 @@ namespace Interlinked.User.Services;
|
||||||
|
|
||||||
public class AuthManager
|
public class AuthManager
|
||||||
{
|
{
|
||||||
public const string AUTH_COOKIE_NAME = "bOiKissEr_ᓚᘏᗢ_UwU";
|
public const string AUTH_COOKIE_NAME = "bOiKissEr_UwU";
|
||||||
public AuthManager(StoreContext db, HttpContext http, IMemoryCache cache)
|
public AuthManager(StoreContext db, IHttpContextAccessor http, IMemoryCache cache)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_http = http;
|
_http = http;
|
||||||
|
@ -22,7 +22,7 @@ public class AuthManager
|
||||||
|
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly StoreContext _db;
|
private readonly StoreContext _db;
|
||||||
private readonly HttpContext _http;
|
private readonly IHttpContextAccessor _http;
|
||||||
|
|
||||||
public async Task<AuthSession> CreateSession(UserModel user)
|
public async Task<AuthSession> CreateSession(UserModel user)
|
||||||
{
|
{
|
||||||
|
@ -33,13 +33,14 @@ public class AuthManager
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Secret = secret,
|
Secret = secret,
|
||||||
Key = key
|
Key = key,
|
||||||
|
LastAddress = _http.HttpContext!.Connection.RemoteIpAddress
|
||||||
};
|
};
|
||||||
|
|
||||||
await _db.AuthSessions.AddAsync(sess);
|
await _db.AuthSessions.AddAsync(sess);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
_http.Response.Cookies.Append(AUTH_COOKIE_NAME, sess.ToString());
|
_http.HttpContext!.Response.Cookies.Append(AUTH_COOKIE_NAME, sess.ToString());
|
||||||
return sess;
|
return sess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,29 +48,39 @@ public class AuthManager
|
||||||
{
|
{
|
||||||
_db.AuthSessions.Remove(sess);
|
_db.AuthSessions.Remove(sess);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_http.HttpContext!.Response.Cookies.Delete(AUTH_COOKIE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthSession?> RetrieveCookie()
|
public async Task<AuthSession?> RetrieveCookie()
|
||||||
{
|
{
|
||||||
if (!_http.Request.Cookies.TryGetValue(AUTH_COOKIE_NAME, out string? val) || val is null)
|
if (!_http.HttpContext!.Request.Cookies.TryGetValue(AUTH_COOKIE_NAME, out string? val) || val is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int idx = val.IndexOf('.');
|
try
|
||||||
string keyStr = val[0..(idx - 1)], hashStr = val;
|
{
|
||||||
byte[] key = Convert.FromBase64String(keyStr), hash = Convert.FromBase64String(hashStr);
|
|
||||||
|
|
||||||
var sess = await _db.AuthSessions.SingleOrDefaultAsync(s => s.Key.SequenceEqual(key));
|
int idx = val.IndexOf('.');
|
||||||
|
string keyStr = val[0..idx], hashStr = val[(idx + 1)..];
|
||||||
|
byte[] key = Convert.FromBase64String(keyStr), hash = Convert.FromBase64String(hashStr);
|
||||||
|
|
||||||
if (sess is null)
|
var sess = await _db.AuthSessions.SingleOrDefaultAsync(s => s.Key.SequenceEqual(key));
|
||||||
|
|
||||||
|
if (sess is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var trueHash = sha.ComputeHash(sess.Secret);
|
||||||
|
|
||||||
|
return trueHash.SequenceEqual(hash) ? sess : null;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var sha = SHA256.Create();
|
|
||||||
var trueHash = sha.ComputeHash(sess.Secret);
|
|
||||||
|
|
||||||
return trueHash.SequenceEqual(hash) ? sess : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,17 @@ public class StoreContext : DbContext
|
||||||
public DbSet<Tag> Tags => Set<Tag>();
|
public DbSet<Tag> Tags => Set<Tag>();
|
||||||
public DbSet<AuthSession> AuthSessions => Set<AuthSession>();
|
public DbSet<AuthSession> AuthSessions => Set<AuthSession>();
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder o)
|
||||||
|
{
|
||||||
|
o.UseSqlite("Data Source=./store.db;Cache=Shared");
|
||||||
|
o.EnableDetailedErrors();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder mb)
|
protected override void OnModelCreating(ModelBuilder mb)
|
||||||
{
|
{
|
||||||
mb.Entity<UserModel>().HasMany(u => u.AuthSessions).WithOne(a => a.User);
|
mb.Entity<UserModel>().HasMany(u => u.AuthSessions).WithOne(a => a.User);
|
||||||
mb.Entity<UserModel>().HasMany(u => u.Interests).WithMany(t => t.Users);
|
mb.Entity<UserModel>().HasMany(u => u.Interests).WithMany(t => t.Users);
|
||||||
|
mb.Entity<UserModel>().Property(p => p.Username).UseCollation("NOCASE");
|
||||||
mb.Entity<Project>().HasMany(p => p.Tags).WithMany(t => t.Projects);
|
mb.Entity<Project>().HasMany(p => p.Tags).WithMany(t => t.Projects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue