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;
|
_myInfoMateDbContext = myInfoMateDbContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSuperAdmin() =>
|
||||||
|
User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a list of all applicationInstance (summary)
|
/// Get a list of all applicationInstance (summary)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -54,6 +57,11 @@ namespace ManagerService.Controllers
|
|||||||
return new OkObjectResult(applicationInstances.Select(ai => {
|
return new OkObjectResult(applicationInstances.Select(ai => {
|
||||||
var dto = ai.ToDTO(_myInfoMateDbContext);
|
var dto = ai.ToDTO(_myInfoMateDbContext);
|
||||||
dto.hasStats = instance?.HasStats ?? false;
|
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;
|
return dto;
|
||||||
}).OrderBy(c => c.appType));
|
}).OrderBy(c => c.appType));
|
||||||
}
|
}
|
||||||
@ -92,6 +100,12 @@ namespace ManagerService.Controllers
|
|||||||
ApplicationInstance applicationInstance = new ApplicationInstance().FromDTO(newApplicationInstanceDTO);
|
ApplicationInstance applicationInstance = new ApplicationInstance().FromDTO(newApplicationInstanceDTO);
|
||||||
applicationInstance.Id = idService.GenerateHexId();
|
applicationInstance.Id = idService.GenerateHexId();
|
||||||
|
|
||||||
|
if (!IsSuperAdmin())
|
||||||
|
{
|
||||||
|
applicationInstance.AppStoreUrl = null;
|
||||||
|
applicationInstance.PlayStoreUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
_myInfoMateDbContext.ApplicationInstances.Add(applicationInstance);
|
_myInfoMateDbContext.ApplicationInstances.Add(applicationInstance);
|
||||||
|
|
||||||
_myInfoMateDbContext.SaveChanges();
|
_myInfoMateDbContext.SaveChanges();
|
||||||
@ -133,7 +147,17 @@ namespace ManagerService.Controllers
|
|||||||
if (applicationInstance == null)
|
if (applicationInstance == null)
|
||||||
throw new KeyNotFoundException("application instance does not exist");
|
throw new KeyNotFoundException("application instance does not exist");
|
||||||
|
|
||||||
|
var appStoreUrl = applicationInstance.AppStoreUrl;
|
||||||
|
var playStoreUrl = applicationInstance.PlayStoreUrl;
|
||||||
|
|
||||||
applicationInstance = applicationInstance.FromDTO(updatedApplicationInstanceDTO);
|
applicationInstance = applicationInstance.FromDTO(updatedApplicationInstanceDTO);
|
||||||
|
|
||||||
|
if (!IsSuperAdmin())
|
||||||
|
{
|
||||||
|
applicationInstance.AppStoreUrl = appStoreUrl;
|
||||||
|
applicationInstance.PlayStoreUrl = playStoreUrl;
|
||||||
|
}
|
||||||
|
|
||||||
_myInfoMateDbContext.SaveChanges();
|
_myInfoMateDbContext.SaveChanges();
|
||||||
|
|
||||||
return new OkObjectResult(applicationInstance.ToDTO(_myInfoMateDbContext));
|
return new OkObjectResult(applicationInstance.ToDTO(_myInfoMateDbContext));
|
||||||
|
|||||||
@ -65,7 +65,7 @@ namespace ManagerService.Controllers
|
|||||||
public ObjectResult CheckSlug(string slug)
|
public ObjectResult CheckSlug(string slug)
|
||||||
{
|
{
|
||||||
var sanitized = SanitizeSlug(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 });
|
return new OkObjectResult(new { slug = sanitized, available });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ namespace ManagerService.Controllers
|
|||||||
var slug = SanitizeSlug(dto.slug);
|
var slug = SanitizeSlug(dto.slug);
|
||||||
if (slug.Length < 3)
|
if (slug.Length < 3)
|
||||||
throw new ArgumentNullException(nameof(dto.slug), "Slug must be at least 3 characters");
|
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");
|
throw new InvalidOperationException("This address is already taken");
|
||||||
|
|
||||||
var plan = _myInfoMateDbContext.SubscriptionPlans.FirstOrDefault(p => p.Id == EssentielPlanId);
|
var plan = _myInfoMateDbContext.SubscriptionPlans.FirstOrDefault(p => p.Id == EssentielPlanId);
|
||||||
|
|||||||
@ -35,5 +35,19 @@ namespace ManagerService.DTOs
|
|||||||
public bool isAssistant { get; set; }
|
public bool isAssistant { get; set; }
|
||||||
|
|
||||||
public bool hasStats { 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 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)
|
public ApplicationInstanceDTO ToDTO(MyInfoMateDbContext myInfoMateDbContext)
|
||||||
{
|
{
|
||||||
@ -80,7 +88,11 @@ namespace ManagerService.Data
|
|||||||
languages = Languages,
|
languages = Languages,
|
||||||
sectionEventId = SectionEventId,
|
sectionEventId = SectionEventId,
|
||||||
sectionEventDTO = sectionEventDTO,
|
sectionEventDTO = sectionEventDTO,
|
||||||
isAssistant = IsAssistant
|
isAssistant = IsAssistant,
|
||||||
|
isQRCodeEnabled = IsQRCodeEnabled,
|
||||||
|
appName = AppName,
|
||||||
|
appStoreUrl = AppStoreUrl,
|
||||||
|
playStoreUrl = PlayStoreUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +110,10 @@ namespace ManagerService.Data
|
|||||||
Configurations = dto.configurations;
|
Configurations = dto.configurations;
|
||||||
SectionEventId = dto.sectionEventId;
|
SectionEventId = dto.sectionEventId;
|
||||||
IsAssistant = dto.isAssistant;
|
IsAssistant = dto.isAssistant;
|
||||||
|
IsQRCodeEnabled = dto.isQRCodeEnabled;
|
||||||
|
AppName = dto.appName;
|
||||||
|
AppStoreUrl = dto.appStoreUrl;
|
||||||
|
PlayStoreUrl = dto.playStoreUrl;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -265,6 +265,13 @@ namespace ManagerService.Data
|
|||||||
v => JsonSerializer.Serialize(v, options),
|
v => JsonSerializer.Serialize(v, options),
|
||||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(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.
|
// Guide IA — recherche sémantique.
|
||||||
// Le type vector n'existe que sur Npgsql : les tests tournent sur EF InMemory,
|
// 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.
|
// 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
|
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>
|
/// <summary>
|
||||||
/// Generate a URL-safe slug from a name, appending "-1", "-2", etc. until unique among Instances.WebSlug.
|
/// Generate a URL-safe slug from a name, appending "-1", "-2", etc. until unique among Instances.WebSlug.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -24,7 +33,7 @@ namespace ManagerService.Helpers
|
|||||||
|
|
||||||
var baseSlug = slug;
|
var baseSlug = slug;
|
||||||
var counter = 1;
|
var counter = 1;
|
||||||
while (db.Instances.Any(i => i.WebSlug == slug))
|
while (IsTaken(db, slug))
|
||||||
slug = $"{baseSlug}-{counter++}";
|
slug = $"{baseSlug}-{counter++}";
|
||||||
|
|
||||||
return slug;
|
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")
|
b.Property<string>("Id")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("AppName")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("AppStoreUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<int>("AppType")
|
b.Property<int>("AppType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
@ -163,6 +169,9 @@ namespace ManagerService.Migrations
|
|||||||
b.Property<bool>("IsAssistant")
|
b.Property<bool>("IsAssistant")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsQRCodeEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.PrimitiveCollection<List<string>>("Languages")
|
b.PrimitiveCollection<List<string>>("Languages")
|
||||||
.HasColumnType("text[]");
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
@ -178,6 +187,9 @@ namespace ManagerService.Migrations
|
|||||||
b.Property<string>("MainImageUrl")
|
b.Property<string>("MainImageUrl")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PlayStoreUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("PrimaryColor")
|
b.Property<string>("PrimaryColor")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user