From bed9c8a2725d354251c0931f68cb8f5f66a2fd28 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 11 Jul 2026 03:26:13 +0100 Subject: [PATCH 001/151] fixes the compile and timestamps to db --- .../PostIt.Android/PlatformBootstrap.cs | 4 ++-- .../Controllers/Business/BillingController.cs | 20 +++++++++---------- .../Business/BookQueryApiController.cs | 2 +- .../Business/EstimateApiController.cs | 2 +- .../Controllers/HairCut/HairCutController.cs | 2 +- .../NativeConfidentialController.cs | 12 +++++------ .../Accounting/ManageController.cs | 2 +- .../Contracting/CommandController.cs | 2 +- .../Haircut/HairCutCommandController.cs | 2 +- .../Services/ChatHubConnexionManager.cs | 2 +- src/Yavsc.Org/Services/DiskUsageTracker.cs | 4 ++-- .../ViewComponents/CalendarViewComponent.cs | 4 ++-- src/Yavsc.Server/Helpers/RequestHelper.cs | 6 +++--- src/Yavsc.Server/Hubs/ChatHub.cs | 2 +- .../Models/ApplicationDbContext.cs | 6 +++--- 15 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index 0d11035a..d59b154f 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -19,11 +19,11 @@ internal static class PlatformBootstrap if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0) return; - Platform.DefaultRedirectUri = Settings.AndroidRedirectUri; + Platform.DefaultRedirectUri = ViewModels.Settings.AndroidRedirectUri; Platform.CreateBrowser = () => { var activity = MainActivity.Current; return activity is null ? null : new AndroidSystemBrowser(activity); }; } -} \ No newline at end of file +} diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs index 65a6dbcf..87035406 100644 --- a/src/Yavsc.Api/Controllers/Business/BillingController.cs +++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs @@ -53,18 +53,18 @@ namespace Yavsc.ApiControllers [HttpGet("facture-{billingCode}-{id}.pdf"), Authorize] public async Task GetPdf(string billingCode, long id) - { + { var bill = await billingService.GetBillAsync(billingCode, id); if ( authorizationService.AuthorizeAsync(User, bill, new ReadPermission()).IsFaulted) { return new ChallengeResult(); } - + var fi = bill.GetBillInfo(billingService); if (!fi.Exists) return Ok(new { Error = "Not generated" }); - return File(fi.OpenRead(), "application/x-pdf", fi.Name); + return File(fi.OpenRead(), "application/x-pdf", fi.Name); } [HttpGet("facture-{billingCode}-{id}.tex"), Authorize] @@ -90,7 +90,7 @@ namespace Yavsc.ApiControllers public async Task GeneratePdf(string billingCode, long id) { var bill = await billingService.GetBillAsync(billingCode, id); - + if (bill==null) { logger.LogCritical ( $"# not found !! {id} in {billingCode}"); return this.NotFound(); @@ -111,20 +111,20 @@ namespace Yavsc.ApiControllers return new BadRequestResult(); if (!(await authorizationService.AuthorizeAsync(User, estimate, new ReadPermission())).Succeeded) - + { return new ChallengeResult(); } if (Request.Form.Files.Count!=1) return new BadRequestResult(); await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"pro"); - estimate.ProviderValidationDate = DateTime.Now; + estimate.ProviderValidationDate = DateTime.UtcNow; dbContext.SaveChanges(User.GetUserId()); // Notify the client var locstr = _localizer["EstimationMessageToClient"]; var yaev = new EstimationEvent(estimate,_localizer); - + var regids = new [] { estimate.Client.Id }; bool gcmSent = false; var grep = await _GCMSender.NotifyEstimateAsync(regids,yaev); @@ -138,7 +138,7 @@ namespace Yavsc.ApiControllers // For authorization purpose var estimate = dbContext.Estimates.FirstOrDefault(e=>e.Id == id); if (!(await authorizationService.AuthorizeAsync(User, estimate, new ReadPermission())).Succeeded) - + { return new ChallengeResult(); } @@ -163,7 +163,7 @@ namespace Yavsc.ApiControllers if (Request.Form.Files.Count!=1) return new BadRequestResult(); await User.ReceiveProSignatureAsync(billingCode,id,Request.Form.Files[0],"cli"); - estimate.ClientValidationDate = DateTime.Now; + estimate.ClientValidationDate = DateTime.UtcNow; dbContext.SaveChanges(User.GetUserId()); return Ok (new { ClientValidationDate = estimate.ClientValidationDate }); } @@ -177,7 +177,7 @@ namespace Yavsc.ApiControllers { return new ChallengeResult(); } - + var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id); FileInfo fi = new FileInfo(Path.Combine(AbstractFileSystemHelpers.UserBillsDirName, filename)); if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" }); diff --git a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs index 24fa3c28..494075c6 100644 --- a/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/BookQueryApiController.cs @@ -40,7 +40,7 @@ namespace Yavsc.Controllers public IEnumerable GetCommands(long maxId=long.MaxValue) { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - var now = DateTime.Now; + var now = DateTime.UtcNow; var result = _context.RdvQueries.Include(c => c.Location). Include(c => c.Client).Where(c => c.PerformerId == uid && c.Id < maxId && c.EventDate > now diff --git a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs index 9b3ca807..41bdd353 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs @@ -134,7 +134,7 @@ namespace Yavsc.Controllers { return BadRequest(ModelState); } - query.ValidationDate = DateTime.Now; + query.ValidationDate = DateTime.UtcNow; _context.SaveChanges(User.GetUserId()); _context.Entry(query).State = EntityState.Detached; } diff --git a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs index 927c0acc..822c3182 100644 --- a/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs +++ b/src/Yavsc.Api/Controllers/HairCut/HairCutController.cs @@ -44,7 +44,7 @@ namespace Yavsc.ApiControllers var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - var now = DateTime.Now; + var now = DateTime.UtcNow; var result = _context.HairCutQueries .Include(q => q.Prestation) .Include(q => q.Client) diff --git a/src/Yavsc.Api/Controllers/NativeConfidentialController.cs b/src/Yavsc.Api/Controllers/NativeConfidentialController.cs index c8784cbc..01cd8478 100644 --- a/src/Yavsc.Api/Controllers/NativeConfidentialController.cs +++ b/src/Yavsc.Api/Controllers/NativeConfidentialController.cs @@ -40,14 +40,14 @@ public class NativeConfidentialController : Controller _logger.LogError("Invalid model for GCMD"); return new BadRequestObjectResult(ModelState); } - declaration.LatestActivityUpdate = DateTime.Now; + declaration.LatestActivityUpdate = DateTime.UtcNow; _logger.LogInformation($"Registering device with id:{declaration.DeviceId} for {uid}"); DeviceDeclaration? alreadyRegisteredDevice = _context.DeviceDeclaration.FirstOrDefault(d => d.DeviceId == declaration.DeviceId); var deviceAlreadyRegistered = (alreadyRegisteredDevice!=null); if (alreadyRegisteredDevice==null) { - declaration.DeclarationDate = DateTime.Now; + declaration.DeclarationDate = DateTime.UtcNow; declaration.DeviceOwnerId = uid; _context.DeviceDeclaration.Add(declaration); } @@ -59,12 +59,12 @@ public class NativeConfidentialController : Controller _context.Update(alreadyRegisteredDevice); _context.SaveChanges(User.GetUserId()); } - + _context.SaveChanges(User.GetUserId()); - + var latestActivityUpdate = _context.Activities.Max(a=>a.DateModified); - return Json(new { - IsAnUpdate = deviceAlreadyRegistered, + return Json(new { + IsAnUpdate = deviceAlreadyRegistered, UpdateActivities = latestActivityUpdate != declaration.LatestActivityUpdate }); } diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs index 4f32a438..7419c889 100644 --- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs @@ -108,7 +108,7 @@ namespace Yavsc.Controllers UserName = user.UserName, PostsCounter = pc, Balance = user.AccountBalance, - ActiveCommandCount = _dbContext.RdvQueries.Count(x => (x.ClientId == user.Id) && (x.EventDate > DateTime.Now)), + ActiveCommandCount = _dbContext.RdvQueries.Count(x => (x.ClientId == user.Id) && (x.EventDate > DateTime.UtcNow)), HasDedicatedCalendar = !string.IsNullOrEmpty(user.DedicatedGoogleCalendar), Roles = await _userManager.GetRolesAsync(user), PostalAddress = user.PostalAddress?.Address, diff --git a/src/Yavsc.Org/Controllers/Contracting/CommandController.cs b/src/Yavsc.Org/Controllers/Contracting/CommandController.cs index 1a07ff63..7733e1b3 100644 --- a/src/Yavsc.Org/Controllers/Contracting/CommandController.cs +++ b/src/Yavsc.Org/Controllers/Contracting/CommandController.cs @@ -109,7 +109,7 @@ namespace Yavsc.Controllers ViewBag.GoogleSettings = _googleSettings; var userid = User.GetUserId(); var user = _userManager.FindByIdAsync(userid).Result; - return View("Create", new RdvQuery(activityCode, new Location(), DateTime.Now.AddHours(4)) + return View("Create", new RdvQuery(activityCode, new Location(), DateTime.UtcNow.AddHours(4)) { PerformerProfile = pro, PerformerId = pro.PerformerId, diff --git a/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs b/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs index 80e5f80c..2b981c60 100644 --- a/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs +++ b/src/Yavsc.Org/Controllers/Haircut/HairCutCommandController.cs @@ -91,7 +91,7 @@ namespace Yavsc.Controllers // FIXME Assert (command.ValidationDate == null) if (command.ValidationDate == null) { paymentOk = true; - command.ValidationDate = DateTime.Now; + command.ValidationDate = DateTime.UtcNow; } else _logger.LogError ("This Command were yet validated, and is now paied one more ..."); diff --git a/src/Yavsc.Org/Services/ChatHubConnexionManager.cs b/src/Yavsc.Org/Services/ChatHubConnexionManager.cs index bf541a14..43365366 100644 --- a/src/Yavsc.Org/Services/ChatHubConnexionManager.cs +++ b/src/Yavsc.Org/Services/ChatHubConnexionManager.cs @@ -145,7 +145,7 @@ namespace Yavsc.Services if (Channels.TryRemove(roomName, out deadchanInfo)) { var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName); - room.LatestJoinPart = DateTime.Now; + room.LatestJoinPart = DateTime.UtcNow; _dbContext.SaveChanges(); } } diff --git a/src/Yavsc.Org/Services/DiskUsageTracker.cs b/src/Yavsc.Org/Services/DiskUsageTracker.cs index eda3f027..90f3e98a 100644 --- a/src/Yavsc.Org/Services/DiskUsageTracker.cs +++ b/src/Yavsc.Org/Services/DiskUsageTracker.cs @@ -10,7 +10,7 @@ public class DiskUsageTracker : IDiskUsageTracker { public DUTInfo() { - Creation = DateTime.Now; + Creation = DateTime.UtcNow; } public long Usage { get; set; } public long Quota { get; set; } @@ -46,7 +46,7 @@ public class DiskUsageTracker : IDiskUsageTracker if (DiskUsage.Count > ulistLength) { // remove the oldest - var oldestts = DateTime.Now; + var oldestts = DateTime.UtcNow; DUTInfo oinfo = null; string ouname = null; foreach (var diskusage in DiskUsage) diff --git a/src/Yavsc.Org/ViewComponents/CalendarViewComponent.cs b/src/Yavsc.Org/ViewComponents/CalendarViewComponent.cs index 2a9b4213..ed8b7acd 100644 --- a/src/Yavsc.Org/ViewComponents/CalendarViewComponent.cs +++ b/src/Yavsc.Org/ViewComponents/CalendarViewComponent.cs @@ -20,7 +20,7 @@ namespace Yavsc.ViewComponents string htmlFieldName, string calId ) { - var minDate = DateTime.Now; + var minDate = DateTime.UtcNow; var maxDate = minDate.AddDays(20); var model = await _manager.CreateViewModelAsync( @@ -30,6 +30,6 @@ namespace Yavsc.ViewComponents return View(model); } - + } } diff --git a/src/Yavsc.Server/Helpers/RequestHelper.cs b/src/Yavsc.Server/Helpers/RequestHelper.cs index 783d471b..9edbbbd8 100644 --- a/src/Yavsc.Server/Helpers/RequestHelper.cs +++ b/src/Yavsc.Server/Helpers/RequestHelper.cs @@ -17,7 +17,7 @@ namespace Yavsc.Server.Helpers string WRPostMultipart(string url, Dictionary parameters, string authorizationHeader = null) { - string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + string boundary = "---------------------------" + DateTime.UtcNow.Ticks.ToString("x"); byte[] boundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n"); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); @@ -99,7 +99,7 @@ namespace Yavsc.Server.Helpers { var client = new HttpClient(); var formData = new MultipartFormDataContent(); - + if (access_token != null) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token); foreach (var formFile in formFiles) @@ -108,7 +108,7 @@ namespace Yavsc.Server.Helpers if (formFile.ContentType!=null) fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue(formFile.ContentType); else fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - + // fileStreamContent.Headers.ContentDisposition = formFile.ContentDisposition!=null? new ContentDispositionHeaderValue( // formFile.ContentDisposition) : new ContentDispositionHeaderValue("form-data; name=\"file\"; filename=\"" + formFile.Name + "\""); fileStreamContent.Headers.Add("Content-Disposition", formFile.ContentDisposition); diff --git a/src/Yavsc.Server/Hubs/ChatHub.cs b/src/Yavsc.Server/Hubs/ChatHub.cs index ee6f71df..56ffe901 100644 --- a/src/Yavsc.Server/Hubs/ChatHub.cs +++ b/src/Yavsc.Server/Hubs/ChatHub.cs @@ -204,7 +204,7 @@ namespace Yavsc.Server.Hubs // TODO get and require some admin status for current user on this channel newRoom.Topic = channelInfo.Topic; } - newRoom.LatestJoinPart = DateTime.Now; + newRoom.LatestJoinPart = DateTime.UtcNow; _dbContext.ChatRoom.Add(newRoom); _dbContext.SaveChanges(user.Id); diff --git a/src/Yavsc.Server/Models/ApplicationDbContext.cs b/src/Yavsc.Server/Models/ApplicationDbContext.cs index 94076715..8643f3d3 100644 --- a/src/Yavsc.Server/Models/ApplicationDbContext.cs +++ b/src/Yavsc.Server/Models/ApplicationDbContext.cs @@ -299,7 +299,7 @@ namespace Yavsc.Models public DbSet MusicLoverSettings { get; set; } public DbSet CoWorking { get; set; } - private void AddTimestamps(string userId)  + private void AddTimestamps(string userId) { var entities = ChangeTracker.Entries() .Where(x => x.Entity.GetType().GetInterface(nameof(ITrackedEntity)) != null @@ -310,11 +310,11 @@ namespace Yavsc.Models { if (entity.State == EntityState.Added) { - ((ITrackedEntity)entity.Entity).DateCreated = DateTime.Now; + ((ITrackedEntity)entity.Entity).DateCreated = DateTime.UtcNow; ((ITrackedEntity)entity.Entity).UserCreated = userId; } - ((ITrackedEntity)entity.Entity).DateModified = DateTime.Now; + ((ITrackedEntity)entity.Entity).DateModified = DateTime.UtcNow; ((ITrackedEntity)entity.Entity).UserModified = userId; } } From 98613e7070842fa7a7e268648b32f41d43b8c1d9 Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 18:45:27 +0100 Subject: [PATCH 002/151] Blog: enforce Restrict FK on BlogPost.Author and Comment.Author MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tout billet a un auteur, tout commentaire a un auteur. On aligne la base sur ce contrat (Postgres) en droppant les orphelins existants puis en remplaçant les FK en cascade par des FK Restrict. - ApplicationDbContext: fluent pour BlogPost.Author et Comment.Author en DeleteBehavior.Restrict. - ApplicationUser: ajoute la nav inverse BlogComments (manquait, EF aurait sinon créé une shadow FK). - Migration 20260711173717_EnforceBlogAuthorFKs: Up purge les Comment/BlogSpot dont l'AuthorId n'existe plus, log le volume, puis drop+add des FK. Down laisse la cascade (état pré-migration). Le code applicatif (BlogSpotService.Details) s'appuiera sur cette contrainte dans un commit séparé. --- ...711173717_EnforceBlogAuthorFKs.Designer.cs | 4675 +++++++++++++++++ .../20260711173717_EnforceBlogAuthorFKs.cs | 89 + .../ApplicationDbContextModelSnapshot.cs | 9 +- .../Models/ApplicationDbContext.cs | 16 + src/Yavsc.Server/Models/ApplicationUser.cs | 7 + 5 files changed, 4793 insertions(+), 3 deletions(-) create mode 100644 src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs create mode 100644 src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs diff --git a/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs new file mode 100644 index 00000000..4f8c664e --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.Designer.cs @@ -0,0 +1,4675 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260711173717_EnforceBlogAuthorFKs")] + partial class EnforceBlogAuthorFKs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.Property("Comment") + .HasColumnType("boolean"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .HasColumnType("text"); + + b.Property("BIC") + .HasColumnType("text"); + + b.Property("BankCode") + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("WicketCode") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("BlogpostId") + .HasColumnType("bigint"); + + b.HasKey("BlogpostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Forms.Form", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Form"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("AdditionalInfo") + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.ToTable("HairCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("HairMultiCutQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("RdvQueries"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("GitId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.RdvQuery", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "BlogPost") + .WithMany() + .HasForeignKey("BlogpostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Prestation"); + + b.Navigation("Regularization"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId"); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("Location"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs new file mode 100644 index 00000000..b13ef467 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260711173717_EnforceBlogAuthorFKs.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class EnforceBlogAuthorFKs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Assainir les orphelins AVANT d'enforcer la FK Restrict. + // En prod (Postgres), la migration aurait sinon planté + // sur des billets/commentaires dont l'AuthorId pointe + // vers un user déjà supprimé. La logique métier refuse + // désormais l'orphelin (cf. BlogSpotService.Details) — on + // aligne l'état de la base avec ce contrat. + migrationBuilder.Sql(@" + DO $$ + DECLARE n_comments int; + n_posts int; + BEGIN + DELETE FROM ""Comment"" + WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers""); + GET DIAGNOSTICS n_comments = ROW_COUNT; + + DELETE FROM ""BlogSpot"" + WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers""); + GET DIAGNOSTICS n_posts = ROW_COUNT; + + RAISE NOTICE 'EnforceBlogAuthorFKs: % orphaned comments deleted, % orphaned blog posts deleted', + n_comments, n_posts; + END $$; + "); + + migrationBuilder.DropForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot"); + + migrationBuilder.DropForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment"); + + migrationBuilder.AddForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot"); + + migrationBuilder.DropForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment"); + + migrationBuilder.AddForeignKey( + name: "FK_BlogSpot_AspNetUsers_AuthorId", + table: "BlogSpot", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Comment_AspNetUsers_AuthorId", + table: "Comment", + column: "AuthorId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index 398b5a6e..ef96638c 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3803,7 +3803,8 @@ namespace Yavsc.Migrations { b.HasOne("Yavsc.Models.ApplicationUser", "Author") .WithMany("Posts") - .HasForeignKey("AuthorId"); + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); b.Navigation("Author"); }); @@ -3830,9 +3831,9 @@ namespace Yavsc.Migrations modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => { b.HasOne("Yavsc.Models.ApplicationUser", "Author") - .WithMany() + .WithMany("BlogComments") .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("Yavsc.Models.Blog.Comment", "Parent") @@ -4542,6 +4543,8 @@ namespace Yavsc.Migrations b.Navigation("BlackList"); + b.Navigation("BlogComments"); + b.Navigation("Book"); b.Navigation("Circles"); diff --git a/src/Yavsc.Server/Models/ApplicationDbContext.cs b/src/Yavsc.Server/Models/ApplicationDbContext.cs index 8643f3d3..c3404aed 100644 --- a/src/Yavsc.Server/Models/ApplicationDbContext.cs +++ b/src/Yavsc.Server/Models/ApplicationDbContext.cs @@ -214,6 +214,22 @@ namespace Yavsc.Models // Log immuable — pas de update autorisé e.ToTable(tb => tb.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1")); }); + + // ── Blog FK strictness ───────────────────────────────────────────── + // Tout billet a un auteur, tout commentaire a un auteur : pas de + // cascade en suppression d'un user, pas d'orphelin toléré. Le code + // applicatif (BlogSpotService.Details) s'appuie sur cette + // contrainte pour pouvoir assumer l'existence de l'auteur. + builder.Entity() + .HasOne(b => b.Author) + .WithMany(u => u.Posts) + .HasForeignKey(b => b.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + builder.Entity() + .HasOne(c => c.Author) + .WithMany(u => u.BlogComments) + .HasForeignKey(c => c.AuthorId) + .OnDelete(DeleteBehavior.Restrict); } /// diff --git a/src/Yavsc.Server/Models/ApplicationUser.cs b/src/Yavsc.Server/Models/ApplicationUser.cs index fa577c8b..b64bad3e 100644 --- a/src/Yavsc.Server/Models/ApplicationUser.cs +++ b/src/Yavsc.Server/Models/ApplicationUser.cs @@ -114,6 +114,13 @@ namespace Yavsc.Models [InverseProperty("Member")] public virtual List? Membership { get; set; } + /// + /// User's blog comments + /// + [JsonIgnore] + [InverseProperty("Author")] + public virtual List? BlogComments { get; set; } + IAccountBalance? IApplicationUser.AccountBalance => AccountBalance; ILocation? IApplicationUser.PostalAddress { get => PostalAddress; } From bbdcc7f2adb1a1b6303918db3142dc95a0ee0f2a Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 19:41:09 +0100 Subject: [PATCH 003/151] Blog: render user avatar through a null-safe helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /BlogSpot/Details/1 retournait 500 (avec un 500-sur-500 sur la page d'erreur elle-même) parce que DisplayTemplates /ApplicationUser.cshtml faisait `var avuri = "/Avatars/" + Model.UserName + ".s.png"` : avec enable, Razor émet un null-check implicite sur Model.UserName et lève NullReferenceException quand l'auteur n'a pas de UserName posé (donnée héritée, user partiellement initialisé). - UserDisplayHelpers.AvatarSrc : helper statique pur dans Yavsc.Abstract.Identity qui retourne YavscConstants.DefaultAvatar pour user null / UserName vide ou whitespace, et un path /avatars/.s.png sinon. - ApplicationUser.cshtml : utilise le helper. - Tests : 4 cas (null, vide, whitespace, valide) dans Yavsc.Org.Tests/NonRegression. Bonus : le path d'avatar passe de "/Avatars/" (S majuscule, ne résolvait pas dans le middleware de fichiers statiques) à YavscConstants.AvatarsPath ("/avatars" minuscule), pour fermer l'autre trou que centraliser le calcul permettait de fixer proprement. --- .../Identity/UserDisplayHelpers.cs | 36 +++++++++++ .../NonRegression/UserDisplayHelpersTests.cs | 63 +++++++++++++++++++ .../DisplayTemplates/ApplicationUser.cshtml | 6 +- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs create mode 100644 src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs diff --git a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs new file mode 100644 index 00000000..c2700e93 --- /dev/null +++ b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs @@ -0,0 +1,36 @@ +namespace Yavsc.Abstract.Identity +{ + /// + /// Helpers Razor-friendly pour rendre un user dans une vue + /// sans exposer le template à des accesseurs nullables qui + /// lèveraient . + /// + public static class UserDisplayHelpers + { + /// + /// Chemin d'avatar à utiliser pour + /// dans un display template. Défense contre null + /// (user pas chargé, FK orpheline) et contre un + /// UserName vide (donnée héritée, user partiellement + /// initialisé). Sans cette garde, Razor émet une + /// NullReferenceException dès qu'un accesseur .UserName + /// apparaît dans le template, ce qui remonte en 500 et + /// masque la page d'erreur elle-même. + /// + /// + /// Le path retourné est aligné sur + /// (minuscule). + /// Les anciens display templates utilisaient "/Avatars/" + /// avec un S majuscule, en désaccord avec le path statique + /// servi par le middleware de fichiers — les images ne + /// résolvaient pas. Centraliser le calcul ici ferme les + /// deux trous. + /// + public static string AvatarSrc(IApplicationUser? user) + { + if (string.IsNullOrWhiteSpace(user?.UserName)) + return YavscConstants.DefaultAvatar; + return $"{YavscConstants.AvatarsPath}/{user!.UserName}.s.png"; + } + } +} diff --git a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs new file mode 100644 index 00000000..a0249fa4 --- /dev/null +++ b/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs @@ -0,0 +1,63 @@ +using Xunit; +using Yavsc.Abstract; +using Yavsc.Abstract.Identity; + +namespace Yavsc.Org.Tests.NonRegression; + +/// +/// Régression sur le 500 GET /BlogSpot/Details/{id} : un +/// billet dont l'auteur a un UserName null ou absent faisait +/// lever NullReferenceException dans le display template +/// ApplicationUser.cshtml à l'évaluation de +/// Model.UserName. ASP.NET transforme en 500, et la page +/// d'erreur elle-même crash (ErrorViewModel manquant), donc on +/// ne voit rien — juste un 500 muet. +/// +/// Le fix passe par qui +/// retourne pour toute +/// donnée partielle. Ces tests couvrent les trois formes de +/// "donnée absente" : user null, UserName vide, UserName whitespace. +/// +public class UserDisplayHelpersTests +{ + [Fact] + public void AvatarSrc_null_user_returns_default_avatar() + { + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(null)); + } + + [Fact] + public void AvatarSrc_user_with_empty_UserName_returns_default_avatar() + { + var user = new FakeUser { UserName = "" }; + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + } + + [Fact] + public void AvatarSrc_user_with_whitespace_UserName_returns_default_avatar() + { + var user = new FakeUser { UserName = " " }; + Assert.Equal(YavscConstants.DefaultAvatar, UserDisplayHelpers.AvatarSrc(user)); + } + + [Fact] + public void AvatarSrc_valid_user_returns_canonical_avatars_path() + { + var user = new FakeUser { UserName = "alice" }; + // Le path doit matcher YavscConstants.AvatarsPath (minuscule), + // pas un /Avatars/ avec S majuscule qui ne résout pas + // dans le middleware de fichiers statiques. + var expected = $"{YavscConstants.AvatarsPath}/alice.s.png"; + Assert.Equal(expected, UserDisplayHelpers.AvatarSrc(user)); + } + + private sealed class FakeUser : IApplicationUser + { + public string Id { get; set; } = ""; + public string? UserName { get; set; } + public string? Avatar { get; set; } + public IAccountBalance? AccountBalance => null; + public string? DedicatedGoogleCalendar => null; + public ILocation? PostalAddress => null; + } +} diff --git a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml index b7bd532f..2e4ef1bb 100644 --- a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml +++ b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml @@ -1,7 +1,11 @@ @using Yavsc.Abstract.Identity @model ApplicationUser @{ - var avuri = "/Avatars/" + Model.UserName + ".s.png"; + // Le helper défend contre Model null et contre UserName vide + // ou whitespace. Sans cette garde, Razor lève + // NullReferenceException ici, ce qui propage un 500 et + // masque aussi la page d'erreur. + var avuri = UserDisplayHelpers.AvatarSrc(Model); }
From cb7526de9dcbdd5708f1a95d95bda1b5b9f7655d Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 19:59:03 +0100 Subject: [PATCH 004/151] Blog: add test guarding the display-template fix + document test architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le commit 2 a fixé la NPE du /BlogSpot/Details/{id} en passant le display template par UserDisplayHelpers.AvatarSrc, qui défend contre un UserName null. Ce commit complète le filet de non-régression et pose la doc d'architecture des tests. - ApplicationUserDisplayTemplateTests : assert que le cshtml ne concatène plus directement Model.UserName (ancien code fautif) et qu'il utilise bien le helper. Si quelqu'un revert la ligne 4 du cshtml, les tests cassent. Les autres usages de Model.UserName (alt, title, asp-route-id) sont autorisés : ils ne sont pas la cause du 500, juste laids si null. - doc/testing.md : vue d'ensemble de la stratégie de test (conventions NonRegression/Mandatory/Smoke/Controllers, EF in-memory via InMemoryDatabaseRoot partagé, auth stubs, quand ne pas écrire de test). - src/Yavsc.Tests.Shared/README.md : détails du scaffold partagé (WebHostFixture + son cycle de vie et ses hooks, TestAuthPolicyProvider, TestTokenIssuer) et des deux spécialisations dans le repo (Yavsc.Org.Tests.WebServerFixture et Yavsc.Blogs.Tests.BlogsWebServerFixture). - doc/README.md : entrée vers testing.md dans l'index. --- doc/README.md | 1 + doc/testing.md | 88 +++++++++++ .../ApplicationUserDisplayTemplateTests.cs | 69 ++++++++ src/Yavsc.Tests.Shared/README.md | 149 ++++++++++++++++++ 4 files changed, 307 insertions(+) create mode 100644 doc/testing.md create mode 100644 src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs create mode 100644 src/Yavsc.Tests.Shared/README.md diff --git a/doc/README.md b/doc/README.md index b189bb27..912a2e56 100644 --- a/doc/README.md +++ b/doc/README.md @@ -17,6 +17,7 @@ La racine de l'architecture est [Architecture.md](Architecture.md). | [architecture/postit-oidc.md](architecture/postit-oidc.md) | Client desktop PostIt, custom URI scheme, silent refresh | | [architecture/postit.md](architecture/postit.md) | PostIt — topologie des projets, ViewLocator custo, navigation, DI, conventions de binding | | [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md) | Découpage des projets .NET (Abstract, Server, Org, Api, Blogs, Web, Org.Tests) | +| [testing.md](testing.md) | Stratégie de test : conventions des dossiers, EF Core in-memory, auth stubs, scaffold partagé | ## Roadmap & design exploration diff --git a/doc/testing.md b/doc/testing.md new file mode 100644 index 00000000..ef80cda9 --- /dev/null +++ b/doc/testing.md @@ -0,0 +1,88 @@ +# Stratégie de test + +Yavsc utilise **xUnit** (`xunit.v3`) avec un mix d'unitaire pur +et d'intégration légère. Les projets de tests sont sous +`src/.Tests/` et consomment le scaffold partagé +`src/Yavsc.Tests.Shared/`. + +## Vue d'ensemble + +| Sujet | Document | +|---|---| +| Scaffold partagé (`WebHostFixture`, JWT de test, etc.) | [src/Yavsc.Tests.Shared/README.md](../src/Yavsc.Tests.Shared/README.md) | +| Convention des dossiers de tests | [Conventions](#conventions-des-dossiers-de-tests) | +| Driver EF Core en test | [EF Core en test](#ef-core-en-test) | +| Stubs d'authentification et de permissions | [Auth et permissions](#auth-et-permissions) | + +## Conventions des dossiers de tests + +Sous `src/.Tests/`, on trouve quatre dossiers de premier +niveau qui classifient les tests par intention : + +| Dossier | Usage | +|---|---| +| `NonRegression/` | Régressions : un bug constaté, un test qui le détecte si on le réintroduit | +| `Mandatory/` | Tests bloquants : ils doivent passer avant tout merge | +| `Smoke/` | Smoke tests HTTP rapides, montent un host léger | +| `Controllers/` | Tests unitaires des contrôleurs (mock du service, assertions sur le mapping HTTP) | + +Les `NonRegression` sont la cible par défaut quand on fixe un +bug : ils doivent être **rouges avant le fix, verts après**, et +continuer à **casser** si quelqu'un revert le fix. Pas de test +qui passe à vide. + +## EF Core en test + +Pour les tests qui ont besoin d'un `ApplicationDbContext`, on +utilise **`UseInMemoryDatabase`** avec un `InMemoryDatabaseRoot` +partagé au niveau de la fixture. Pas de SQLite, pas de Docker, +pas de mock du contexte : le service testé s'exécute contre +un vrai `DbContext` sur in-memory. + +```csharp +private static readonly InMemoryDatabaseRoot _dbRoot = new(); + +var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("Yavsc.Org.Tests.MyFixture", _dbRoot) + .Options; +``` + +Le `InMemoryDatabaseRoot` partagé est important : sans lui, EF +crée un store indépendant par `DbContext` dans certaines +configurations, et un test qui seed + read sur deux contextes +voit un store vide. Le pattern est documenté dans +`BlogsWebServerFixture` ([src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs](../src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs)). + +> **Limite connue** : le provider in-memory **ignore** les +> `Migration` EF et ne respecte pas les FK **sur les raw +> SQL** (`ExecuteSqlRaw`). Pour tester des contraintes FK, on +> écrit la configuration dans `OnModelCreating` et on s'appuie +> sur le fait qu'EF la respecte à l'`Add`/`SaveChanges`. Pour +> tester des migrations, c'est l'environnement de staging. + +## Auth et permissions + +L'authorization policy provider de prod est swappé contre +`TestAuthPolicyProvider` (dans `Yavsc.Tests.Shared`) par les +fixtures spécialisées. Les tests qui ont besoin qu'un user soit +"Administrator" envoient un header `X-Test-Rôle` ; ceux qui +veulent un user anonyme omettent le header. + +Pour les tests unitaires qui n'ont pas besoin du pipeline +HTTP, on stub `IAuthorizationService` directement (cf. +`BlogspotController` dans `Yavsc.Org.Tests/NonRegression/`) +pour éviter de monter un host complet. + +## Quand ne PAS écrire de test + +Un test qui ne détecte rien n'est pas un test. Si l'invariant +qu'on cherche à protéger est déjà enforced par EF, par le +compilateur, ou par une couche applicative en amont, le test +est du bruit. Mieux vaut : +- Un test qui assert un **comportement observable** (code + retour HTTP, exception typée, valeur de retour) +- Ou pas de test, et une note dans le code + +La non-régression se prouve par un test qui casse si on +réintroduit le bug. Pas par un test qui passe aujourd'hui et +qui continuera à passer après un revert. diff --git a/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs b/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs new file mode 100644 index 00000000..9fed6a15 --- /dev/null +++ b/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs @@ -0,0 +1,69 @@ +using System.IO; +using Xunit; + +namespace Yavsc.Org.Tests.NonRegression; + +/// +/// Régression du 500 sur GET /BlogSpot/Details/{id} (auteur +/// sans UserName) : le display template +/// ApplicationUser.cshtml ne doit plus accéder à +/// Model.UserName directement. Toute lecture passe par +/// UserDisplayHelpers.AvatarSrc, qui défend contre +/// null et contre les chaînes vides/whitespace. +/// +/// On ne compile pas la vue Razor ici (coût de mise en place +/// disproportionné pour un seul display template) ; on asserte +/// statiquement que le cshtml ne porte plus l'accès fautif. Si +/// quelqu'un revert la ligne, ce test casse. +/// +public class ApplicationUserDisplayTemplateTests +{ + [Fact] + public void ApplicationUser_cshtml_does_not_construct_avatar_path_from_Model_UserName() + { + // L'invariant qu'on protège : l'URL d'avatar ne doit plus + // être construite à partir de Model.UserName direct (le + // commit 2 du fix). Cette construction était la cause du + // 500 sur GET /BlogSpot/Details/{id} : avec + // enable, Razor émet un null-check + // implicite sur l'expression, et lève NPE si UserName est + // null. Le helper AvatarSrc défend contre ce cas. + // + // On n'interdit pas les autres usages de Model.UserName + // (alt, title, asp-route-id) : Razor les rend en chaîne + // vide si null, sans NPE. C'est laid, pas cassé. + var path = ResolveTemplatePath(); + var content = File.ReadAllText(path); + + // L'ancien code fautif concaténait directement + // "/Avatars/" + Model.UserName + ".s.png". + Assert.DoesNotContain("Model.UserName + ", content); + Assert.DoesNotContain("Model.UserName+", content); + } + + [Fact] + public void ApplicationUser_cshtml_uses_the_null_safe_helper_for_avatar() + { + var path = ResolveTemplatePath(); + var content = File.ReadAllText(path); + + Assert.Contains("UserDisplayHelpers.AvatarSrc", content); + } + + private static string ResolveTemplatePath() + { + // Le test s'exécute depuis src/Yavsc.Org.Tests/bin/..., + // on remonte pour trouver la vue source. + var dir = AppContext.BaseDirectory; + for (var i = 0; i < 8 && dir is not null; i++) + { + var candidate = Path.Combine(dir, + "src", "Yavsc.Org", "Views", "Shared", + "DisplayTemplates", "ApplicationUser.cshtml"); + if (File.Exists(candidate)) return candidate; + dir = Path.GetDirectoryName(dir); + } + throw new FileNotFoundException( + "Could not locate ApplicationUser.cshtml from " + AppContext.BaseDirectory); + } +} diff --git a/src/Yavsc.Tests.Shared/README.md b/src/Yavsc.Tests.Shared/README.md new file mode 100644 index 00000000..2bef8fc9 --- /dev/null +++ b/src/Yavsc.Tests.Shared/README.md @@ -0,0 +1,149 @@ +# Yavsc.Tests.Shared + +Scaffold partagé pour les tests d'intégration ASP.NET Core de +Yavsc. Ce projet **n'est pas lui-même un projet de tests** — il +n'a pas xUnit ni de test runner. Il expose des fixtures +réutilisables que les projets de tests consommateurs +(`Yavsc.Org.Tests`, `Yavsc.Blogs.Tests`, etc.) héritent ou +instancient. + +## Contenu + +| Fichier | Rôle | +|---|---| +| `WebHostFixture.cs` | Base abstraite : Kestrel HTTPS, certificat auto-signé, port dynamique, host partagé inter-fixtures | +| `TestAuthPolicyProvider.cs` | `IAuthorizationPolicyProvider` de test, lit `X-Test-Role` au lieu d'interroger la DB | +| `TestTokenIssuer.cs` | Émet des JWT HS256 signés avec une clé statique, pour les tests d'API qui montent un `AddJwtBearer` réel | + +## WebHostFixture + +`WebHostFixture` est la base de toute fixture d'intégration. +Une seule instance de `WebApplication` tourne par process ; les +fixtures qui héritent partagent le host. Kestrel est bindé sur +`127.0.0.1:0` (port dynamique) avec un certificat auto-signé +généré lazily. + +### Cycle de vie + +- **Premier ctor** d'une fixture concrète → `InitializeAsync()` + lance `BuildApp(builder)` puis `ConfigurePipelineAsync(app)` + puis `app.StartAsync()`. L'`IServerAddressesFeature` est lu + pour peupler `Addresses`. +- **Ctors suivants** (xUnit instancie une fixture par + `IClassFixture`) → reprise de l'état partagé via + `CopySpecialisedSharedState()` (vide par défaut, surchargeable). +- **Dernier `Dispose`** → `app.StopAsync()`, reset des slots + statiques. + +### Hooks à surcharger + +| Hook | Quand | Quoi y mettre | +|---|---|---| +| `BuildApp(builder)` | Toujours | Enregistrement des services, configuration in-memory, seeding éventuel | +| `ConfigurePipelineAsync(app)` | Optionnel | Pipeline middleware spécifique (sinon : pas de pipeline custom) | +| `CopySpecialisedSharedState()` | Optionnel | Recopie des slots statiques de la spécialisation sur les propriétés d'instance | + +### Exemple : fixture de portée minimale + +```csharp +public sealed class MyFixture : WebHostFixture +{ + protected override WebApplication BuildApp(WebApplicationBuilder builder) + { + // In-memory config, services, etc. + return builder.Build(); + } +} +``` + +`MyFixture` n'a pas de test runner propre ; c'est l'assembly +consommateur (par exemple `Yavsc.MyModule.Tests`) qui déclare +les `[Fact]` et utilise `IClassFixture`. + +## Spécifications : fixtures concrètes + +Deux fixtures héritent de `WebHostFixture` dans le repo : + +### Yavsc.Org.Tests.WebServerFixture + +Pour le host principal de Yavsc.Org. Caractéristiques : + +- Configure `InMemory` pour la `ConnectionStrings` Yavsc +- Remplace `IAuthorizationPolicyProvider` par + `TestAuthPolicyProvider` **avant** `ConfigureWebAppServices` + (qui freeze la collection de services) +- Stub `ISmtpClientFactory` par `RecordingSmtpClientFactory` + pour capturer les envois sans SMTP réel +- Seed IdentityServer8 : un `Client` + une `ApiScope` "test" + + un `ApplicationUser` "Tester" +- Configure le pipeline via `app.ConfigurePipeline(...)` avec + un manifeste de static assets explicite (le MSBuild target + `CopyYavscOrgStaticAssets` du csproj miroir les manifests + Yavsc.Org sous le nom Yavsc.Org.Tests.* dans le bin de test) + +Cf. `src/Yavsc.Org.Tests/WebServerFixture.cs`. + +### Yavsc.Blogs.Tests.BlogsWebServerFixture + +Pour le host API de Yavsc.Blogs. Caractéristiques : + +- `UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot)` — + un `InMemoryDatabaseRoot` partagé pour que POST + GET voient + le même store +- `BlogSpotService` réel (pas de mock) +- `PermissionHandler` réel (le handler d'authorization qui + résout `IsOwner(user, blog)`) +- `AddJwtBearer` réel avec HS256, validation contre + `TestTokenIssuer.SigningKey` — pas d'OIDC discovery, pas + d'IdP +- Politique `BlogScope` verbatim (`RequireAuthenticatedUser` + + `RequireClaim("scope", "blogs")`) + +Cf. `src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs`. + +## TestAuthPolicyProvider + +`IAuthorizationPolicyProvider` de test qui lit le rôle dans +l'en-tête HTTP `X-Test-Role` au lieu d'interroger la +`UserManager`. Permet aux smoke tests d'exercer `[Authorize +("AdministratorOnly")]` sans seed de rôle réel. + +Activation : enregistré par les fixtures spécialisées **avant** +`ConfigureWebAppServices` (qui call `builder.Build()` et +fige la collection). La sémantique last-write-wins du +`AddSingleton` fait que le test provider prend le pas. + +## TestTokenIssuer + +Émet un JWT HS256 avec une `SigningKey` statique, exposé en +`TestTokenIssuer.SigningKey` (et `Issuer`). Les fixtures qui +montent un `AddJwtBearer` réutilisent cette clé pour valider +les tokens localement, sans OIDC discovery. + +Helpers : + +- `TestTokenIssuer.Issue(subject, scope, lifetime)` → + chaîne `"Bearer "` prête pour un header HTTP +- `TestTokenIssuer.SigningKey` — `SymmetricSecurityKey` à + passer au `TokenValidationParameters` du `AddJwtBearer` + +## Tests statiques sur du code compilé + +Pour tester un display template Razor sans monter un host +ASP.NET, on peut s'appuyer sur la lecture du fichier source +et asserter des invariants syntaxiques. Cf. +`Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests` +pour un exemple : on asserte que le cshtml ne porte plus +`Model.UserName` directement, ce qui aurait rouvert la +non-régression du 500 sur `/BlogSpot/Details/{id}`. + +C'est pragmatique : la mise en place d'un `RazorProjectEngine` +pour compiler et rendre une vue hors host coûte plus cher que +ce qu'elle protège pour un seul template. + +## Pour aller plus loin + +- `doc/testing.md` à la racine : vue d'ensemble de la + stratégie de test +- `src/Yavsc.Org.Tests/NonRegression/` et + `src/Yavsc.Blogs.Tests/` : exemples d'utilisation From 7a066707b3ec1ab411e194af32998a9988dd3f9e Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 11 Jul 2026 20:50:39 +0100 Subject: [PATCH 005/151] Tests: route Yavsc.Org test host through Testing environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestWebApplicationFactory used ASPNETCORE_ENVIRONMENT=Development, which caused Program.Main's AddConfiguration("org") to load the tracked appsettings-org.json (the reference file with the '*** via dotnet user-secrets ou variable d'environnement ***' placeholder connection string). Npgsql then failed to parse that placeholder during host startup, failing six integration tests (observed 2026-07-11: System.ArgumentException on NpgsqlConnectionStringBuilder.set_Item). Switching the test host to a dedicated Testing environment makes AddConfiguration("org") pick up the new optional appsettings-org.Testing.json file as the last source in the chain (JSON → env vars), which overrides YavscConnection with the InMemory marker and the Smtp section with the test stub values. The .gitignore exception whitelists this file explicitly: it is a configuration source for the test host, not a secrets file. The WebServerFixture path is unchanged — it owns its WebApplicationBuilder and adds the same in-memory override via its BuildApp hook. --- .gitignore | 9 +++++++++ src/Yavsc.Org.Tests/TestWebApplicationFactory.cs | 13 +++++++++---- src/Yavsc.Org/appsettings-org.Testing.json | 11 +++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/Yavsc.Org/appsettings-org.Testing.json diff --git a/.gitignore b/.gitignore index fb843f96..a94475e3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,15 @@ data/ appsettings.*.json appsettings-*.*.json +# Exception: the Testing-environment override for Yavsc.Org is a tracked +# configuration source, not a secrets file. TestWebApplicationFactory +# (Yavsc.Org.Tests) flips ASPNETCORE_ENVIRONMENT to "Testing" so +# AddConfiguration("org") in Program.Main loads this file as the +# last in the chain (it is optional). It overrides the connection +# string and SMTP section for the in-memory test host and contains +# no production secrets. +!src/Yavsc.Org/appsettings-org.Testing.json + generated/ *.tmp DataDir/ diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs index c51f3f8e..dfd6edea 100644 --- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -26,10 +26,15 @@ public class TestWebApplicationFactory : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { - // UseDevelopmentEnvironment triggers the dev signing credential - // path in the production startup, so we don't need a real cert - // to satisfy IdentityServer at boot. - builder.UseEnvironment("Development"); + // UseEnvironment("Testing") puts the host in a dedicated + // configuration environment so AddConfiguration("org") in + // Program.Main loads the optional appsettings-org.Testing.json + // file (which overrides the connection string and SMTP section + // for the test host). See that file for the values. + // We don't use "Development" because that environment is also + // used by the dev launcher and would change the signing + // credential path in IdentityServer; "Testing" is unambiguous. + builder.UseEnvironment("Testing"); builder.ConfigureTestServices(services => { diff --git a/src/Yavsc.Org/appsettings-org.Testing.json b/src/Yavsc.Org/appsettings-org.Testing.json new file mode 100644 index 00000000..c640ffcd --- /dev/null +++ b/src/Yavsc.Org/appsettings-org.Testing.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "YavscConnection": "InMemory" + }, + "Smtp": { + "Host": "smtp.test.local", + "Port": 465, + "UserName": "test-user", + "Password": "test-pass" + } +} From fa7794b7a01c41588e54fc4395777b118940deab Mon Sep 17 00:00:00 2001 From: Lum Date: Sat, 11 Jul 2026 21:56:25 +0100 Subject: [PATCH 006/151] repoduces the bug --- Directory.Packages.props | 1 + src/Yavsc.Blogs.Tests/BlogApiTests.cs | 9 +-- .../BlogsWebServerFixture.cs | 32 ++++++--- .../Yavsc.Blogs.Tests.csproj | 1 + .../TestWebApplicationFactory.cs | 69 +++++++++++++++++++ src/Yavsc.Org.Tests/WebServerFixture.cs | 15 +++- src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj | 1 + src/Yavsc.Org/Extensions/HostingExtensions.cs | 56 +++++++++++++-- src/Yavsc.Org/Yavsc.Org.csproj | 1 + src/Yavsc.Org/appsettings-org.Testing.json | 2 +- 10 files changed, 169 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e4b09159..84380e44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/src/Yavsc.Blogs.Tests/BlogApiTests.cs b/src/Yavsc.Blogs.Tests/BlogApiTests.cs index fd64555a..4ac14c50 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiTests.cs @@ -30,10 +30,11 @@ public sealed class BlogApiTests : IClassFixture } /// Reset the in-memory database to a known empty state. - /// UseInMemoryDatabase shares its store across the - /// lifetime of the instance, - /// so without a per-test reset the test order would leak - /// state between tests. + /// The fixture now uses SQLite in-memory (see forgejo#3), which + /// shares its store across the lifetime of the + /// instance, so without a + /// per-test reset the test order would leak state between + /// tests.
private void ResetDatabase() { using var scope = _fixture.Services.CreateScope(); diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs index 6e39fd00..a76a37a7 100644 --- a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs @@ -48,6 +48,19 @@ namespace Yavsc.Blogs.Tests; /// public sealed class BlogsWebServerFixture : WebHostFixture { + // SQLite in-memory database is created once and shared across all + // DbContext instances for the test lifetime. The connection must + // stay open: closing it destroys the in-memory database. The + // Microsoft.Data.Sqlite pool will then open additional connections + // to the same in-memory store, as long as the original connection + // is alive. This is the SQLite equivalent of the EF Core + // InMemoryDatabaseRoot we used to use. + private Microsoft.Data.Sqlite.SqliteConnection? _sharedSqliteConnection; + // Legacy field kept to make the migration diff readable. The + // InMemory provider path is no longer used by this fixture, but + // removing it is out of scope for the SQLite-in-memory migration + // (forgejo#3 follow-up). + [System.Obsolete("Replaced by SQLite in-memory (forgejo#3).")] private InMemoryDatabaseRoot? _inMemoryRoot; protected override WebApplication BuildApp(WebApplicationBuilder builder) @@ -58,15 +71,18 @@ public sealed class BlogsWebServerFixture : WebHostFixture // against an empty table returns an empty list, which is // exactly what the first test wants to assert. // - // Share a single InMemoryDatabaseRoot across the test - // lifetime so POST + GET on the same fixture see the same - // store. Without the root, EF Core's In-Memory provider - // creates independent stores per DbContext in some - // configurations, and the second request would see an - // empty list even after the first wrote a row. - _inMemoryRoot = new InMemoryDatabaseRoot(); + // We use SQLite in-memory (not the EF Core InMemory provider) + // because the InMemory provider cannot materialise navigation + // properties from IdentityServer8 entity types (see forgejo#3). + // SQLite in-memory is a transient, file-less store that + // executes real SQL, so navigation properties work as + // expected. The shared SqliteConnection keeps the database + // alive for the test lifetime, mirroring the + // InMemoryDatabaseRoot pattern we used previously. + _sharedSqliteConnection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); + _sharedSqliteConnection.Open(); builder.Services.AddDbContext(opt => - opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot)); + opt.UseSqlite(_sharedSqliteConnection)); // Trivial file-system auth: the GET index path never calls // into it, but the DI container needs an instance. diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index ec1f7f0a..831097b0 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs index dfd6edea..af40bb78 100644 --- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -2,7 +2,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Yavsc.Models; using Yavsc.Tests.Shared; namespace Yavsc.Org.Tests; @@ -24,6 +28,21 @@ namespace Yavsc.Org.Tests; /// public class TestWebApplicationFactory : WebApplicationFactory { + // SQLite in-memory: the connection must stay open for the lifetime + // of the host, otherwise the in-memory database is destroyed and + // every new DbContext sees an empty store. We hold the connection + // here so it is disposed only when the factory is disposed. The + // Microsoft.Data.Sqlite pool reuses the underlying in-memory store + // across additional connections opened against the same connection + // string, as long as the original connection is alive. This is the + // SQLite equivalent of the EF Core InMemoryDatabaseRoot pattern. + private readonly SqliteConnection _sharedSqliteConnection = new("Data Source=:memory:"); + + public TestWebApplicationFactory() + { + _sharedSqliteConnection.Open(); + } + protected override void ConfigureWebHost(IWebHostBuilder builder) { // UseEnvironment("Testing") puts the host in a dedicated @@ -38,6 +57,25 @@ public class TestWebApplicationFactory : WebApplicationFactory builder.ConfigureTestServices(services => { + // The production Program.Main calls AddConfiguration("org") + // and then AddIdentityDBAndStores which calls + // GetConnectionString("YavscConnection"). The result is + // "Data Source=:memory:" (from appsettings-org.Testing.json), + // and the production code path in HostingExtensions routes + // that to UseSqlite. However, the EF Core in-memory test + // pattern needs all DbContext instances to see the same + // store; with a raw "Data Source=:memory:" connection string, + // each connection opens its own private database. We + // therefore drop the production DbContext registration and + // re-register ApplicationDbContext with the shared + // SqliteConnection held by this factory. Tests that need + // the schema to exist call EnsureCreated on the resulting + // DbContext (e.g. ClientControllerCollectionTests seeds a + // Client row in its constructor). + services.RemoveAll>(); + services.AddDbContext(opt => + opt.UseSqlite(_sharedSqliteConnection)); + // Replace the production IAuthorizationPolicyProvider with // the test one. The default registered by AddAuthorization // becomes irrelevant: any GetPolicyAsync call is routed here. @@ -48,6 +86,37 @@ public class TestWebApplicationFactory : WebApplicationFactory // TestUserMiddleware runs after UseAuthentication/Authorization. services.AddTransient(); services.AddTransient(); + + // Run EnsureCreated once at host start. With SQLite in-memory + // and a shared connection, this creates the schema once + // and the schema persists for the host lifetime. The + // test code (e.g. ClientControllerCollectionTests seed) can + // then write rows without having to call EnsureCreated + // itself. EnsureCreated is idempotent: re-running it on + // an existing schema is a no-op. + services.AddHostedService(); }); } + + private sealed class SqliteEnsureCreatedHostedService : IHostedService + { + private readonly IServiceProvider _services; + public SqliteEnsureCreatedHostedService(IServiceProvider services) + { + _services = services; + } + public Task StartAsync(CancellationToken cancellationToken) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return db.Database.EnsureCreatedAsync(cancellationToken); + } + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (disposing) _sharedSqliteConnection.Dispose(); + base.Dispose(disposing); + } } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index ed0bd69d..ebb50fc7 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -75,7 +75,20 @@ public sealed class WebServerFixture : WebHostFixture // that plus the in-memory overrides below. builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", + // The EF Core in-memory provider cannot materialise + // entity types from IdentityServer8 (see forgejo#3): + // it crashes with IndexOutOfRangeException on the + // multi-Include query in ClientController.LoadClientAsync + // and on per-collection LoadAsync. SQLite in-memory is + // a transient, file-less store that uses the same + // connection string semantics as the InMemory provider + // ("keep the connection open for the host lifetime") + // but actually executes SQL, so it handles + // navigation-property entities correctly. The + // HostingExtensions code path detects this connection + // string and routes to UseSqlite. See + // doc/testing.md for the test-driver policy. + [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "Data Source=:memory:", // SMTP test config: UserName non-null so MailSender // exercises the Authenticate branch — the // RecordingSmtpClient captures it. diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index 79a6bae1..5d049626 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -53,6 +53,7 @@ + + Redirection en cours + Vous êtes maintenant authentifié, et vous devriez pouvoir fermer cette page. + diff --git a/src/Yavsc.Org/Views/Shared/Redirect.cshtml b/src/Yavsc.Org/Views/Shared/Redirect.cshtml index b1aa8b72..62aee641 100644 --- a/src/Yavsc.Org/Views/Shared/Redirect.cshtml +++ b/src/Yavsc.Org/Views/Shared/Redirect.cshtml @@ -3,9 +3,9 @@ - @Localizer["Redirecting"] + @Localizer["Redirecting-Title"] -

@Localizer["Redirecting to"] @Model.RedirectUrl

+ @Localizer["Redirecting-Message"] - \ No newline at end of file + From eba44b46e2e0c9cc04d2a218bab1f3dca630d266 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 15:51:55 +0100 Subject: [PATCH 024/151] postit: allow self-signed OIDC TLS in Development --- src/PostIt/PostIt/ViewModels/Settings.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 5bd3a844..9d556712 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Text.Json; using System.Threading; @@ -182,6 +183,16 @@ public partial class Settings : ViewModelBase // PKCE is enabled by default when no client_secret is provided. }; + if (IsDevelopmentEnvironment()) + { + // Dev only: allow local/self-signed TLS for discovery/token + // endpoints when the machine does not trust a custom root. + options.BackchannelHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + } + if (browser is not null) options.Browser = browser; @@ -231,6 +242,14 @@ public partial class Settings : ViewModelBase } } + private static bool IsDevelopmentEnvironment() + { + return string.Equals( + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), + "Development", + StringComparison.OrdinalIgnoreCase); + } + internal void Load() { if (Loaded) return; From 13964b9f7f2de228aac7966d1bf8d976eea78d83 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 16:07:28 +0100 Subject: [PATCH 025/151] org: show full error details in development --- src/Yavsc.Org/Views/Shared/Error.cshtml | 39 +++++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Yavsc.Org/Views/Shared/Error.cshtml b/src/Yavsc.Org/Views/Shared/Error.cshtml index ed29d328..49c7539f 100644 --- a/src/Yavsc.Org/Views/Shared/Error.cshtml +++ b/src/Yavsc.Org/Views/Shared/Error.cshtml @@ -1,14 +1,41 @@ -@model ErrorViewModel +@using Microsoft.AspNetCore.Hosting +@using Yavsc.Models +@inject IWebHostEnvironment Env +@model object @{ ViewBag.Title = "Error"; }

Error.

-

An error occurred while processing your request.

-@if (Model!=null) if (Model.ShowRequestId) +@if (Env.IsDevelopment()) { -

- Request ID: @Model.RequestId -

+

An unhandled exception occurred while processing your request.

+ if (Model is Exception exception) + { +
@exception.ToString()
+ } + else if (Model is ErrorViewModel errorViewModel && !string.IsNullOrWhiteSpace(errorViewModel.Description)) + { +
@errorViewModel.Description
+ } +} +else +{ +

An error occurred while processing your request.

+ + if (Model is ErrorViewModel errorViewModel) + { + if (errorViewModel.ShowRequestId) + { +

+ Request ID: @errorViewModel.RequestId +

+ } + + if (!string.IsNullOrWhiteSpace(errorViewModel.Description)) + { +

@errorViewModel.Description

+ } + } } From 3d80a3f2a17af9aa0ffca960c478b50ae02e95e2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 16:14:37 +0100 Subject: [PATCH 026/151] Post logout redirect uri --- .vscode/settings.json | 1 + src/PostIt/PostIt/ViewModels/Settings.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e22cb5e..dc0a5a77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,7 @@ "envsubst", "Newtonsoft", "Npgsql", + "PKCE", "postit", "pschneider", "SLNDIR", diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 9d556712..890ca15c 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -179,7 +179,7 @@ public partial class Settings : ViewModelBase RedirectUri = Authentication.RedirectUri, Scope = string.Join(' ', MergeScopes(this.Authentication.Scopes)), TokenClientCredentialStyle = IdentityModel.Client.ClientCredentialStyle.PostBody, - PostLogoutRedirectUri = "https//yavsc.pschneider.fr", + PostLogoutRedirectUri = Authentication.Authority, // PKCE is enabled by default when no client_secret is provided. }; From 9622dbeaf2c901585364ed276f2cd33d80cb15da Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 16:26:43 +0100 Subject: [PATCH 027/151] Hsts --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index dc0a5a77..16bbe483 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "DOTNET", "ecdsa", "envsubst", + "Hsts", "Newtonsoft", "Npgsql", "PKCE", From fda43ba2d1a0fbcb1fde803a0d87aceecb3530ce Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 16:27:13 +0100 Subject: [PATCH 028/151] Hsts --- src/Yavsc.Org/Extensions/HostingExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index a494a05b..74a1a53e 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -972,7 +972,8 @@ public static class HostingExtensions else { app.UseExceptionHandler("/Home/Error"); - logger.LogInformation("Running in production mode. Ensure the database is migrated."); + app.UseHsts(); + logger.LogInformation("⨝ Running in production mode. Ensure the database is migrated."); await app.MigrateDatabaseAsync(); } From 786016344b0a63b2f105e14cb96bf5bd3b25055a Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 12 Jul 2026 17:56:23 +0100 Subject: [PATCH 029/151] refacto error handling --- .../Accounting/AccountController.cs | 10 ++-- .../Accounting/ManageController.cs | 14 ----- .../Controllers/Consent/ConsentController.cs | 17 +++--- .../Controllers/Device/DeviceController.cs | 20 ++++++- src/Yavsc.Org/Controllers/HomeController.cs | 56 +++++++++++++++---- src/Yavsc.Org/Extensions/HostingExtensions.cs | 1 - src/Yavsc.Org/Helpers/ErrorViewHelpers.cs | 52 +++++++++++++++++ src/Yavsc.Server/Models/ErrorViewModel.cs | 1 + 8 files changed, 126 insertions(+), 45 deletions(-) create mode 100644 src/Yavsc.Org/Helpers/ErrorViewHelpers.cs diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index e8a63163..43565442 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -791,12 +791,12 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { if (userId == null || code == null) { - return View("Error"); + return this.ErrorView("Error: userId or code is null."); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { - return View("Error"); + return this.ErrorView("Error: user not found."); } IdentityResult result = null; try @@ -819,12 +819,12 @@ IHtmlLocalizerFactory htmlLocalizerFactory, { if (userId == null || code == null) { - return View("Error"); + return this.ErrorView("Error: userId or code is null."); } var user = await _userManager.FindByIdAsync(userId); if (user == null) { - return View("Error"); + return this.ErrorView("Error: user not found."); } bool result = false; try @@ -837,7 +837,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, _logger.LogError(ex.StackTrace); _logger.LogError(ex.Message); } - return View(result ? "EmailConfirmed" : "Error"); + return result ? View("EmailConfirmed") : this.ErrorView("Error confirming two factor token."); } // diff --git a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs index 7419c889..43e11cd2 100644 --- a/src/Yavsc.Org/Controllers/Accounting/ManageController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/ManageController.cs @@ -1,6 +1,5 @@ using System.Security.Claims; -using System.IO; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; @@ -490,12 +489,7 @@ namespace Yavsc.Controllers : message == ManageMessageId.Error ? "An error has occurred." : ""; var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } var userLogins = await _userManager.GetLoginsAsync(user); - ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1; return View(new ManageLoginsViewModel @@ -522,15 +516,7 @@ namespace Yavsc.Controllers public async Task LinkLoginCallback() { var user = await GetCurrentUserAsync(); - if (user == null) - { - return View("Error"); - } var info = await _signInManager.GetExternalLoginInfoAsync(User.GetUserId()); - if (info == null) - { - return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); - } var result = await _userManager.AddLoginAsync(user, info); var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; return RedirectToAction(nameof(ManageLogins), new { Message = message }); diff --git a/src/Yavsc.Org/Controllers/Consent/ConsentController.cs b/src/Yavsc.Org/Controllers/Consent/ConsentController.cs index 15c4a93b..b7b6eff8 100644 --- a/src/Yavsc.Org/Controllers/Consent/ConsentController.cs +++ b/src/Yavsc.Org/Controllers/Consent/ConsentController.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System; using Yavsc; using Yavsc.Extensions; +using Yavsc.Models; namespace IdentityServerHost.Quickstart.UI { @@ -53,10 +54,11 @@ namespace IdentityServerHost.Quickstart.UI { return View("Index", vm); } - - return View("Error"); + return this.ErrorView("No consent request matching request: " + returnUrl); } + + /// /// Handles the consent screen postback /// @@ -88,8 +90,8 @@ namespace IdentityServerHost.Quickstart.UI { return View("Index", result.ViewModel); } - - return View("Error"); + return this.ErrorView($"ReturnUrl: {model}, result: {result}" ); + } /*****************************************/ @@ -170,11 +172,6 @@ namespace IdentityServerHost.Quickstart.UI { return CreateConsentViewModel(model, returnUrl, request); } - else - { - _logger.LogError("No consent request matching request: {0}", returnUrl); - } - return null; } @@ -199,7 +196,7 @@ namespace IdentityServerHost.Quickstart.UI vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); var apiScopes = new List(); - foreach(var parsedScope in request.ValidatedResources.ParsedScopes) + foreach (var parsedScope in request.ValidatedResources.ParsedScopes) { var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) diff --git a/src/Yavsc.Org/Controllers/Device/DeviceController.cs b/src/Yavsc.Org/Controllers/Device/DeviceController.cs index 5e0c7780..2e516aa6 100644 --- a/src/Yavsc.Org/Controllers/Device/DeviceController.cs +++ b/src/Yavsc.Org/Controllers/Device/DeviceController.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Yavsc.Models; using Yavsc.Models.Access; namespace Yavsc.Controllers @@ -49,7 +50,7 @@ namespace Yavsc.Controllers if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); var vm = await BuildViewModelAsync(userCode); - if (vm == null) return View("Error"); + if (vm == null) return this.ErrorView($"ViewModel is null! userCodeParamName: {userCodeParamName}, userCode: {userCode}" );; vm.ConfirmUserCode = true; return View("UserCodeConfirmation", vm); @@ -60,7 +61,7 @@ namespace Yavsc.Controllers public async Task UserCodeCapture(string userCode) { var vm = await BuildViewModelAsync(userCode); - if (vm == null) return View("Error"); + if (vm == null) return this.ErrorView($"UserCodeCapture: ViewModel is null! userCode: {userCode}" ); return View("UserCodeConfirmation", vm); } @@ -72,7 +73,20 @@ namespace Yavsc.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); var result = await ProcessConsent(model); - if (result.HasValidationError) return View("Error"); + if (result.HasValidationError) + { + if (HttpContext.RequestServices.GetRequiredService().IsDevelopment()) + { + throw new InvalidOperationException("Device Authorization Input validation error: " + result.ValidationError); + } + + return View("Error", + new ErrorViewModel { + RequestId = HttpContext.TraceIdentifier, + Description = "Device Authorization Input validation error: " + result.ValidationError + } + ); + } return View("Success"); } diff --git a/src/Yavsc.Org/Controllers/HomeController.cs b/src/Yavsc.Org/Controllers/HomeController.cs index 2602a1a3..67c19fed 100644 --- a/src/Yavsc.Org/Controllers/HomeController.cs +++ b/src/Yavsc.Org/Controllers/HomeController.cs @@ -15,18 +15,24 @@ namespace Yavsc.Controllers public class HomeController : Controller { readonly ApplicationDbContext _dbContext; - + readonly ILogger _logger; + private readonly bool _isDevelopment; readonly IHtmlLocalizer _localizer; private SiteSettings siteSettings; public HomeController(ILogger logger, IHtmlLocalizer localizer, ApplicationDbContext context, - IOptions settingsOptions) + IOptions settingsOptions, + IWebHostEnvironment env + ) { _localizer = localizer; _dbContext = context; siteSettings = settingsOptions.Value; + _logger = logger; + _isDevelopment = env.IsDevelopment(); + } public async Task Index(string id) @@ -99,18 +105,44 @@ namespace Yavsc.Controllers public IActionResult Error() { - var feature = this.HttpContext.Features.Get(); - if (feature == null) return View(); - var errorType = feature?.Error; - if (errorType == null) return View(); - if (errorType is NotSupportedException notSupported) + if (_isDevelopment) { - return View(new ErrorViewModel { - Description = notSupported.Message, - RequestId = this.HttpContext.TraceIdentifier - }); + _logger.LogInformation( + "Home/Error requested in Development. This endpoint is disabled because DeveloperExceptionPage should handle unhandled exceptions."); + + return NotFound( + "In Development, /Home/Error is disabled. Unhandled exceptions are rendered by DeveloperExceptionPage."); } - return View("~/Views/Shared/Error.cshtml", feature?.Error); + + var errorViewModel = new ErrorViewModel + { + RequestId = HttpContext.TraceIdentifier + }; + + var exceptionHandlerPathFeature = + HttpContext.Features.Get(); + + if (exceptionHandlerPathFeature is null) + { + _logger.LogWarning( + "Home/Error called without IExceptionHandlerPathFeature in non-development environment."); + + return View("~/Views/Shared/Error.cshtml", errorViewModel); + } + + if (exceptionHandlerPathFeature?.Error is FileNotFoundException) + { + errorViewModel.Description = "The file was not found."; + } + + if (exceptionHandlerPathFeature?.Path == "/") + { + errorViewModel.Description ??= string.Empty; + errorViewModel.Description += " Page: Home."; + } + + + return View("~/Views/Shared/Error.cshtml", errorViewModel); } public IActionResult Status(int id) { diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 74a1a53e..b3f90639 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -967,7 +967,6 @@ public static class HostingExtensions if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); - await app.MigrateDatabaseAsync(); } else { diff --git a/src/Yavsc.Org/Helpers/ErrorViewHelpers.cs b/src/Yavsc.Org/Helpers/ErrorViewHelpers.cs new file mode 100644 index 00000000..3d038c3f --- /dev/null +++ b/src/Yavsc.Org/Helpers/ErrorViewHelpers.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Yavsc.Models; + +public static class ErrorViewHelpers +{ + public static IActionResult ErrorView(this Controller controller, string message) + { + var logger = controller.HttpContext.RequestServices.GetRequiredService() + .CreateLogger(); + + logger.LogError(message); + Dictionary dictionary = new Dictionary(); + + if (!controller.ModelState.IsValid) + { + foreach (var modelState in controller.ModelState.Values) + { + foreach (var error in modelState.Errors) + { + logger.LogError("ModelState error: {0}", error.ErrorMessage); + foreach (var key in controller.ModelState.Keys) + { + logger.LogError("ModelState key: {0}", key); + dictionary.Add(key, + string.Join("\n", + controller.ModelState[key].Errors.Select( e => e.ErrorMessage).ToArray())); + } + } + } + } + + if (controller.HttpContext.Request.Headers.ContainsKey("Accept") + && controller.HttpContext.Request.Headers["Accept"].ToString().Contains("application/json")) + { + return controller.Json(new + { + RequestId = controller.HttpContext.TraceIdentifier, + Description = message, + ModelErrors = dictionary + }); + } + + return controller.View("Error", + new ErrorViewModel + { + RequestId = controller.HttpContext.TraceIdentifier, + Description = message, + ModelErrors = dictionary + } + ); + } +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/ErrorViewModel.cs b/src/Yavsc.Server/Models/ErrorViewModel.cs index 1b779ead..17819476 100644 --- a/src/Yavsc.Server/Models/ErrorViewModel.cs +++ b/src/Yavsc.Server/Models/ErrorViewModel.cs @@ -7,4 +7,5 @@ public class ErrorViewModel public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + public Dictionary ModelErrors { get; set; } } From 286c29f4e3594123d286fba7da90a4db9d1d97a8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 2 Aug 2026 21:08:25 +0100 Subject: [PATCH 030/151] Navigate to Main Page --- src/PostIt/PostIt/App.axaml.cs | 22 +++++++++---------- .../PostIt/ViewModels/HomePageViewModel.cs | 5 +++-- src/PostIt/PostIt/Views/HomePage.axaml | 8 +++++++ src/PostIt/PostIt/Views/InitialPage.cs | 0 src/PostIt/PostIt/Views/MainPage.axaml | 3 +-- src/PostIt/PostIt/Views/MainWindow.axaml | 2 +- 6 files changed, 24 insertions(+), 16 deletions(-) delete mode 100644 src/PostIt/PostIt/Views/InitialPage.cs diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index ca7e051d..1e0afeaf 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -25,7 +25,7 @@ public partial class App : Application /// DataValidationErrors.SetErrors. /// public IServiceProvider? Services { get; private set; } - + private MainWindow window; public App() { } @@ -137,7 +137,7 @@ public partial class App : Application var homePage = provider.GetRequiredService(); homePage.DataContext = provider.GetRequiredService(); - var window = new MainWindow(); + window = new MainWindow(); window.SessionBanner.DataContext = sessionStatus; // Build the navigation stack from scratch: HomePage is the @@ -165,7 +165,7 @@ public partial class App : Application sessionStatus.LoginSucceeded += () => { var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!; - _ = PushMainPageAsync(provider, w); + _ = PushMainPageAsync(); }; // When the user clicks the "Paramètres" button on the @@ -196,7 +196,7 @@ public partial class App : Application _ = w.NavRoot.PushAsync(settingsPage); }; - window.Opened += async (_, _) => await BootAsync(provider, api, window); + window.Opened += async (_, _) => await BootAsync(provider, api); } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView) { @@ -223,15 +223,14 @@ public partial class App : Application /// private static async Task BootAsync( IServiceProvider provider, - YavscApiClient api, - MainWindow window) + YavscApiClient api) { var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true); var sessionStatus = provider.GetRequiredService(); sessionStatus.Refresh(); if (!refreshed) return; - await PushMainPageAsync(provider, window).ConfigureAwait(true); + await PushMainPageAsync().ConfigureAwait(true); } /// @@ -241,12 +240,13 @@ public partial class App : Application /// (interactive login from the banner). Pulled out as a helper so /// the two callers can't drift apart. /// - private static async Task PushMainPageAsync(IServiceProvider provider, MainWindow window) + public static async Task PushMainPageAsync() { - var mainVm = provider.GetRequiredService(); - var mainPage = provider.GetRequiredService(); + var app = (App)Current; + var mainVm = app.Services.GetRequiredService(); + var mainPage = app.Services.GetRequiredService(); mainPage.DataContext = mainVm; - await window.NavRoot.PushAsync(mainPage).ConfigureAwait(true); + await app.window.FindControl("NavRoot").PushAsync(mainPage).ConfigureAwait(true); } private bool TryHandOffCustomSchemeUrl() diff --git a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs index 8d5b3a17..c11e396a 100644 --- a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs @@ -1,6 +1,7 @@ +using CommunityToolkit.Mvvm.Input; using PostIt; using PostIt.Services; -using PostIt.ViewModels; +namespace PostIt.ViewModels; public class HomePageViewModel : ViewModelBase { @@ -22,7 +23,7 @@ public class HomePageViewModel : ViewModelBase Api = api; Settings = settings; } - + public RelayCommand OpenBlogs { get; set; } = new RelayCommand(() => App.PushMainPageAsync()); /// /// Avalonia designer constructor. Builds a self-contained VM /// with a freshly-constructed Settings so the XAML preview can diff --git a/src/PostIt/PostIt/Views/HomePage.axaml b/src/PostIt/PostIt/Views/HomePage.axaml index 82473542..7eb6dbb3 100644 --- a/src/PostIt/PostIt/Views/HomePage.axaml +++ b/src/PostIt/PostIt/Views/HomePage.axaml @@ -1,7 +1,12 @@ + + + @@ -9,5 +14,8 @@ FontSize="22" FontWeight="SemiBold" HorizontalAlignment="Center"/> + - public IServiceProvider? Services { get; private set; } + public IServiceProvider? ServiceProvider { get; private set; } private MainWindow window; public App() { @@ -91,19 +91,17 @@ public partial class App : Application services.AddSingleton(sessionStatus); services.AddTransient(); - var provider = services.BuildServiceProvider(); + ServiceProvider = services.BuildServiceProvider(); // Bind the canonical Settings to the static accessor so any // code path that can't easily take a constructor parameter // (designer surfaces, Avalonia data templates) still gets // the same instance the rest of the app is using. Idempotent: // re-binding from a second App boot (tests) is a no-op. - Settings.BindToServiceProvider(provider); - - Services = provider; + Settings.BindToServiceProvider(ServiceProvider); DataTemplates.Clear(); - DataTemplates.Add(new ViewLocator(provider)); + DataTemplates.Add(new ViewLocator(ServiceProvider)); // Wire the Settings singleton onto the SettingsPage singleton // once, at composition time. The page is registered as a @@ -113,7 +111,7 @@ public partial class App : Application // DataContext, and the TwoWay bindings inside the page keep // mutating the same in-memory Settings instance that the rest // of the app reads (OidcClientOptions construction, etc.). - provider.GetRequiredService().DataContext = settings; + ServiceProvider.GetRequiredService().DataContext = settings; // Settings.DarkMode was previously a dead field: it round- // tripped through the settings file and the SettingsPage @@ -134,8 +132,8 @@ public partial class App : Application if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var homePage = provider.GetRequiredService(); - homePage.DataContext = provider.GetRequiredService(); + var homePage = ServiceProvider.GetRequiredService(); + homePage.DataContext = ServiceProvider.GetRequiredService(); window = new MainWindow(); window.SessionBanner.DataContext = sessionStatus; @@ -155,8 +153,8 @@ public partial class App : Application { var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!; var nav = w.NavRoot; - var hp = provider.GetRequiredService(); - hp.DataContext = provider.GetRequiredService(); + var hp = ServiceProvider.GetRequiredService(); + hp.DataContext = ServiceProvider.GetRequiredService(); _ = nav.PopToRootAsync(); }; @@ -187,7 +185,7 @@ public partial class App : Application sessionStatus.OpenSettingsRequested += () => { var w = (MainWindow)((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime!).MainWindow!; - var settingsPage = provider.GetRequiredService(); + var settingsPage = ServiceProvider.GetRequiredService(); var stack = w.NavRoot.NavigationStack; if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], settingsPage)) { @@ -196,13 +194,13 @@ public partial class App : Application _ = w.NavRoot.PushAsync(settingsPage); }; - window.Opened += async (_, _) => await BootAsync(provider, api); + window.Opened += async (_, _) => await BootAsync(ServiceProvider, api); } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView) { singleView.MainView = new MainWindow { - DataContext = provider.GetRequiredService() + DataContext = ServiceProvider.GetRequiredService() }; } } @@ -243,8 +241,8 @@ public partial class App : Application public static async Task PushMainPageAsync() { var app = (App)Current; - var mainVm = app.Services.GetRequiredService(); - var mainPage = app.Services.GetRequiredService(); + var mainVm = app.ServiceProvider.GetRequiredService(); + var mainPage = app.ServiceProvider.GetRequiredService(); mainPage.DataContext = mainVm; await app.window.FindControl("NavRoot").PushAsync(mainPage).ConfigureAwait(true); } diff --git a/src/PostIt/PostIt/Models/BlogPost.cs b/src/PostIt/PostIt/Models/BlogPost.cs index 7867eb02..e62fcea2 100644 --- a/src/PostIt/PostIt/Models/BlogPost.cs +++ b/src/PostIt/PostIt/Models/BlogPost.cs @@ -1,16 +1,37 @@ using System; +using Yavsc.Abstract.Identity; +using Yavsc.Abstract.Identity.Security; +using Yavsc.Blogspot; namespace PostIt.Models; -public class BlogPost +public class BlogPost : IBlogPost { - public long Id { get; set; } - public string Title { get; set; } = string.Empty; - public string? Article { get; set; } - public string? Photo { get; set; } - public string? AuthorId { get; set; } - public DateTime DateCreated { get; set; } - public string? UserCreated { get; set; } - public DateTime DateModified { get; set; } - public string? UserModified { get; set; } + public string AuthorId { get; set; } + + public IApplicationUser Author { get; set; } + + public string Article { get; set ; } + public string Photo { get; set ; } + public long Id { get; set ; } + public DateTime DateCreated { get; set ; } + public string UserCreated { get; set ; } + public DateTime DateModified { get; set ; } + public string UserModified { get; set ; } + public string Title { get; set ; } + + public bool AuthorizeCircle(long circleId) + { + throw new NotImplementedException(); + } + + public ICircleAuthorization[] GetACL() + { + throw new NotImplementedException(); + } + + public string[] GetTags() + { + throw new NotImplementedException(); + } } diff --git a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs index c11e396a..876f862c 100644 --- a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.DependencyInjection; using PostIt; using PostIt.Services; namespace PostIt.ViewModels; @@ -7,6 +8,7 @@ public class HomePageViewModel : ViewModelBase { public YavscApiClient Api { get; } public Settings Settings { get; } + public SessionStatusViewModel SessionStatus { get; } private string _welcomeText = "Welcome to PostIt!"; public string WelcomeText @@ -18,10 +20,12 @@ public class HomePageViewModel : ViewModelBase public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); } public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); } - public HomePageViewModel(YavscApiClient api, Settings settings) + public HomePageViewModel(YavscApiClient api, Settings settings, SessionStatusViewModel sessionStatus) { Api = api; Settings = settings; + SessionStatus = sessionStatus; + } public RelayCommand OpenBlogs { get; set; } = new RelayCommand(() => App.PushMainPageAsync()); /// @@ -33,5 +37,8 @@ public class HomePageViewModel : ViewModelBase /// (thread-safe dispatcher marshalling on PropertyChanged) — a /// designer-only duplicate instance is therefore harmless. /// - public HomePageViewModel() : this(null!, new Settings()) { } + public HomePageViewModel() : this(null!, new Settings(), new SessionStatusViewModel()) + { + + } } diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs index 1535d2f4..6a907a01 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml.cs +++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs @@ -28,7 +28,7 @@ public partial class MainPage : ContentPage // Resolve via the App's DI container so the page gets // the canonical services (Api client, settings, ...). var app = Application.Current as App; - var services = app?.Services; + var services = app?.ServiceProvider; if (services is null) return; var page = services.GetRequiredService(); diff --git a/src/Yavsc.Abstract/Blogspot/IBlog.cs b/src/Yavsc.Abstract/Blogspot/IBlog.cs deleted file mode 100644 index 86efc8e3..00000000 --- a/src/Yavsc.Abstract/Blogspot/IBlog.cs +++ /dev/null @@ -1,19 +0,0 @@ - - - -using Yavsc.Abstract.Identity; - -namespace Yavsc -{ - public interface IBlogPostPayLoad - { - string Article { get; set; } - string Photo { get; set; } - - } - public interface IBlogPost : IBlogPostPayLoad, ITrackedEntity, IIdentified, ITitle - { - string AuthorId { get; set; } - IApplicationUser Author { get; } - } -} diff --git a/src/Yavsc.Abstract/Blogspot/IBlogPost.cs b/src/Yavsc.Abstract/Blogspot/IBlogPost.cs new file mode 100644 index 00000000..691e03f2 --- /dev/null +++ b/src/Yavsc.Abstract/Blogspot/IBlogPost.cs @@ -0,0 +1,15 @@ + + + +using Yavsc.Abstract.Identity; +using Yavsc.Abstract.Identity.Security; +using Yavsc.Interfaces; + +namespace Yavsc.Blogspot +{ + public interface IBlogPost : IBlogPostPayLoad, ICircleAuthorized, ITaggable, ITrackedEntity, IIdentified, ITitle + { + string AuthorId { get; set; } + IApplicationUser Author { get; } + } +} diff --git a/src/Yavsc.Abstract/Blogspot/IBlogPostPayLoad.cs b/src/Yavsc.Abstract/Blogspot/IBlogPostPayLoad.cs new file mode 100644 index 00000000..d8aeb4fe --- /dev/null +++ b/src/Yavsc.Abstract/Blogspot/IBlogPostPayLoad.cs @@ -0,0 +1,9 @@ +namespace Yavsc.Blogspot +{ + public interface IBlogPostPayLoad + { + string Article { get; set; } + string Photo { get; set; } + + } +} diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index 69b37e60..23d71742 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Yavsc.Blogspot; using Yavsc.Models.Blog; using Yavsc.Server.Exceptions; using Yavsc.Server.Helpers; diff --git a/src/Yavsc.Server/Models/Blog/BlogPost.cs b/src/Yavsc.Server/Models/Blog/BlogPost.cs index eb58cb62..e1b44603 100644 --- a/src/Yavsc.Server/Models/Blog/BlogPost.cs +++ b/src/Yavsc.Server/Models/Blog/BlogPost.cs @@ -3,15 +3,14 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; using Yavsc.Abstract.Identity; using Yavsc.Abstract.Identity.Security; -using Yavsc.Interfaces; using Yavsc.Models.Access; using Yavsc.Models.Relationship; +using Yavsc.Blogspot; namespace Yavsc.Models.Blog { - - public class BlogPost : - IBlogPost, ICircleAuthorized, ITaggable + + public class BlogPost : IBlogPost { [Key(), DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Display(Name = "Identifiant du post")] diff --git a/src/Yavsc.Server/Services/BlogSpotService.cs b/src/Yavsc.Server/Services/BlogSpotService.cs index a8b52d0d..dc02cde2 100644 --- a/src/Yavsc.Server/Services/BlogSpotService.cs +++ b/src/Yavsc.Server/Services/BlogSpotService.cs @@ -10,6 +10,7 @@ using Yavsc.Server.Helpers; using Yavsc.Services; using Yavsc.ViewModels.Auth; using Microsoft.AspNetCore.Http; +using Yavsc.Blogspot; public class BlogSpotService { From 3744d9ae9cc9459bea2cfe57eca9e631547a827b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 3 Aug 2026 01:48:15 +0100 Subject: [PATCH 034/151] Enable blogs on connected status --- src/PostIt/PostIt/Views/HomePage.axaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PostIt/PostIt/Views/HomePage.axaml b/src/PostIt/PostIt/Views/HomePage.axaml index 7eb6dbb3..16e66cc0 100644 --- a/src/PostIt/PostIt/Views/HomePage.axaml +++ b/src/PostIt/PostIt/Views/HomePage.axaml @@ -16,6 +16,7 @@ HorizontalAlignment="Center"/>