Visibilite de section et logo des e-mails transactionnels

Endpoint dedie `PUT Section/{id}/visibility` : passer par `Update` aurait
reconstruit le sous-type via SectionFactory, donc efface le contenu specifique
de la section.

Le logo des e-mails est servi par le service lui-meme (wwwroot + UseStaticFiles)
plutot que par le manager deploye en face, dont il ne doit pas dependre.
This commit is contained in:
Thomas Fransolet 2026-09-08 15:03:31 +02:00
parent 847f81393b
commit 1e1a36ad5a
6 changed files with 142 additions and 21 deletions

View File

@ -758,6 +758,46 @@ namespace ManagerService.Controllers
} }
} }
/// <summary>
/// Show or hide a section in the visitor apps
/// </summary>
/// <param name="id">Section id</param>
/// <param name="isActive">true = visible, false = hidden</param>
/// <remarks>
/// Endpoint dédié plutôt que <see cref="Update"/> : celui-ci reconstruit le
/// sous-type via SectionFactory, donc un SectionDTO nu effacerait tout le
/// contenu spécifique de la section.
/// </remarks>
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut("{id}/visibility")]
public ObjectResult SetVisibility(string id, [FromQuery] bool isActive)
{
try
{
Section section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == id);
if (section == null)
throw new KeyNotFoundException("Section does not exist");
section.IsActive = isActive;
_myInfoMateDbContext.SaveChanges();
MqttClientService.PublishMessage($"config/{section.ConfigurationId}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
return new OkObjectResult(SectionFactory.ToDTO(section));
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary> /// <summary>
/// Update sections order /// Update sections order
/// </summary> /// </summary>

View File

@ -3,57 +3,127 @@ using System;
namespace ManagerService.EmailTemplates namespace ManagerService.EmailTemplates
{ {
/// <summary> /// <summary>
/// Shared HTML shell for all transactional emails — MyInfoMate branding /// Shared HTML shell for all transactional emails. Palette taken from the
/// (cyan #0df2df header, dark logo band, white content card). /// MyInfoMate platform deck: navy #0a1222, cyan #0df2df, teal #0e8f8a.
/// </summary> /// </summary>
public static class EmailLayout public static class EmailLayout
{ {
public static string Render(string title, string bodyHtml, string ctaText = null, string ctaUrl = null) /// <summary>Absolute URL of the logo shown in the header band. Empty = wordmark only.</summary>
public static string LogoUrl { get; private set; } = "";
/// <summary>Public site linked from the footer. Empty = no link.</summary>
public static string LandingUrl { get; private set; } = "";
/// <summary>Called once at startup from configuration (see Startup.cs).</summary>
public static void Configure(string logoUrl, string landingUrl)
{ {
var ctaHtml = ""; // Une URL relative afficherait une image cassée dans la boîte de
if (!string.IsNullOrEmpty(ctaText) && !string.IsNullOrEmpty(ctaUrl)) // réception : mieux vaut le bandeau sans logo que ça.
{ LogoUrl = logoUrl != null && logoUrl.StartsWith("http") ? logoUrl : "";
ctaHtml = $@" LandingUrl = landingUrl ?? "";
<table role=""presentation"" cellpadding=""0"" cellspacing=""0"" style=""margin: 28px 0 8px;"">
<tr>
<td style=""border-radius: 12px; background: #0df2df;"">
<a href=""{ctaUrl}"" style=""display: inline-block; padding: 14px 28px; font-family: Arial, sans-serif; font-size: 15px; font-weight: 700; color: #043b37; text-decoration: none;"">{ctaText}</a>
</td>
</tr>
</table>";
} }
return $@"<!DOCTYPE html> public static string Render(string title, string bodyHtml, string ctaText = null, string ctaUrl = null)
<html lang=""fr""> {
<head><meta charset=""utf-8""><meta name=""viewport"" content=""width=device-width, initial-scale=1.0""></head> var hasCta = !string.IsNullOrEmpty(ctaText) && !string.IsNullOrEmpty(ctaUrl);
<body style=""margin:0; padding:0; background:#f1f5f9; font-family: Arial, sans-serif;"">
<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#f1f5f9; padding: 32px 0;""> // Le texte de pré-en-tête est ce que la boîte de réception affiche à
// côté de l'objet : sans lui elle recopie le début du HTML.
var preheader = StripTags(bodyHtml);
if (preheader.Length > 140) preheader = preheader.Substring(0, 140) + "…";
var logoHtml = string.IsNullOrEmpty(LogoUrl)
? ""
: $@"<img src=""{LogoUrl}"" width=""44"" height=""42"" alt="""" style=""display:block; border:0;"">";
var ctaHtml = "";
if (hasCta)
{
ctaHtml = $@"
<table role=""presentation"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""margin:32px 0 0;"">
<tr> <tr>
<td align=""center""> <td align=""center"" bgcolor=""#0df2df"" style=""border-radius:10px;"">
<table role=""presentation"" width=""560"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff; border-radius: 16px; overflow:hidden;""> <a href=""{ctaUrl}"" style=""display:block; padding:16px 34px; font-family:Helvetica,Arial,sans-serif; font-size:16px; font-weight:bold; color:#0a1222; text-decoration:none; letter-spacing:.2px;"">{ctaText}</a>
<tr>
<td style=""background:#0a1222; padding: 24px 32px;"">
<span style=""font-size: 18px; font-weight: 800; color:#ffffff;"">MyInfoMate</span>
</td>
</tr>
<tr>
<td style=""padding: 32px;"">
<h1 style=""margin:0 0 16px; font-size: 20px; color:#0f172a;"">{title}</h1>
<div style=""font-size: 14px; line-height: 1.6; color:#334155;"">{bodyHtml}</div>
{ctaHtml}
</td>
</tr>
<tr>
<td style=""padding: 20px 32px; border-top: 1px solid #e2e8f0; font-size: 12px; color:#94a3b8;"">
MyInfoMate {DateTime.UtcNow.Year}
</td> </td>
</tr> </tr>
</table> </table>
<p style=""margin:20px 0 0; font-family:Helvetica,Arial,sans-serif; font-size:12.5px; line-height:1.6; color:#94a3b8;"">
Le bouton ne fonctionne pas ? Copiez ce lien dans votre navigateur :<br>
<span style=""color:#0e8f8a; word-break:break-all;"">{ctaUrl}</span>
</p>";
}
var landingHtml = string.IsNullOrEmpty(LandingUrl)
? ""
: $@" · <a href=""{LandingUrl}"" style=""color:#0e8f8a; text-decoration:none; font-weight:bold;"">myinfomate.be</a>";
return $@"<!DOCTYPE html>
<html lang=""fr"">
<head>
<meta charset=""utf-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<meta name=""color-scheme"" content=""light"">
<title>{title}</title>
</head>
<body style=""margin:0; padding:0; background:#f5f7fa;"">
<div style=""display:none; max-height:0; overflow:hidden; opacity:0;"">{preheader}</div>
<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""background:#f5f7fa;"">
<tr>
<td align=""center"" style=""padding:40px 16px;"">
<table role=""presentation"" width=""600"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""width:600px; max-width:100%; background:#ffffff; border-radius:18px; overflow:hidden; box-shadow:0 2px 12px rgba(10,18,34,.07);"">
<tr>
<td bgcolor=""#0a1222"" style=""padding:30px 40px;"">
<table role=""presentation"" cellpadding=""0"" cellspacing=""0"" border=""0"">
<tr>
<td style=""padding-right:14px;"">{logoHtml}</td>
<td style=""font-family:Helvetica,Arial,sans-serif; font-size:21px; font-weight:bold; color:#ffffff; letter-spacing:-.3px;"">MyInfoMate</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height=""4"" bgcolor=""#0df2df"" style=""height:4px; line-height:4px; font-size:0;"">&nbsp;</td>
</tr>
<tr>
<td style=""padding:44px 40px 48px;"">
<h1 style=""margin:0 0 20px; font-family:Helvetica,Arial,sans-serif; font-size:26px; line-height:1.25; font-weight:bold; color:#0a1222; letter-spacing:-.4px;"">{title}</h1>
<div style=""font-family:Helvetica,Arial,sans-serif; font-size:15.5px; line-height:1.75; color:#3b4754;"">{bodyHtml}</div>
{ctaHtml}
</td>
</tr>
<tr>
<td bgcolor=""#f5f7fa"" style=""padding:26px 40px; border-top:1px solid #d6dde5; font-family:Helvetica,Arial,sans-serif; font-size:12.5px; line-height:1.7; color:#64748b;"">
<strong style=""color:#0a1222;"">MyInfoMate</strong> la plateforme de contenu des lieux culturels.<br>
© {DateTime.UtcNow.Year} Unov{landingHtml}
</td>
</tr>
</table>
</td> </td>
</tr> </tr>
</table> </table>
</body> </body>
</html>"; </html>";
} }
private static string StripTags(string html)
{
if (string.IsNullOrEmpty(html)) return "";
var sb = new System.Text.StringBuilder(html.Length);
var inTag = false;
foreach (var c in html)
{
if (c == '<') inTag = true;
else if (c == '>') inTag = false;
else if (!inTag) sb.Append(c);
}
return sb.ToString().Replace(" ", " ").Trim();
}
} }
} }

View File

@ -121,6 +121,10 @@ namespace ManagerService
services.Configure<StripeSettings>(Configuration.GetSection("Stripe")); services.Configure<StripeSettings>(Configuration.GetSection("Stripe"));
services.Configure<ResendSettings>(Configuration.GetSection("Resend")); services.Configure<ResendSettings>(Configuration.GetSection("Resend"));
ManagerService.EmailTemplates.EmailLayout.Configure(
Configuration["AppUrls:Logo"] ?? $"{Configuration["AppUrls:Api"]}/email-logo.png",
Configuration["AppUrls:Landing"]);
foreach (var policy in ManagerService.Service.Security.PoliciesConfiguration) foreach (var policy in ManagerService.Service.Security.PoliciesConfiguration)
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
@ -320,6 +324,11 @@ namespace ManagerService
//app.UseHttpsRedirection(); //app.UseHttpsRedirection();
// Sert wwwroot/ — aujourd'hui le seul fichier est le logo des e-mails
// transactionnels, qui doit vivre sur le même service que l'envoi pour
// ne pas dépendre du build du manager déployé en face.
app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@ -26,7 +26,8 @@
}, },
"AppUrls": { "AppUrls": {
"ManagerApp": "http://localhost:9090", "ManagerApp": "http://localhost:9090",
"Landing": "http://localhost:3000" "Landing": "http://localhost:3000",
"Api": "http://localhost:5000"
} }
} }

View File

@ -47,7 +47,8 @@
}, },
"AppUrls": { "AppUrls": {
"ManagerApp": "https://manager.myinfomate.be", "ManagerApp": "https://manager.myinfomate.be",
"Landing": "https://myinfomate.be" "Landing": "https://myinfomate.be",
"Api": "https://api.myinfomate.be"
}, },
"Stripe": { "Stripe": {
"SecretKey": "sk_test_51U14rjRLQgHvlM4X4ewATCOOIzdIfTZEJCwwZT9sfpWm9LkrISoSEHfgHbPhJTujdYdPdRyCnrDoo5Sf8NMyCrPT00RfYB8fSN", "SecretKey": "sk_test_51U14rjRLQgHvlM4X4ewATCOOIzdIfTZEJCwwZT9sfpWm9LkrISoSEHfgHbPhJTujdYdPdRyCnrDoo5Sf8NMyCrPT00RfYB8fSN",

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB