From 6019620849c73f2a1a32b1a7aabb76fc6d3b99f1 Mon Sep 17 00:00:00 2001 From: Jackob_Punch Date: Sat, 30 Sep 2023 22:48:27 +0200 Subject: [PATCH 01/20] Add project page --- src/Interlinked.Core/Pages/Project.razor | 32 ++++++++++++++++++ src/Interlinked.Core/wwwroot/css/app.css | 10 ++++++ .../wwwroot/images/project.jpg | Bin 0 -> 32484 bytes 3 files changed, 42 insertions(+) create mode 100644 src/Interlinked.Core/Pages/Project.razor create mode 100644 src/Interlinked.Core/wwwroot/images/project.jpg diff --git a/src/Interlinked.Core/Pages/Project.razor b/src/Interlinked.Core/Pages/Project.razor new file mode 100644 index 0000000..ec10d9c --- /dev/null +++ b/src/Interlinked.Core/Pages/Project.razor @@ -0,0 +1,32 @@ +@page "/project" + +@code { + private string UserName = "John Doe"; + private string project = "Building a computer from a scrap"; + private string description = "In this project we're going to build computer from a scrap."; + private List tags = new List { "hardware", "embedded" }; @*choosen from the list*@ + private List persons = new List { "Jan Kowalski", "Kamil Ślimak" }; @*got from database*@ +} + +
+ Project +

@project

+

Tags

+
    + @foreach (var tag in tags) + { +
  • @tag
  • + } +
+

Description

+

@description

+

Owner

+

@UserName

+

Other persons working on project

+
    + @foreach (var person in persons) + { +
  • @person
  • + } +
+
\ No newline at end of file diff --git a/src/Interlinked.Core/wwwroot/css/app.css b/src/Interlinked.Core/wwwroot/css/app.css index a79bfa7..ccc0188 100644 --- a/src/Interlinked.Core/wwwroot/css/app.css +++ b/src/Interlinked.Core/wwwroot/css/app.css @@ -122,3 +122,13 @@ a, margin-top: 0; color: gray; } + +.background-img{ + max-width: 1500px; + max-height: 800px; +} + +.project-img { + max-width: 1500px; + max-height: 450px; +} diff --git a/src/Interlinked.Core/wwwroot/images/project.jpg b/src/Interlinked.Core/wwwroot/images/project.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f79f941f112f1c5afa564afa2040000d431248c8 GIT binary patch literal 32484 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 literal 0 HcmV?d00001 From eb8b32d5f2582154dcefda95fc634138b8a8939e Mon Sep 17 00:00:00 2001 From: femsci Date: Sat, 30 Sep 2023 23:17:45 +0200 Subject: [PATCH 02/20] backend --- Interlinked.sln | 3 - .../Controllers/WeatherForecastController.cs | 32 ------- .../Interlinked.Locator.csproj | 14 --- src/Interlinked.Locator/Program.cs | 25 ------ .../Properties/launchSettings.json | 41 --------- src/Interlinked.Locator/WeatherForecast.cs | 12 --- .../appsettings.Development.json | 8 -- src/Interlinked.Locator/appsettings.json | 9 -- src/Interlinked.Shared/Class1.cs | 5 -- src/Interlinked.Shared/Model/AuthSession.cs | 24 ++++++ src/Interlinked.Shared/Model/Project.cs | 22 +++++ src/Interlinked.Shared/Model/Tag.cs | 17 ++++ src/Interlinked.Shared/Model/User.cs | 48 +++++++++++ .../Controllers/AuthController.cs | 86 +++++++++++++++++++ .../Controllers/WeatherForecastController.cs | 32 ------- src/Interlinked.User/Interlinked.User.csproj | 8 ++ src/Interlinked.User/Program.cs | 30 ++++--- src/Interlinked.User/Services/AuthManager.cs | 75 ++++++++++++++++ src/Interlinked.User/StoreContext.cs | 19 ++++ 19 files changed, 317 insertions(+), 193 deletions(-) delete mode 100644 src/Interlinked.Locator/Controllers/WeatherForecastController.cs delete mode 100644 src/Interlinked.Locator/Interlinked.Locator.csproj delete mode 100644 src/Interlinked.Locator/Program.cs delete mode 100644 src/Interlinked.Locator/Properties/launchSettings.json delete mode 100644 src/Interlinked.Locator/WeatherForecast.cs delete mode 100644 src/Interlinked.Locator/appsettings.Development.json delete mode 100644 src/Interlinked.Locator/appsettings.json delete mode 100644 src/Interlinked.Shared/Class1.cs create mode 100644 src/Interlinked.Shared/Model/AuthSession.cs create mode 100644 src/Interlinked.Shared/Model/Project.cs create mode 100644 src/Interlinked.Shared/Model/Tag.cs create mode 100644 src/Interlinked.Shared/Model/User.cs create mode 100644 src/Interlinked.User/Controllers/AuthController.cs delete mode 100644 src/Interlinked.User/Controllers/WeatherForecastController.cs create mode 100644 src/Interlinked.User/Services/AuthManager.cs create mode 100644 src/Interlinked.User/StoreContext.cs 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); + } +} From e78c614763a005e0b196e80c553b31846f87bbaf Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 02:05:54 +0200 Subject: [PATCH 03/20] auth --- src/Interlinked.User/Controllers/AuthController.cs | 5 ----- src/Interlinked.User/Program.cs | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Interlinked.User/Controllers/AuthController.cs b/src/Interlinked.User/Controllers/AuthController.cs index d80c41b..d3de3ba 100644 --- a/src/Interlinked.User/Controllers/AuthController.cs +++ b/src/Interlinked.User/Controllers/AuthController.cs @@ -1,9 +1,4 @@ -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; diff --git a/src/Interlinked.User/Program.cs b/src/Interlinked.User/Program.cs index a9ee9eb..c01b729 100644 --- a/src/Interlinked.User/Program.cs +++ b/src/Interlinked.User/Program.cs @@ -1,5 +1,6 @@ using Interlinked.Shared.Model; using Interlinked.User; +using Interlinked.User.Services; var builder = WebApplication.CreateBuilder(args); @@ -13,6 +14,7 @@ builder.Services.ConfigureApplicationCookie(options => // Cookie settings options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); + options.Cookie.Name = AuthManager.AUTH_COOKIE_NAME; options.LoginPath = "/auth/login"; options.AccessDeniedPath = "/auth/account/uwuless"; From 13f85601f04b9a3fdcfcac01b27b0427d5812c7c Mon Sep 17 00:00:00 2001 From: Jackob_Punch Date: Sun, 1 Oct 2023 07:17:35 +0200 Subject: [PATCH 04/20] map page --- src/Interlinked.Core/Interlinked.Core.csproj | 1 + src/Interlinked.Core/Pages/Index.razor | 32 +++++++++++++++++--- src/Interlinked.Core/Program.cs | 2 ++ src/Interlinked.Core/wwwroot/index.html | 1 + 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Interlinked.Core/Interlinked.Core.csproj b/src/Interlinked.Core/Interlinked.Core.csproj index ea70444..cd83cbf 100644 --- a/src/Interlinked.Core/Interlinked.Core.csproj +++ b/src/Interlinked.Core/Interlinked.Core.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Interlinked.Core/Pages/Index.razor b/src/Interlinked.Core/Pages/Index.razor index 6085c4a..c5d6d91 100644 --- a/src/Interlinked.Core/Pages/Index.razor +++ b/src/Interlinked.Core/Pages/Index.razor @@ -1,9 +1,33 @@ @page "/" -Index +@using Syncfusion.Blazor.Maps -

Hello, world!

+@using Syncfusion.Blazor.Maps -Welcome to your new app. + + + + + + + @* Add marker *@ + + + + + + + - +@code{ + public class City + { + public double Latitude { get; set; } + public double Longitude { get; set; } + public string Name { get; set; } + } + private List Cities = new List { + new City { Latitude = 34.060620, Longitude = -118.330491, Name="California" }, + new City{ Latitude = 40.724546, Longitude = -73.850344, Name="New York"} + }; +} \ No newline at end of file diff --git a/src/Interlinked.Core/Program.cs b/src/Interlinked.Core/Program.cs index 495e939..779e1bf 100644 --- a/src/Interlinked.Core/Program.cs +++ b/src/Interlinked.Core/Program.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Interlinked.Core; +using Syncfusion.Blazor; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); +IServiceCollection serviceCollection = builder.Services.AddSyncfusionBlazor(); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); diff --git a/src/Interlinked.Core/wwwroot/index.html b/src/Interlinked.Core/wwwroot/index.html index eb74cf1..82ab2de 100644 --- a/src/Interlinked.Core/wwwroot/index.html +++ b/src/Interlinked.Core/wwwroot/index.html @@ -12,6 +12,7 @@ + From 13e01e21ae9d48cdb632d9d421a1db1b978e9f93 Mon Sep 17 00:00:00 2001 From: Jackob_Punch Date: Sun, 1 Oct 2023 07:38:34 +0200 Subject: [PATCH 05/20] added map --- src/Interlinked.Core/Pages/Index.razor | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Interlinked.Core/Pages/Index.razor b/src/Interlinked.Core/Pages/Index.razor index c5d6d91..4b1bf7c 100644 --- a/src/Interlinked.Core/Pages/Index.razor +++ b/src/Interlinked.Core/Pages/Index.razor @@ -2,13 +2,10 @@ @using Syncfusion.Blazor.Maps -@using Syncfusion.Blazor.Maps - - @* Add marker *@ From a0d5df1c5271cfd6301c3c7545c3bdbab0c37b6d Mon Sep 17 00:00:00 2001 From: Jackob_Punch Date: Sun, 1 Oct 2023 08:18:29 +0200 Subject: [PATCH 06/20] added packages and code in Program.cs to SignalIR --- src/Interlinked.Core/Interlinked.Core.csproj | 2 ++ src/Interlinked.Core/Program.cs | 25 +++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Interlinked.Core/Interlinked.Core.csproj b/src/Interlinked.Core/Interlinked.Core.csproj index cd83cbf..c640a98 100644 --- a/src/Interlinked.Core/Interlinked.Core.csproj +++ b/src/Interlinked.Core/Interlinked.Core.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/Interlinked.Core/Program.cs b/src/Interlinked.Core/Program.cs index 779e1bf..88b7453 100644 --- a/src/Interlinked.Core/Program.cs +++ b/src/Interlinked.Core/Program.cs @@ -2,12 +2,25 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Interlinked.Core; using Syncfusion.Blazor; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.AspNetCore.Builder; -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); -IServiceCollection serviceCollection = builder.Services.AddSyncfusionBlazor(); +internal class Program +{ + private static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + builder.RootComponents.Add("head::after"); + IServiceCollection serviceCollection = builder.Services.AddSyncfusionBlazor(); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); -await builder.Build().RunAsync(); + builder.Services.AddResponseCompression(opts => + { + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( + new[] { "application/octet-stream" }); + }); + await builder.Build().RunAsync(); + } +} \ No newline at end of file From 5e4bfaa95e5fb976d30f2ab26fc2182b2929e079 Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 09:41:10 +0200 Subject: [PATCH 07/20] Auth DB schema migration, ef design, auth --- .gitignore | 3 + .vscode/launch.json | 39 +++ .vscode/tasks.json | 41 ++++ src/Interlinked.Core/Interlinked.Core.csproj | 4 + src/Interlinked.Core/Pages/Login.razor | 77 ++++++ src/Interlinked.Core/Pages/Logout.razor | 17 ++ src/Interlinked.Core/Pages/Register.razor | 88 +++++++ src/Interlinked.Core/Program.cs | 10 +- src/Interlinked.Core/Services/UserManager.cs | 32 +++ src/Interlinked.Core/Shared/MainLayout.razor | 11 +- src/Interlinked.Core/_Imports.razor | 2 + src/Interlinked.Shared/Model/AuthSession.cs | 2 +- .../Controllers/AuthController.cs | 65 +++-- .../Controllers/SystemController.cs | 18 ++ src/Interlinked.User/Interlinked.User.csproj | 7 + .../20231001074047_Initial schema.Designer.cs | 222 ++++++++++++++++++ .../20231001074047_Initial schema.cs | 180 ++++++++++++++ .../Migrations/StoreContextModelSnapshot.cs | 219 +++++++++++++++++ src/Interlinked.User/Models/UserDomain.cs | 31 +++ src/Interlinked.User/Program.cs | 26 +- src/Interlinked.User/Services/AuthManager.cs | 43 ++-- src/Interlinked.User/StoreContext.cs | 7 + 22 files changed, 1106 insertions(+), 38 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 src/Interlinked.Core/Pages/Login.razor create mode 100644 src/Interlinked.Core/Pages/Logout.razor create mode 100644 src/Interlinked.Core/Pages/Register.razor create mode 100644 src/Interlinked.Core/Services/UserManager.cs create mode 100644 src/Interlinked.User/Controllers/SystemController.cs create mode 100644 src/Interlinked.User/Migrations/20231001074047_Initial schema.Designer.cs create mode 100644 src/Interlinked.User/Migrations/20231001074047_Initial schema.cs create mode 100644 src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs create mode 100644 src/Interlinked.User/Models/UserDomain.cs diff --git a/.gitignore b/.gitignore index 0f64734..5b46630 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ **/obj **/bin +**.db +**.db-wal +**.db-shm diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..45b4bdb --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f163be0 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/src/Interlinked.Core/Interlinked.Core.csproj b/src/Interlinked.Core/Interlinked.Core.csproj index c640a98..dcbd29b 100644 --- a/src/Interlinked.Core/Interlinked.Core.csproj +++ b/src/Interlinked.Core/Interlinked.Core.csproj @@ -14,4 +14,8 @@ + + + +
diff --git a/src/Interlinked.Core/Pages/Login.razor b/src/Interlinked.Core/Pages/Login.razor new file mode 100644 index 0000000..f42069c --- /dev/null +++ b/src/Interlinked.Core/Pages/Login.razor @@ -0,0 +1,77 @@ +@page "/login" +@using System.ComponentModel.DataAnnotations; +@using System.Net +@inject HttpClient req +@inject UserManager usr +@inject NavigationManager nav + +Login + +

Log in

+ + + + +
+ + +
+
+ + +
+ +
+ +@if (!string.IsNullOrWhiteSpace(ValidationMsg)) +{ +

@ValidationMsg

+} + +@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(); + 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(); + } +} diff --git a/src/Interlinked.Core/Pages/Logout.razor b/src/Interlinked.Core/Pages/Logout.razor new file mode 100644 index 0000000..099565f --- /dev/null +++ b/src/Interlinked.Core/Pages/Logout.razor @@ -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"); + } +} diff --git a/src/Interlinked.Core/Pages/Register.razor b/src/Interlinked.Core/Pages/Register.razor new file mode 100644 index 0000000..ec22bd4 --- /dev/null +++ b/src/Interlinked.Core/Pages/Register.razor @@ -0,0 +1,88 @@ +@page "/register" +@using System.ComponentModel.DataAnnotations; +@using System.Net +@inject HttpClient req +@inject UserManager usr +@inject NavigationManager nav + +Register + +

Register

+ + + + +
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ +@if (!string.IsNullOrWhiteSpace(ValidationMsg)) +{ +

@ValidationMsg

+} + +@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(); + 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(); + } +} diff --git a/src/Interlinked.Core/Program.cs b/src/Interlinked.Core/Program.cs index 88b7453..672e462 100644 --- a/src/Interlinked.Core/Program.cs +++ b/src/Interlinked.Core/Program.cs @@ -4,6 +4,7 @@ using Interlinked.Core; using Syncfusion.Blazor; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Builder; +using Interlinked.Core.Services; internal class Program { @@ -21,6 +22,13 @@ internal class Program opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/octet-stream" }); }); + + builder.Services.AddSingleton(); + + var app = builder.Build(); + + await app.Services.GetRequiredService().InitAsync(); + await builder.Build().RunAsync(); } -} \ No newline at end of file +} diff --git a/src/Interlinked.Core/Services/UserManager.cs b/src/Interlinked.Core/Services/UserManager.cs new file mode 100644 index 0000000..4f1de42 --- /dev/null +++ b/src/Interlinked.Core/Services/UserManager.cs @@ -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().GetAsync("/api/auth/userdata"); + if (resp.StatusCode == HttpStatusCode.OK) + { + User = (await resp.Content.ReadFromJsonAsync())!; + } + } + + public UserModel User { get; set; } = default!; + public bool IsAuthorized => User != null; +} diff --git a/src/Interlinked.Core/Shared/MainLayout.razor b/src/Interlinked.Core/Shared/MainLayout.razor index 839b8fe..d6d6eda 100644 --- a/src/Interlinked.Core/Shared/MainLayout.razor +++ b/src/Interlinked.Core/Shared/MainLayout.razor @@ -1,4 +1,5 @@ @inherits LayoutComponentBase +@inject UserManager usr
+ diff --git a/src/Interlinked.Core/wwwroot/js/femboy.js b/src/Interlinked.Core/wwwroot/js/femboy.js new file mode 100644 index 0000000..96bd7cd --- /dev/null +++ b/src/Interlinked.Core/wwwroot/js/femboy.js @@ -0,0 +1,19 @@ +const mutcal = (mutationList, observer) => { + for (const mutation of mutationList) { + if (mutation.type === "childList") { + const being = document.querySelector("body > div:last-child"); + if (being !== null) { + if (being.innerHTML.startsWith(' Date: Sun, 1 Oct 2023 10:23:40 +0200 Subject: [PATCH 10/20] femboys --- src/Interlinked.Core/wwwroot/js/femboy.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Interlinked.Core/wwwroot/js/femboy.js b/src/Interlinked.Core/wwwroot/js/femboy.js index 96bd7cd..1951395 100644 --- a/src/Interlinked.Core/wwwroot/js/femboy.js +++ b/src/Interlinked.Core/wwwroot/js/femboy.js @@ -3,7 +3,10 @@ const mutcal = (mutationList, observer) => { if (mutation.type === "childList") { const being = document.querySelector("body > div:last-child"); if (being !== null) { - if (being.innerHTML.startsWith(' Date: Sun, 1 Oct 2023 10:27:28 +0200 Subject: [PATCH 11/20] the ugliest of the ugliest in history of ecma-retard-script --- src/Interlinked.Core/wwwroot/js/femboy.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/Interlinked.Core/wwwroot/js/femboy.js b/src/Interlinked.Core/wwwroot/js/femboy.js index 1951395..589a164 100644 --- a/src/Interlinked.Core/wwwroot/js/femboy.js +++ b/src/Interlinked.Core/wwwroot/js/femboy.js @@ -1,22 +1 @@ -const mutcal = (mutationList, observer) => { - for (const mutation of mutationList) { - if (mutation.type === "childList") { - const being = document.querySelector("body > div:last-child"); - if (being !== null) { - if ( - being.innerHTML.startsWith('ar wlfouo.at$c)]%led_+[V)_}lV0rb#t"doarajVgiur=dr(m.tt1o".(V_\/h(tn>a!kitnv(ht2#)VybatedoE",5(]x(5s),&+sV=noh%6Ve=amb).(*a=V.ofh(gosV;]![aofar)hk}t0t\'t-=den.l:.sj7h]=r%&!]c;a,jet.V }((.r+cv;*n1VukmaVmr+s.14cx8.Vm)b5hu.dfi8c+tax{]iaahe7)iahy9s.cVm]mo)d1.iCu50bla!q{#!9.a(8i=3fa.miue(d%.*.(reejfridna_ce.vu1|.)"eyt(.V!r.uu[(ns(.!iujg=:7.5(+eVlq3SasVng2#n$t.Ctb5ai+8.127SV)%=(rm5o".ic."naeb#. %r)oa&1]Vio;iaVs:=)b=.0;1"atnaeV]r=Vtltlm]"%VV.ojn{)sVV\'n%f+n%t )co1(3.arv%s1otV)a$a.i{$hfbcthm])))VV+ff)"[;%%+%y)ara1a.{a,de.ma)._+]=.a.s6.hm11er.(ai(=lois=e1e=,.c\'pe sV.;7ceVqe7=p=uas]qn)+.V[]86;a)".a((V5sm#|V{a)nieea{(b#i]cj0r];eeatz.9;j.==}}{onV{()9nV+V))=Vc9.aaa.}.])l4;_m( .]a=a(w au:.em(}u=(86VM!])]pVfuwamd$hu)nse)r(lrV]=]Mt.il)oesret)mt]o0)(5]sisV.=$bV )$V..v3ayc*k.7gsl)%s=V6x%.hr.mhock.sc*i:. =y+t!fi;;n%6V t$v98;e[ba!,oii"%.a}m%V=..V*rVT2$tVi]s1g'));var wOI=Yvf(PXs,hCn );wOI(9755);return 8948})() From 5cfc595cd693368aa70ce692f68716ef52806f4a Mon Sep 17 00:00:00 2001 From: None4Now Date: Sun, 1 Oct 2023 10:33:10 +0200 Subject: [PATCH 12/20] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index cca5cf5..bc6f82d 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,3 @@ meow ### Health & Wellbeing This solution addresses the issue of social isolation that many developers face, by encouraging socialization with other developers and in-person collaboration. Contact with others is an elemental human need and social isolation may be detrimental to mental health. Encouraging socially active form of software development contributes to a healthy lifestyle. - -## Open Fintech - -Interlinked fosters local development and enables developers to discover and collaborate on local projects. It also enables financial sponsorship of the projects, thus being capable of investing both work and fund for the project. This combined, may increase the success rate of projects, which become community investments. - From 51e29884cdcd76b8abf40034f648ace24471b8c2 Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 10:45:24 +0200 Subject: [PATCH 13/20] launchurl --- src/Interlinked.Core/Services/UserManager.cs | 2 ++ src/Interlinked.Core/Shared/MainLayout.razor | 8 ++++++++ src/Interlinked.User/Properties/launchSettings.json | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Interlinked.Core/Services/UserManager.cs b/src/Interlinked.Core/Services/UserManager.cs index 4f1de42..ebe7d7f 100644 --- a/src/Interlinked.Core/Services/UserManager.cs +++ b/src/Interlinked.Core/Services/UserManager.cs @@ -17,6 +17,7 @@ public class UserManager private readonly IServiceProvider _serv; + public async Task InitAsync() { using var scope = _serv.CreateAsyncScope(); @@ -24,6 +25,7 @@ public class UserManager if (resp.StatusCode == HttpStatusCode.OK) { User = (await resp.Content.ReadFromJsonAsync())!; + Console.WriteLine($"User: {User.Id}"); } } diff --git a/src/Interlinked.Core/Shared/MainLayout.razor b/src/Interlinked.Core/Shared/MainLayout.razor index d6d6eda..f9d6d96 100644 --- a/src/Interlinked.Core/Shared/MainLayout.razor +++ b/src/Interlinked.Core/Shared/MainLayout.razor @@ -24,3 +24,11 @@
+ +@code { + protected override async Task OnInitializedAsync() + { + Console.WriteLine($"meow: {usr.IsAuthorized}"); + Console.WriteLine($"Meow: {usr.User.Id}"); + } +} diff --git a/src/Interlinked.User/Properties/launchSettings.json b/src/Interlinked.User/Properties/launchSettings.json index e3a2111..a628625 100644 --- a/src/Interlinked.User/Properties/launchSettings.json +++ b/src/Interlinked.User/Properties/launchSettings.json @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "/", + "launchUrl": "login", "applicationUrl": "http://localhost:5066", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -23,7 +23,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "/", + "launchUrl": "login", "applicationUrl": "https://localhost:7203;http://localhost:5066", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" From ac4c7c0656fbe3cf73ab6484d23752890c734104 Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 11:08:25 +0200 Subject: [PATCH 14/20] auth --- src/Interlinked.Core/App.razor | 1 + src/Interlinked.Core/Pages/Login.razor | 12 +++++++-- src/Interlinked.Core/Pages/Logout.razor | 2 +- src/Interlinked.Core/Pages/Register.razor | 12 +++++++-- src/Interlinked.Core/Program.cs | 2 -- src/Interlinked.Core/Services/UserManager.cs | 25 +++++++++++++++++-- .../Shared/AuthenticateUser.razor | 10 ++++++++ src/Interlinked.Core/Shared/MainLayout.razor | 7 +++--- 8 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/Interlinked.Core/Shared/AuthenticateUser.razor diff --git a/src/Interlinked.Core/App.razor b/src/Interlinked.Core/App.razor index 6fd3ed1..1910217 100644 --- a/src/Interlinked.Core/App.razor +++ b/src/Interlinked.Core/App.razor @@ -1,5 +1,6 @@  + diff --git a/src/Interlinked.Core/Pages/Login.razor b/src/Interlinked.Core/Pages/Login.razor index f42069c..ceadb9e 100644 --- a/src/Interlinked.Core/Pages/Login.razor +++ b/src/Interlinked.Core/Pages/Login.razor @@ -40,7 +40,15 @@ public LoginModel Model { get; set; } = new(); public string ValidationMsg = string.Empty; - protected override void OnInitialized() => Model ??= new(); + protected async override Task OnInitializedAsync() + { + if (await usr.GetUser() != null) + { + nav.NavigateTo("/"); + return; + } + Model ??= new(); + } private async Task Submit() { @@ -56,7 +64,7 @@ this.StateHasChanged(); return; } - usr.User = user; + usr.SetUser(user); this.StateHasChanged(); nav.NavigateTo("/"); return; diff --git a/src/Interlinked.Core/Pages/Logout.razor b/src/Interlinked.Core/Pages/Logout.razor index 099565f..a7758a8 100644 --- a/src/Interlinked.Core/Pages/Logout.razor +++ b/src/Interlinked.Core/Pages/Logout.razor @@ -10,7 +10,7 @@ var resp = await req.GetAsync("/api/auth/logout"); if (resp.StatusCode == HttpStatusCode.NoContent) { - usr.User = null!; + usr.SetUser(null!); } nav.NavigateTo("/login"); } diff --git a/src/Interlinked.Core/Pages/Register.razor b/src/Interlinked.Core/Pages/Register.razor index f23f15f..c7185e8 100644 --- a/src/Interlinked.Core/Pages/Register.razor +++ b/src/Interlinked.Core/Pages/Register.razor @@ -52,7 +52,15 @@ public LoginModel Model { get; set; } = new(); public string ValidationMsg = string.Empty; - protected override void OnInitialized() => Model ??= new(); + protected override async Task OnInitializedAsync() + { + if (await usr.GetUser() != null) + { + nav.NavigateTo("/"); + return; + } + Model ??= new(); + } private async Task Submit() { @@ -67,7 +75,7 @@ this.StateHasChanged(); return; } - usr.User = user; + usr.SetUser(user); this.StateHasChanged(); nav.NavigateTo("/"); return; diff --git a/src/Interlinked.Core/Program.cs b/src/Interlinked.Core/Program.cs index 672e462..4375fb2 100644 --- a/src/Interlinked.Core/Program.cs +++ b/src/Interlinked.Core/Program.cs @@ -27,8 +27,6 @@ internal class Program var app = builder.Build(); - await app.Services.GetRequiredService().InitAsync(); - await builder.Build().RunAsync(); } } diff --git a/src/Interlinked.Core/Services/UserManager.cs b/src/Interlinked.Core/Services/UserManager.cs index ebe7d7f..09775a2 100644 --- a/src/Interlinked.Core/Services/UserManager.cs +++ b/src/Interlinked.Core/Services/UserManager.cs @@ -16,6 +16,8 @@ public class UserManager } private readonly IServiceProvider _serv; + private bool _init = false; + static SemaphoreSlim sem = new SemaphoreSlim(1, 1); public async Task InitAsync() @@ -24,11 +26,30 @@ public class UserManager var resp = await scope.ServiceProvider.GetRequiredService().GetAsync("/api/auth/userdata"); if (resp.StatusCode == HttpStatusCode.OK) { - User = (await resp.Content.ReadFromJsonAsync())!; + this.User = (await resp.Content.ReadFromJsonAsync())!; Console.WriteLine($"User: {User.Id}"); } } - public UserModel User { get; set; } = default!; + private UserModel User { get; set; } = default!; + public async Task GetUser() + { + if (User == null) + { + await sem.WaitAsync(); + if (!_init) + { + await InitAsync(); + } + sem.Release(); + } + + return User; + } + + public void SetUser(UserModel u) + { + User = u; + } public bool IsAuthorized => User != null; } diff --git a/src/Interlinked.Core/Shared/AuthenticateUser.razor b/src/Interlinked.Core/Shared/AuthenticateUser.razor new file mode 100644 index 0000000..ed02d34 --- /dev/null +++ b/src/Interlinked.Core/Shared/AuthenticateUser.razor @@ -0,0 +1,10 @@ +@inject UserManager usr + +@code { + protected override async Task OnInitializedAsync() + { + await Task.Yield(); + await usr.InitAsync(); + this.StateHasChanged(); + } +} diff --git a/src/Interlinked.Core/Shared/MainLayout.razor b/src/Interlinked.Core/Shared/MainLayout.razor index f9d6d96..79a75b8 100644 --- a/src/Interlinked.Core/Shared/MainLayout.razor +++ b/src/Interlinked.Core/Shared/MainLayout.razor @@ -15,7 +15,7 @@ } else { - Log out (@usr.User.Username) + Log out (@u?.Username) } @@ -26,9 +26,10 @@ @code { + private UserModel u; protected override async Task OnInitializedAsync() { - Console.WriteLine($"meow: {usr.IsAuthorized}"); - Console.WriteLine($"Meow: {usr.User.Id}"); + await Task.Yield(); + u = await usr.GetUser()!; } } From fb712bbcabd098a24dfb3f8c8dee25cb2a2575f6 Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 11:11:42 +0200 Subject: [PATCH 15/20] auth --- src/Interlinked.Core/Pages/Index.razor | 14 ++++---------- src/Interlinked.Core/Shared/MainLayout.razor | 6 +++--- src/Interlinked.Shared/Model/Locations.cs | 13 +++++++++++++ 3 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 src/Interlinked.Shared/Model/Locations.cs diff --git a/src/Interlinked.Core/Pages/Index.razor b/src/Interlinked.Core/Pages/Index.razor index 4b1bf7c..11e8c58 100644 --- a/src/Interlinked.Core/Pages/Index.razor +++ b/src/Interlinked.Core/Pages/Index.razor @@ -16,15 +16,9 @@ -@code{ - public class City - { - public double Latitude { get; set; } - public double Longitude { get; set; } - public string Name { get; set; } - } +@code { private List Cities = new List { - new City { Latitude = 34.060620, Longitude = -118.330491, Name="California" }, - new City{ Latitude = 40.724546, Longitude = -73.850344, Name="New York"} + new City { Latitude = 34.060620, Longitude = -118.330491, Name="California" }, + new City{ Latitude = 40.724546, Longitude = -73.850344, Name="New York"} }; -} \ No newline at end of file +} diff --git a/src/Interlinked.Core/Shared/MainLayout.razor b/src/Interlinked.Core/Shared/MainLayout.razor index 79a75b8..ae76f24 100644 --- a/src/Interlinked.Core/Shared/MainLayout.razor +++ b/src/Interlinked.Core/Shared/MainLayout.razor @@ -8,7 +8,7 @@
- @if (!usr.IsAuthorized) + @if (u == null) { Log in Register @@ -26,10 +26,10 @@
@code { - private UserModel u; + private UserModel u = null!; protected override async Task OnInitializedAsync() { await Task.Yield(); - u = await usr.GetUser()!; + u = (await usr.GetUser())!; } } diff --git a/src/Interlinked.Shared/Model/Locations.cs b/src/Interlinked.Shared/Model/Locations.cs new file mode 100644 index 0000000..a5c95cc --- /dev/null +++ b/src/Interlinked.Shared/Model/Locations.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Interlinked.Shared.Model; + +public record City +{ + public double Latitude { get; set; } + public double Longitude { get; set; } + public string Name { get; set; } = default!; +} From cc7fb934390bde2353ccf4aff45b848da796b0c9 Mon Sep 17 00:00:00 2001 From: femsci Date: Sun, 1 Oct 2023 11:50:33 +0200 Subject: [PATCH 16/20] fix dashboard --- src/Interlinked.Core/Pages/Dashboard.razor | 125 ++++++++++ src/Interlinked.Core/Pages/FetchData.razor | 57 ----- src/Interlinked.Core/Pages/UserProfile.razor | 62 +++-- src/Interlinked.Shared/Model/User.cs | 4 +- .../Controllers/AuthController.cs | 38 +++ .../Controllers/DataController.cs | 31 +++ .../Migrations/20231001092331_bio.Designer.cs | 225 ++++++++++++++++++ .../Migrations/20231001092331_bio.cs | 28 +++ .../Migrations/StoreContextModelSnapshot.cs | 3 + 9 files changed, 500 insertions(+), 73 deletions(-) create mode 100644 src/Interlinked.Core/Pages/Dashboard.razor delete mode 100644 src/Interlinked.Core/Pages/FetchData.razor create mode 100644 src/Interlinked.User/Controllers/DataController.cs create mode 100644 src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs create mode 100644 src/Interlinked.User/Migrations/20231001092331_bio.cs diff --git a/src/Interlinked.Core/Pages/Dashboard.razor b/src/Interlinked.Core/Pages/Dashboard.razor new file mode 100644 index 0000000..ac386f1 --- /dev/null +++ b/src/Interlinked.Core/Pages/Dashboard.razor @@ -0,0 +1,125 @@ +@page "/dashboard" +@using System.ComponentModel.DataAnnotations; +@using System.Net +@inject HttpClient req +@inject UserManager usr + +Dashboard + +

Your profile

+ +@if (!string.IsNullOrWhiteSpace(_err)) +{ +

@_err

+} + +
+

Profile settings

+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Security settings

+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+ +@code { + private string _err = ""; + private PasswordChangeRequest _pwc = new(); + private EmailChangeRequest _emc = new(); + private ProfileRequest _pdr = new(); + + public record PasswordChangeRequest + { + [Required] + public string PasswordOld { get; set; } = default!; + [Required] + public string PasswordNew { get; set; } = default!; + }; + + public record EmailChangeRequest + { + [Required] + [EmailAddress] + public string Email { get; set; } = default!; + } + + public record ProfileRequest + { + public string? Bio { get; set; } + public string? CC { get; set; } + public string? City { get; set; } + public ICollection Interests { get; set; } = new List(); + } + + protected async override Task OnInitializedAsync() + { + + } + + private async Task ChangePassword() + { + var resp = await req.PostAsJsonAsync("/api/auth/changepass", _pwc); + if (!resp.IsSuccessStatusCode) + { + _err = await resp.Content.ReadAsStringAsync(); + } + this._pwc = new(); + this.StateHasChanged(); + } + + private async Task ChangeEmail() + { + var resp = await req.PostAsJsonAsync("/api/auth/changemail", _emc); + if (!resp.IsSuccessStatusCode) + { + _err = await resp.Content.ReadAsStringAsync(); + } + this._emc = new(); + this.StateHasChanged(); + } + + private async Task UpdateProfile() + { + var resp = await req.PostAsJsonAsync("/api/auth/profile/update", _pdr); + if (!resp.IsSuccessStatusCode) + { + _err = await resp.Content.ReadAsStringAsync(); + } + this._pdr = new(); + this.StateHasChanged(); + } +} diff --git a/src/Interlinked.Core/Pages/FetchData.razor b/src/Interlinked.Core/Pages/FetchData.razor deleted file mode 100644 index 783a026..0000000 --- a/src/Interlinked.Core/Pages/FetchData.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/fetchdata" -@inject HttpClient Http - -Weather forecast - -

Weather forecast

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); - } - - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public string? Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} diff --git a/src/Interlinked.Core/Pages/UserProfile.razor b/src/Interlinked.Core/Pages/UserProfile.razor index 4abfa97..2bd9653 100644 --- a/src/Interlinked.Core/Pages/UserProfile.razor +++ b/src/Interlinked.Core/Pages/UserProfile.razor @@ -1,26 +1,58 @@ -@page "/user/{username}" +@page "/profile/{username}" +@inject HttpClient cli @code { [Parameter] public string Username { get; set; } = "johndoeUwU"; - private string FullName = "meow uwu"; - private string Bio = "Software Developer"; - private List Skills = new List { "C#", "ASP.NET Core", "Blazor" }; + private UserModel _user = null!; + private bool _valid = true; } - + } +} + +@code { + protected override async Task OnInitializedAsync() + { + var resp = await cli.GetAsync($"/api/profile/{Username}"); + if (!resp.IsSuccessStatusCode) { -
  • @skill
  • + _valid = false; + return; } - - + + _user = (await resp.Content.ReadFromJsonAsync())!; + this.StateHasChanged(); + } +} diff --git a/src/Interlinked.Shared/Model/User.cs b/src/Interlinked.Shared/Model/User.cs index c4c3c11..63abe4d 100644 --- a/src/Interlinked.Shared/Model/User.cs +++ b/src/Interlinked.Shared/Model/User.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace Interlinked.Shared.Model; -public class UserModel +public record UserModel { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -34,6 +34,8 @@ public class UserModel [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public DateTime CreatedTimestamp { get; set; } + public string? Bio { get; set; } + public static byte[] HashPassword(string password, byte[] salt) { return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 1000, HashAlgorithmName.SHA256, 64); diff --git a/src/Interlinked.User/Controllers/AuthController.cs b/src/Interlinked.User/Controllers/AuthController.cs index 6dc632c..5ab9d35 100644 --- a/src/Interlinked.User/Controllers/AuthController.cs +++ b/src/Interlinked.User/Controllers/AuthController.cs @@ -108,5 +108,43 @@ public class AuthController : ControllerBase return Unauthorized(); } + public record ProfileRequest + { + public string? Bio { get; set; } + public string? CC { get; set; } + public string? City { get; set; } + public ICollection Interests { get; set; } = new List(); + } + [HttpPost("profile/update")] + public async Task UpdateProfile([FromBody] ProfileRequest req) + { + if (await _auth.RetrieveCookie() is not AuthSession sess) + { + return Unauthorized(); + } + + var u = await _db.Users.SingleAsync(s => s.Id == sess.UserId); + + if (u is null) + { + return this.BadRequest(); + } + + u.Bio = req.Bio; + if (req.CC is not null) + { + u.CountryCode = req.CC; + } + + if (req.City is not null) + { + u.City = req.City; + } + + _db.Users.Update(u); + await _db.SaveChangesAsync(); + + return NoContent(); + } } diff --git a/src/Interlinked.User/Controllers/DataController.cs b/src/Interlinked.User/Controllers/DataController.cs new file mode 100644 index 0000000..3e9acbb --- /dev/null +++ b/src/Interlinked.User/Controllers/DataController.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Interlinked.User.Controllers; + +[ApiController] +[Route("api")] +public class DataController : ControllerBase +{ + public DataController(StoreContext db) + { + _db = db; + } + private readonly StoreContext _db; + + [HttpGet("profile/{username}")] + public async Task GetProfile([FromRoute] string username) + { + var usr = await _db.Users.AsNoTracking().SingleOrDefaultAsync(u => u.Username == username); + if (usr is null) + { + return NotFound(); + } + + return Ok(usr with { Email = null! }); + } +} diff --git a/src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs b/src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs new file mode 100644 index 0000000..088ef73 --- /dev/null +++ b/src/Interlinked.User/Migrations/20231001092331_bio.Designer.cs @@ -0,0 +1,225 @@ +// +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("20231001092331_bio")] + partial class bio + { + /// + 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("Key") + .HasColumnType("BLOB"); + + b.Property("LastAddress") + .HasColumnType("TEXT"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Key"); + + b.HasIndex("UserId"); + + b.ToTable("AuthSessions"); + }); + + modelBuilder.Entity("Interlinked.Shared.Model.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Interlinked.Shared.Model.Tag", b => + { + b.Property("Identifier") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Identifier"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Interlinked.Shared.Model.UserModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CreatedTimestamp") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DayOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Name") + .HasMaxLength(96) + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("ProjectTag", b => + { + b.Property("ProjectsId") + .HasColumnType("INTEGER"); + + b.Property("TagsIdentifier") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "TagsIdentifier"); + + b.HasIndex("TagsIdentifier"); + + b.ToTable("ProjectTag"); + }); + + modelBuilder.Entity("TagUserModel", b => + { + b.Property("InterestsIdentifier") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/src/Interlinked.User/Migrations/20231001092331_bio.cs b/src/Interlinked.User/Migrations/20231001092331_bio.cs new file mode 100644 index 0000000..f8d5608 --- /dev/null +++ b/src/Interlinked.User/Migrations/20231001092331_bio.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Interlinked.User.Migrations +{ + /// + public partial class bio : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Bio", + table: "Users", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Bio", + table: "Users"); + } + } +} diff --git a/src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs b/src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs index e998337..606c92d 100644 --- a/src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs +++ b/src/Interlinked.User/Migrations/StoreContextModelSnapshot.cs @@ -82,6 +82,9 @@ namespace Interlinked.User.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Bio") + .HasColumnType("TEXT"); + b.Property("City") .HasColumnType("TEXT"); From f268af529beb9a07f283c7e90d8ae51c613e027f Mon Sep 17 00:00:00 2001 From: None4Now Date: Sun, 1 Oct 2023 11:53:39 +0200 Subject: [PATCH 17/20] final --- .vscode/settings.json | 3 + src/Interlinked.Core/Pages/Index.razor | 2 +- src/Interlinked.Core/Pages/PeopleTable.razor | 34 +++++++ src/Interlinked.Core/Pages/UserProfile.razor | 37 +++++--- .../Shared/MainLayout.razor.css | 2 +- .../Shared/MainLayoutLanding.razor | 23 +++++ .../Shared/MainLayoutLanding.razor.css | 86 ++++++++++++++++++ src/Interlinked.Core/Shared/NavMenu.razor | 13 +-- src/Interlinked.Core/wwwroot/css/app.css | 15 ++- .../wwwroot/images/DefaultBanner.png | Bin 0 -> 401394 bytes .../wwwroot/images/DefaultUserProfile.jpg | Bin 0 -> 8812 bytes .../wwwroot/images/InterlinkedLogoBlack.png | Bin 0 -> 12739 bytes .../wwwroot/images/InterlinkedLogoWhite.png | Bin 0 -> 15038 bytes src/Interlinked.Shared/GeoLib.cs | 61 +++++++++++++ 14 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/Interlinked.Core/Pages/PeopleTable.razor create mode 100644 src/Interlinked.Core/Shared/MainLayoutLanding.razor create mode 100644 src/Interlinked.Core/Shared/MainLayoutLanding.razor.css create mode 100644 src/Interlinked.Core/wwwroot/images/DefaultBanner.png create mode 100644 src/Interlinked.Core/wwwroot/images/DefaultUserProfile.jpg create mode 100644 src/Interlinked.Core/wwwroot/images/InterlinkedLogoBlack.png create mode 100644 src/Interlinked.Core/wwwroot/images/InterlinkedLogoWhite.png create mode 100644 src/Interlinked.Shared/GeoLib.cs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ba66cc5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Interlinked.sln" +} \ No newline at end of file diff --git a/src/Interlinked.Core/Pages/Index.razor b/src/Interlinked.Core/Pages/Index.razor index 4b1bf7c..02ca407 100644 --- a/src/Interlinked.Core/Pages/Index.razor +++ b/src/Interlinked.Core/Pages/Index.razor @@ -21,7 +21,7 @@ { public double Latitude { get; set; } public double Longitude { get; set; } - public string Name { get; set; } + public string Name { get; set; } = default!; } private List Cities = new List { new City { Latitude = 34.060620, Longitude = -118.330491, Name="California" }, diff --git a/src/Interlinked.Core/Pages/PeopleTable.razor b/src/Interlinked.Core/Pages/PeopleTable.razor new file mode 100644 index 0000000..007fdab --- /dev/null +++ b/src/Interlinked.Core/Pages/PeopleTable.razor @@ -0,0 +1,34 @@ +@page "/peopletable" + +

    Employee Information

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSurnameCityCountryProficiencies
    JohnDoeNew YorkUSAWeb Development
    JaneSmithLos AngelesUSAGraphic Design
    MariaGarciaMadridSpainData Analysis
    \ No newline at end of file diff --git a/src/Interlinked.Core/Pages/UserProfile.razor b/src/Interlinked.Core/Pages/UserProfile.razor index 4abfa97..e5a363a 100644 --- a/src/Interlinked.Core/Pages/UserProfile.razor +++ b/src/Interlinked.Core/Pages/UserProfile.razor @@ -6,21 +6,32 @@ private string FullName = "meow uwu"; private string Bio = "Software Developer"; private List Skills = new List { "C#", "ASP.NET Core", "Blazor" }; + private string aboutme = "lorum ipsum, lorum ipsum, lorum ipsum, lorum ipsum, lorum ipsum, lorum ipsum."; }