diff --git a/Interlinked.sln b/Interlinked.sln index fac0aee..0f9d76e 100644 --- a/Interlinked.sln +++ b/Interlinked.sln @@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{59D8D1A8-D9F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interlinked.Core", "src\Interlinked.Core\Interlinked.Core.csproj", "{21F768D4-54BA-4B55-8A03-399F697394FD}" 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}" EndProject 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 GlobalSection(NestedProjects) = preSolution {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} {5B94DC1F-170E-458C-BC5B-513C5ADB3F0A} = {59D8D1A8-D9F0-464D-8654-D513FDC1A17F} {A8E7D5AA-DB9D-44CC-B2A0-767C9D568B96} = {3A0C257D-5E1E-4FCE-A55D-8990F6F2F6B0} diff --git a/src/Interlinked.Locator/Controllers/WeatherForecastController.cs b/src/Interlinked.Locator/Controllers/WeatherForecastController.cs deleted file mode 100644 index bbbe4ff..0000000 --- a/src/Interlinked.Locator/Controllers/WeatherForecastController.cs +++ /dev/null @@ -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 _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable 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(); - } -} diff --git a/src/Interlinked.Locator/Interlinked.Locator.csproj b/src/Interlinked.Locator/Interlinked.Locator.csproj deleted file mode 100644 index d2f5cb0..0000000 --- a/src/Interlinked.Locator/Interlinked.Locator.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net7.0 - enable - enable - - - - - - - - diff --git a/src/Interlinked.Locator/Program.cs b/src/Interlinked.Locator/Program.cs deleted file mode 100644 index 48863a6..0000000 --- a/src/Interlinked.Locator/Program.cs +++ /dev/null @@ -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(); diff --git a/src/Interlinked.Locator/Properties/launchSettings.json b/src/Interlinked.Locator/Properties/launchSettings.json deleted file mode 100644 index cb90574..0000000 --- a/src/Interlinked.Locator/Properties/launchSettings.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/src/Interlinked.Locator/WeatherForecast.cs b/src/Interlinked.Locator/WeatherForecast.cs deleted file mode 100644 index e64b312..0000000 --- a/src/Interlinked.Locator/WeatherForecast.cs +++ /dev/null @@ -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; } -} diff --git a/src/Interlinked.Locator/appsettings.Development.json b/src/Interlinked.Locator/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/Interlinked.Locator/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/Interlinked.Locator/appsettings.json b/src/Interlinked.Locator/appsettings.json deleted file mode 100644 index 10f68b8..0000000 --- a/src/Interlinked.Locator/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/Interlinked.Shared/Class1.cs b/src/Interlinked.Shared/Class1.cs deleted file mode 100644 index 4dca761..0000000 --- a/src/Interlinked.Shared/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Interlinked.Shared; -public class Class1 -{ - -} diff --git a/src/Interlinked.Shared/Model/AuthSession.cs b/src/Interlinked.Shared/Model/AuthSession.cs new file mode 100644 index 0000000..b594c0b --- /dev/null +++ b/src/Interlinked.Shared/Model/AuthSession.cs @@ -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 + } +} diff --git a/src/Interlinked.Shared/Model/Project.cs b/src/Interlinked.Shared/Model/Project.cs new file mode 100644 index 0000000..639fe43 --- /dev/null +++ b/src/Interlinked.Shared/Model/Project.cs @@ -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 Tags { get; set; } = default!; + +} diff --git a/src/Interlinked.Shared/Model/Tag.cs b/src/Interlinked.Shared/Model/Tag.cs new file mode 100644 index 0000000..6993833 --- /dev/null +++ b/src/Interlinked.Shared/Model/Tag.cs @@ -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 Users { get; set; } = default!; + public virtual ICollection Projects { get; set; } = default!; +} diff --git a/src/Interlinked.Shared/Model/User.cs b/src/Interlinked.Shared/Model/User.cs new file mode 100644 index 0000000..c4c3c11 --- /dev/null +++ b/src/Interlinked.Shared/Model/User.cs @@ -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 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 AuthSessions { get; set; } = default!; +} diff --git a/src/Interlinked.User/Controllers/AuthController.cs b/src/Interlinked.User/Controllers/AuthController.cs new file mode 100644 index 0000000..d80c41b --- /dev/null +++ b/src/Interlinked.User/Controllers/AuthController.cs @@ -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 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 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 Logout() + { + throw new NotImplementedException(); + } + + +} diff --git a/src/Interlinked.User/Controllers/WeatherForecastController.cs b/src/Interlinked.User/Controllers/WeatherForecastController.cs deleted file mode 100644 index 59805f6..0000000 --- a/src/Interlinked.User/Controllers/WeatherForecastController.cs +++ /dev/null @@ -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 _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable 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(); - } -} diff --git a/src/Interlinked.User/Interlinked.User.csproj b/src/Interlinked.User/Interlinked.User.csproj index d2f5cb0..24208bd 100644 --- a/src/Interlinked.User/Interlinked.User.csproj +++ b/src/Interlinked.User/Interlinked.User.csproj @@ -7,8 +7,16 @@ + + + + + + + + diff --git a/src/Interlinked.User/Program.cs b/src/Interlinked.User/Program.cs index 48863a6..a9ee9eb 100644 --- a/src/Interlinked.User/Program.cs +++ b/src/Interlinked.User/Program.cs @@ -1,23 +1,29 @@ +using Interlinked.Shared.Model; +using Interlinked.User; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. - +builder.Services.AddDbContext(); builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddMemoryCache(); +builder.Services.AddDefaultIdentity(o => o.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); +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(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/src/Interlinked.User/Services/AuthManager.cs b/src/Interlinked.User/Services/AuthManager.cs new file mode 100644 index 0000000..cca0f17 --- /dev/null +++ b/src/Interlinked.User/Services/AuthManager.cs @@ -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 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 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; + } +} diff --git a/src/Interlinked.User/StoreContext.cs b/src/Interlinked.User/StoreContext.cs new file mode 100644 index 0000000..9284ca6 --- /dev/null +++ b/src/Interlinked.User/StoreContext.cs @@ -0,0 +1,19 @@ +using Interlinked.Shared.Model; +using Microsoft.EntityFrameworkCore; + +namespace Interlinked.User; + +public class StoreContext : DbContext +{ + public DbSet Users => Set(); + public DbSet Projects => Set(); + public DbSet Tags => Set(); + public DbSet AuthSessions => Set(); + + protected override void OnModelCreating(ModelBuilder mb) + { + mb.Entity().HasMany(u => u.AuthSessions).WithOne(a => a.User); + mb.Entity().HasMany(u => u.Interests).WithMany(t => t.Users); + mb.Entity().HasMany(p => p.Tags).WithMany(t => t.Projects); + } +}