diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs new file mode 100644 index 0000000..05f88e7 --- /dev/null +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -0,0 +1,95 @@ +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using NodaTime; + +namespace DysonNetwork.Pass; + +public abstract class ModelBase +{ + public Instant CreatedAt { get; set; } + public Instant UpdatedAt { get; set; } + public Instant? DeletedAt { get; set; } +} + +public class AppDatabase( + DbContextOptions options, + IConfiguration configuration +) : DbContext(options) +{ + public DbSet PermissionNodes { get; set; } + public DbSet PermissionGroups { get; set; } + public DbSet PermissionGroupMembers { get; set; } + + public DbSet Accounts { get; set; } + public DbSet AccountConnections { get; set; } + public DbSet AccountProfiles { get; set; } + public DbSet AccountContacts { get; set; } + public DbSet AccountAuthFactors { get; set; } + public DbSet AccountRelationships { get; set; } + + public DbSet AuthSessions { get; set; } + public DbSet AuthChallenges { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql( + configuration.GetConnectionString("App"), + opt => opt + .UseNodaTime() + ).UseSnakeCaseNamingConvention(); + + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(pg => new { pg.GroupId, pg.Actor }); + + modelBuilder.Entity() + .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = now; + entry.Entity.UpdatedAt = now; + } + else if (entry.State == EntityState.Modified) + { + entry.Entity.UpdatedAt = now; + } + else if (entry.State == EntityState.Deleted) + { + entry.State = EntityState.Modified; + entry.Entity.DeletedAt = now; + } + } + + return await base.SaveChangesAsync(cancellationToken); + } +} + +public class AppDatabaseFactory : IDesignTimeDbContextFactory +{ + public AppDatabase CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + return new AppDatabase(optionsBuilder.Options, configuration); + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/DysonNetwork.Pass.csproj b/DysonNetwork.Pass/DysonNetwork.Pass.csproj index df15174..7938438 100644 --- a/DysonNetwork.Pass/DysonNetwork.Pass.csproj +++ b/DysonNetwork.Pass/DysonNetwork.Pass.csproj @@ -23,7 +23,7 @@ - + diff --git a/DysonNetwork.Shared/Protos/account.proto b/DysonNetwork.Shared/Protos/account.proto index d06d957..909568c 100644 --- a/DysonNetwork.Shared/Protos/account.proto +++ b/DysonNetwork.Shared/Protos/account.proto @@ -5,7 +5,7 @@ package dyson_network.sphere.account; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; -option csharp_namespace = "DysonNetwork.Sphere.Account.Proto"; +option csharp_namespace = "DysonNetwork.Shared.Protos.Account"; service AccountService { rpc GetAccount(google.protobuf.Empty) returns (AccountResponse); diff --git a/DysonNetwork.Shared/Protos/auth.proto b/DysonNetwork.Shared/Protos/auth.proto index c81050a..0390bf8 100644 --- a/DysonNetwork.Shared/Protos/auth.proto +++ b/DysonNetwork.Shared/Protos/auth.proto @@ -5,7 +5,7 @@ package dyson_network.sphere.auth; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; -option csharp_namespace = "DysonNetwork.Sphere.Auth.Proto"; +option csharp_namespace = "DysonNetwork.Shared.Protos.Auth"; service AuthService { rpc Login(LoginRequest) returns (LoginResponse); diff --git a/DysonNetwork.Sphere/Auth/AuthGrpcService.cs b/DysonNetwork.Sphere/Auth/AuthGrpcService.cs index c13da85..9c385b8 100644 --- a/DysonNetwork.Sphere/Auth/AuthGrpcService.cs +++ b/DysonNetwork.Sphere/Auth/AuthGrpcService.cs @@ -6,33 +6,21 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using System.Text.Json; using DysonNetwork.Shared.Models; -using Challenge = DysonNetwork.Sphere.Auth.Proto.Challenge; -using Session = DysonNetwork.Sphere.Auth.Proto.Session; namespace DysonNetwork.Sphere.Auth; -public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthServiceBase +public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth) + : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthServiceBase { - private readonly AppDatabase _db; - private readonly AccountService _accounts; - private readonly AuthService _auth; - - public AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth) - { - _db = db; - _accounts = accounts; - _auth = auth; - } - public override async Task Login(LoginRequest request, ServerCallContext context) { - var account = await _accounts.LookupAccount(request.Username); + var account = await accounts.LookupAccount(request.Username); if (account == null) { throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found.")); } - var factor = await _db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password); + var factor = await db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password); if (factor == null || !factor.VerifyPassword(request.Password)) { throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid credentials.")); @@ -46,10 +34,10 @@ public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthSe Challenge = new Challenge() // Create a dummy challenge }; - _db.AuthSessions.Add(session); - await _db.SaveChangesAsync(); + db.AuthSessions.Add(session); + await db.SaveChangesAsync(); - var token = _auth.CreateToken(session); + var token = auth.CreateToken(session); return new LoginResponse { @@ -60,9 +48,9 @@ public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthSe public override async Task IntrospectToken(IntrospectTokenRequest request, ServerCallContext context) { - if (_auth.ValidateToken(request.Token, out var sessionId)) + if (auth.ValidateToken(request.Token, out var sessionId)) { - var session = await _db.AuthSessions + var session = await db.AuthSessions .Include(s => s.Account) .Include(s => s.Challenge) .FirstOrDefaultAsync(s => s.Id == sessionId); @@ -91,13 +79,13 @@ public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthSe if (authorizationHeader != null) { var token = authorizationHeader.Value.Replace("Bearer ", ""); - if (_auth.ValidateToken(token, out var sessionId)) + if (auth.ValidateToken(token, out var sessionId)) { - var session = await _db.AuthSessions.FindAsync(sessionId); + var session = await db.AuthSessions.FindAsync(sessionId); if (session != null) { - _db.AuthSessions.Remove(session); - await _db.SaveChangesAsync(); + db.AuthSessions.Remove(session); + await db.SaveChangesAsync(); } } }