backend
This commit is contained in:
parent
6019620849
commit
eb8b32d5f2
19 changed files with 317 additions and 193 deletions
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
namespace Interlinked.Shared;
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
24
src/Interlinked.Shared/Model/AuthSession.cs
Normal file
24
src/Interlinked.Shared/Model/AuthSession.cs
Normal 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
|
||||
}
|
||||
}
|
22
src/Interlinked.Shared/Model/Project.cs
Normal file
22
src/Interlinked.Shared/Model/Project.cs
Normal 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!;
|
||||
|
||||
}
|
17
src/Interlinked.Shared/Model/Tag.cs
Normal file
17
src/Interlinked.Shared/Model/Tag.cs
Normal 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!;
|
||||
}
|
48
src/Interlinked.Shared/Model/User.cs
Normal file
48
src/Interlinked.Shared/Model/User.cs
Normal 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!;
|
||||
}
|
86
src/Interlinked.User/Controllers/AuthController.cs
Normal file
86
src/Interlinked.User/Controllers/AuthController.cs
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -7,8 +7,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<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.EntityFrameworkCore" Version="7.0.11" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Interlinked.Shared\Interlinked.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
using Interlinked.Shared.Model;
|
||||
using Interlinked.User;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddDbContext<StoreContext>();
|
||||
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<UserModel>(o => o.SignIn.RequireConfirmedAccount = true)
|
||||
.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();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
|
75
src/Interlinked.User/Services/AuthManager.cs
Normal file
75
src/Interlinked.User/Services/AuthManager.cs
Normal 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;
|
||||
}
|
||||
}
|
19
src/Interlinked.User/StoreContext.cs
Normal file
19
src/Interlinked.User/StoreContext.cs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue