Nom d'app, drapeau QR et liens stores par application
ApplicationInstance porte désormais AppName (traductions, jsonb), IsQRCodeEnabled (défaut true) et les liens App Store / Play Store. Une ligne existe par (instance, type d'app) : chaque réglage vaut donc séparément pour le mobile et pour le web. Les liens stores ne sont modifiables que par un SuperAdmin — c'est Unov qui publie les apps —, contrôlé côté API et pas seulement masqué dans l'UI. La liste anonyme des apps expose le slug web et le nom de l'instance : la page /download de visitapp-web n'a que l'instanceId d'un QR imprimé. Réserve les slugs download, demo et api : un segment statique de visitapp-web l'emporte sur [slug], l'instance serait inaccessible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9cc45c5767
commit
4e17c572ba
@ -36,6 +36,9 @@ namespace ManagerService.Controllers
|
||||
_myInfoMateDbContext = myInfoMateDbContext;
|
||||
}
|
||||
|
||||
private bool IsSuperAdmin() =>
|
||||
User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all applicationInstance (summary)
|
||||
/// </summary>
|
||||
@ -54,6 +57,11 @@ namespace ManagerService.Controllers
|
||||
return new OkObjectResult(applicationInstances.Select(ai => {
|
||||
var dto = ai.ToDTO(_myInfoMateDbContext);
|
||||
dto.hasStats = instance?.HasStats ?? false;
|
||||
// Porté par l'Instance, pas par l'application. La page /download de
|
||||
// visitapp-web n'a que l'instanceId d'un QR imprimé : sans ce slug, elle
|
||||
// ne peut pas renvoyer vers la visite web d'une instance sans app mobile.
|
||||
dto.webSlug = instance?.WebSlug;
|
||||
dto.instanceName = instance?.Name;
|
||||
return dto;
|
||||
}).OrderBy(c => c.appType));
|
||||
}
|
||||
@ -92,6 +100,12 @@ namespace ManagerService.Controllers
|
||||
ApplicationInstance applicationInstance = new ApplicationInstance().FromDTO(newApplicationInstanceDTO);
|
||||
applicationInstance.Id = idService.GenerateHexId();
|
||||
|
||||
if (!IsSuperAdmin())
|
||||
{
|
||||
applicationInstance.AppStoreUrl = null;
|
||||
applicationInstance.PlayStoreUrl = null;
|
||||
}
|
||||
|
||||
_myInfoMateDbContext.ApplicationInstances.Add(applicationInstance);
|
||||
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
@ -133,7 +147,17 @@ namespace ManagerService.Controllers
|
||||
if (applicationInstance == null)
|
||||
throw new KeyNotFoundException("application instance does not exist");
|
||||
|
||||
var appStoreUrl = applicationInstance.AppStoreUrl;
|
||||
var playStoreUrl = applicationInstance.PlayStoreUrl;
|
||||
|
||||
applicationInstance = applicationInstance.FromDTO(updatedApplicationInstanceDTO);
|
||||
|
||||
if (!IsSuperAdmin())
|
||||
{
|
||||
applicationInstance.AppStoreUrl = appStoreUrl;
|
||||
applicationInstance.PlayStoreUrl = playStoreUrl;
|
||||
}
|
||||
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new OkObjectResult(applicationInstance.ToDTO(_myInfoMateDbContext));
|
||||
|
||||
@ -65,7 +65,7 @@ namespace ManagerService.Controllers
|
||||
public ObjectResult CheckSlug(string slug)
|
||||
{
|
||||
var sanitized = SanitizeSlug(slug);
|
||||
var available = sanitized.Length >= 3 && !_myInfoMateDbContext.Instances.Any(i => i.WebSlug == sanitized);
|
||||
var available = sanitized.Length >= 3 && !SlugHelper.IsTaken(_myInfoMateDbContext, sanitized);
|
||||
return new OkObjectResult(new { slug = sanitized, available });
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ namespace ManagerService.Controllers
|
||||
var slug = SanitizeSlug(dto.slug);
|
||||
if (slug.Length < 3)
|
||||
throw new ArgumentNullException(nameof(dto.slug), "Slug must be at least 3 characters");
|
||||
if (_myInfoMateDbContext.Instances.Any(i => i.WebSlug == slug))
|
||||
if (SlugHelper.IsTaken(_myInfoMateDbContext, slug))
|
||||
throw new InvalidOperationException("This address is already taken");
|
||||
|
||||
var plan = _myInfoMateDbContext.SubscriptionPlans.FirstOrDefault(p => p.Id == EssentielPlanId);
|
||||
|
||||
@ -35,5 +35,19 @@ namespace ManagerService.DTOs
|
||||
public bool isAssistant { get; set; }
|
||||
|
||||
public bool hasStats { get; set; }
|
||||
|
||||
/// <summary>Slug web de l'Instance. Lecture seule, rempli par le contrôleur.</summary>
|
||||
public string? webSlug { get; set; }
|
||||
|
||||
/// <summary>Nom de l'Instance. Lecture seule, rempli par le contrôleur.</summary>
|
||||
public string? instanceName { get; set; }
|
||||
|
||||
public bool isQRCodeEnabled { get; set; } = true;
|
||||
|
||||
public List<TranslationDTO>? appName { get; set; }
|
||||
|
||||
public string? appStoreUrl { get; set; }
|
||||
|
||||
public string? playStoreUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,14 @@ namespace ManagerService.Data
|
||||
|
||||
public bool IsAssistant { get; set; } = false;
|
||||
|
||||
public bool IsQRCodeEnabled { get; set; } = true;
|
||||
|
||||
public List<TranslationDTO>? AppName { get; set; } // Specific Mobile et web
|
||||
|
||||
// Chaque client a sa propre app publiée par Unov : seul un SuperAdmin les modifie.
|
||||
public string? AppStoreUrl { get; set; } // Specific Mobile
|
||||
|
||||
public string? PlayStoreUrl { get; set; } // Specific Mobile
|
||||
|
||||
public ApplicationInstanceDTO ToDTO(MyInfoMateDbContext myInfoMateDbContext)
|
||||
{
|
||||
@ -80,7 +88,11 @@ namespace ManagerService.Data
|
||||
languages = Languages,
|
||||
sectionEventId = SectionEventId,
|
||||
sectionEventDTO = sectionEventDTO,
|
||||
isAssistant = IsAssistant
|
||||
isAssistant = IsAssistant,
|
||||
isQRCodeEnabled = IsQRCodeEnabled,
|
||||
appName = AppName,
|
||||
appStoreUrl = AppStoreUrl,
|
||||
playStoreUrl = PlayStoreUrl
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,6 +110,10 @@ namespace ManagerService.Data
|
||||
Configurations = dto.configurations;
|
||||
SectionEventId = dto.sectionEventId;
|
||||
IsAssistant = dto.isAssistant;
|
||||
IsQRCodeEnabled = dto.isQRCodeEnabled;
|
||||
AppName = dto.appName;
|
||||
AppStoreUrl = dto.appStoreUrl;
|
||||
PlayStoreUrl = dto.playStoreUrl;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -265,6 +265,13 @@ namespace ManagerService.Data
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<ApplicationInstance>()
|
||||
.Property(ai => ai.AppName)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
// Guide IA — recherche sémantique.
|
||||
// Le type vector n'existe que sur Npgsql : les tests tournent sur EF InMemory,
|
||||
// qui échoue à la validation du modèle s'il rencontre une propriété Vector.
|
||||
|
||||
@ -6,6 +6,15 @@ namespace ManagerService.Helpers
|
||||
{
|
||||
public static class SlugHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Premiers segments d'URL déjà pris par des routes de visitapp-web : un segment
|
||||
/// statique l'emporte sur [slug], l'instance serait donc inaccessible.
|
||||
/// </summary>
|
||||
private static readonly string[] ReservedSlugs = { "download", "demo", "api" };
|
||||
|
||||
public static bool IsTaken(MyInfoMateDbContext db, string slug) =>
|
||||
ReservedSlugs.Contains(slug) || db.Instances.Any(i => i.WebSlug == slug);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a URL-safe slug from a name, appending "-1", "-2", etc. until unique among Instances.WebSlug.
|
||||
/// </summary>
|
||||
@ -24,7 +33,7 @@ namespace ManagerService.Helpers
|
||||
|
||||
var baseSlug = slug;
|
||||
var counter = 1;
|
||||
while (db.Instances.Any(i => i.WebSlug == slug))
|
||||
while (IsTaken(db, slug))
|
||||
slug = $"{baseSlug}-{counter++}";
|
||||
|
||||
return slug;
|
||||
|
||||
1934
ManagerService/Migrations/20260910083904_AddAppNameQrAndStoresToApplicationInstance.Designer.cs
generated
Normal file
1934
ManagerService/Migrations/20260910083904_AddAppNameQrAndStoresToApplicationInstance.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,59 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAppNameQrAndStoresToApplicationInstance : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AppName",
|
||||
table: "ApplicationInstances",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AppStoreUrl",
|
||||
table: "ApplicationInstances",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsQRCodeEnabled",
|
||||
table: "ApplicationInstances",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PlayStoreUrl",
|
||||
table: "ApplicationInstances",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AppName",
|
||||
table: "ApplicationInstances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AppStoreUrl",
|
||||
table: "ApplicationInstances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsQRCodeEnabled",
|
||||
table: "ApplicationInstances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PlayStoreUrl",
|
||||
table: "ApplicationInstances");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,6 +147,12 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AppName")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("AppStoreUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AppType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
@ -163,6 +169,9 @@ namespace ManagerService.Migrations
|
||||
b.Property<bool>("IsAssistant")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsQRCodeEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Languages")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
@ -178,6 +187,9 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("MainImageUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PlayStoreUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PrimaryColor")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user