This commit is contained in:
femsci 2023-09-30 23:17:45 +02:00
parent 19aeb5aba9
commit 66e32907fe
Signed by: femsci
GPG key ID: 08F7911F0E650C67
19 changed files with 317 additions and 193 deletions

View file

@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{59D8D1A8-D9F
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Core", "src\Interlinked.Core\Interlinked.Core.csproj", "{21F768D4-54BA-4B55-8A03-399F697394FD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Core", "src\Interlinked.Core\Interlinked.Core.csproj", "{21F768D4-54BA-4B55-8A03-399F697394FD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Locator", "src\Interlinked.Locator\Interlinked.Locator.csproj", "{069E0BCC-F019-4EDB-83F2-698EE5C33825}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Shared", "src\Interlinked.Shared\Interlinked.Shared.csproj", "{0EF4026C-3675-4BBB-B352-26FBB7C96FA3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Shared", "src\Interlinked.Shared\Interlinked.Shared.csproj", "{0EF4026C-3675-4BBB-B352-26FBB7C96FA3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.User", "src\Interlinked.User\Interlinked.User.csproj", "{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.User", "src\Interlinked.User\Interlinked.User.csproj", "{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A}"
@ -49,7 +47,6 @@ Global
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{21F768D4-54BA-4B55-8A03-399F697394FD} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F} {21F768D4-54BA-4B55-8A03-399F697394FD} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
{069E0BCC-F019-4EDB-83F2-698EE5C33825} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
{0EF4026C-3675-4BBB-B352-26FBB7C96FA3} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F} {0EF4026C-3675-4BBB-B352-26FBB7C96FA3} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
{5B94DC1F-170E-458C-BC5B-513C5ADB3F0A} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F} {5B94DC1F-170E-458C-BC5B-513C5ADB3F0A} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F}
{A8E7D5AA-DB9D-44CC-B2A0-767C9D568B96} = {3A0C257D-5E1E-4FCE-A55D-8990F6F2F6B0} {A8E7D5AA-DB9D-44CC-B2A0-767C9D568B96} = {3A0C257D-5E1E-4FCE-A55D-8990F6F2F6B0}

View file

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Interlinked.Locator.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

View file

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>

View file

@ -1,25 +0,0 @@
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View file

@ -1,41 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:57682",
"sslPort": 44326
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5202",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7210;http://localhost:5202",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -1,12 +0,0 @@
namespace Interlinked.Locator;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View file

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View file

@ -1,5 +0,0 @@
namespace Interlinked.Shared;
public class Class1
{
}

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Security.Cryptography;
namespace Interlinked.Shared.Model;
public class AuthSession
{
[Key]
public required byte[] Key { get; set; }
public required byte[] Secret { get; set; }
public long UserId { get; set; }
public IPAddress LastAddress { get; set; } = default!;
public UserModel User { get; set; } = default!;
public override string ToString()
{
//TODO: Use key
using var sha = SHA256.Create();
var hash = sha.ComputeHash(Secret);
return $"{Convert.ToBase64String(Key)}.{Convert.ToBase64String(hash)}";
//Validate
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace Interlinked.Shared.Model;
public class Project
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public long OwnerId { get; set; }
public virtual UserModel Owner { get; set; } = default!;
public ICollection<Tag> Tags { get; set; } = default!;
}

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Interlinked.Shared.Model;
public class Tag
{
[Key]
public required string Identifier { get; set; }
public required string Name { get; set; }
public virtual ICollection<UserModel> Users { get; set; } = default!;
public virtual ICollection<Project> Projects { get; set; } = default!;
}

View file

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Interlinked.Shared.Model;
public class UserModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[MaxLength(32)]
public required string Username { get; set; }
[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; }
public static byte[] HashPassword(string password, byte[] salt)
{
return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 1000, HashAlgorithmName.SHA256, 64);
}
public byte[] HashPassword(string password) => HashPassword(password, Salt);
public bool CheckHash(string password) => HashPassword(password).SequenceEqual(Hash);
[JsonIgnore]
public virtual ICollection<AuthSession> AuthSessions { get; set; } = default!;
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Interlinked.Shared.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Interlinked.User.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
public AuthController(StoreContext db)
{
_db = db;
}
private readonly StoreContext _db;
[AllowAnonymous]
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] string email, [FromBody] string username, [FromBody] string? name, [FromBody] string password)
{
email = email.ToLower().Trim();
username = username.Trim();
if (_db.Users.AsNoTracking().Any(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase)))
{
return Conflict("Username taken.");
}
//VALID???
if (_db.Users.AsNoTracking().Any(u => u.Email == email))
{
return Conflict("Email taken.");
}
byte[] salt = new byte[16];
RandomNumberGenerator.Fill(salt);
byte[] hash = UserModel.HashPassword(password, salt.ToArray());
UserModel u = new()
{
Email = email,
Username = username,
Salt = salt,
Name = name,
Hash = hash
};
//throw new NotImplementedException();
await _db.Users.AddAsync(u);
await _db.SaveChangesAsync();
return Ok(u);
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] string email, [FromBody] string password)
{
email = email.ToLower().Trim();
UserModel? u = _db.Users.AsNoTracking().FirstOrDefault(u => email.Equals(u.Email));
if (u is null || !u.CheckHash(password))
{
return Unauthorized();
}
//Create auth session
throw new NotImplementedException();
}
[Authorize]
[HttpGet("logout")]
public async Task<IActionResult> Logout()
{
throw new NotImplementedException();
}
}

View file

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Interlinked.User.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

View file

@ -7,8 +7,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" 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="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
</ItemGroup>
</Project> </Project>

View file

@ -1,23 +1,29 @@
using Interlinked.Shared.Model;
using Interlinked.User;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddDbContext<StoreContext>();
builder.Services.AddControllers(); builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddMemoryCache();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddDefaultIdentity<UserModel>(o => o.SignIn.RequireConfirmedAccount = true)
builder.Services.AddSwaggerGen(); .AddEntityFrameworkStores<StoreContext>();
builder.Services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/auth/login";
options.AccessDeniedPath = "/auth/account/uwuless";
options.SlidingExpiration = true;
});
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Interlinked.Shared.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace Interlinked.User.Services;
public class AuthManager
{
public const string AUTH_COOKIE_NAME = "bOiKissEr_ᓚᘏᗢ_UwU";
public AuthManager(StoreContext db, HttpContext http, IMemoryCache cache)
{
_db = db;
_http = http;
_cache = cache;
}
private readonly IMemoryCache _cache;
private readonly StoreContext _db;
private readonly HttpContext _http;
public async Task<AuthSession> CreateSession(UserModel user)
{
byte[] secret = new byte[16], key = new byte[16];
RandomNumberGenerator.Fill(secret);
RandomNumberGenerator.Fill(key);
AuthSession sess = new()
{
UserId = user.Id,
Secret = secret,
Key = key
};
await _db.AuthSessions.AddAsync(sess);
await _db.SaveChangesAsync();
_http.Response.Cookies.Append(AUTH_COOKIE_NAME, sess.ToString());
return sess;
}
public async Task RevokeSession(AuthSession sess)
{
_db.AuthSessions.Remove(sess);
await _db.SaveChangesAsync();
}
public async Task<AuthSession?> RetrieveCookie()
{
if (!_http.Request.Cookies.TryGetValue(AUTH_COOKIE_NAME, out string? val) || val is null)
{
return null;
}
int idx = val.IndexOf('.');
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));
if (sess is null)
{
return null;
}
using var sha = SHA256.Create();
var trueHash = sha.ComputeHash(sess.Secret);
return trueHash.SequenceEqual(hash) ? sess : null;
}
}

View file

@ -0,0 +1,19 @@
using Interlinked.Shared.Model;
using Microsoft.EntityFrameworkCore;
namespace Interlinked.User;
public class StoreContext : DbContext
{
public DbSet<UserModel> Users => Set<UserModel>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<AuthSession> AuthSessions => Set<AuthSession>();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<UserModel>().HasMany(u => u.AuthSessions).WithOne(a => a.User);
mb.Entity<UserModel>().HasMany(u => u.Interests).WithMany(t => t.Users);
mb.Entity<Project>().HasMany(p => p.Tags).WithMany(t => t.Projects);
}
}