Compare commits
2 Commits
847f81393b
...
9cc45c5767
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cc45c5767 | ||
|
|
1e1a36ad5a |
@ -12,6 +12,7 @@ using ManagerService.Data;
|
|||||||
using ManagerService.Data.SubSection;
|
using ManagerService.Data.SubSection;
|
||||||
using ManagerService.DTOs;
|
using ManagerService.DTOs;
|
||||||
using ManagerService.Services;
|
using ManagerService.Services;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@ -379,14 +380,28 @@ namespace ManagerService.Controllers
|
|||||||
/// Export a configuration
|
/// Export a configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Id of configuration to export</param>
|
/// <param name="id">Id of configuration to export</param>
|
||||||
/// <param name="language">Language to export</param>
|
/// <param name="language">Language to export</param>
|
||||||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
/// <remarks>
|
||||||
|
/// Ouverte aux apps visiteur par <c>X-Api-Key</c> : c'est l'appel que fait
|
||||||
|
/// mymuseum-visitapp pour télécharger une visite hors ligne.
|
||||||
|
///
|
||||||
|
/// ⚠️ <c>[Authorize(AppReadAccess)]</c> ne suffisait pas : ASP.NET Core **combine**
|
||||||
|
/// les <c>[Authorize]</c> de la classe et de l'action. Le contrôleur exige
|
||||||
|
/// <c>ContentEditor</c>, qu'une clé API n'a pas — la clé authentifiait donc la
|
||||||
|
/// requête, puis l'autorisation la refusait : 403 sans corps, et côté app un
|
||||||
|
/// téléchargement qui échouait sans rien dire. Seul <c>[AllowAnonymous]</c>
|
||||||
|
/// court-circuite la policy du contrôleur ; le contrôle d'accès se fait ici.
|
||||||
|
/// Même correctif que <see cref="InstanceController.GetDetail"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(FileContentResult), 200)]
|
[ProducesResponseType(typeof(FileContentResult), 200)]
|
||||||
[ProducesResponseType(typeof(string), 400)]
|
[ProducesResponseType(typeof(string), 400)]
|
||||||
|
[ProducesResponseType(typeof(string), 401)]
|
||||||
|
[ProducesResponseType(typeof(string), 403)]
|
||||||
[ProducesResponseType(typeof(string), 404)]
|
[ProducesResponseType(typeof(string), 404)]
|
||||||
[ProducesResponseType(typeof(string), 500)]
|
[ProducesResponseType(typeof(string), 500)]
|
||||||
[HttpGet("{id}/export")]
|
[HttpGet("{id}/export")]
|
||||||
public FileContentResult Export(string id, [FromQuery] string language)
|
public async Task<IActionResult> Export(string id, [FromQuery] string language)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -398,6 +413,26 @@ namespace ManagerService.Controllers
|
|||||||
if (configuration == null)
|
if (configuration == null)
|
||||||
throw new KeyNotFoundException("Configuration does not exist");
|
throw new KeyNotFoundException("Configuration does not exist");
|
||||||
|
|
||||||
|
// Le schéma ApiKey n'est pas le schéma par défaut : sur une action
|
||||||
|
// [AllowAnonymous] il faut le déclencher explicitement.
|
||||||
|
var apiKeyAuth = await HttpContext.AuthenticateAsync("ApiKey");
|
||||||
|
var keyInstanceId = apiKeyAuth.Succeeded
|
||||||
|
? apiKeyAuth.Principal?.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Ne PAS déduire « utilisateur du manager » d'un claim de permission : le
|
||||||
|
// handler de clé API pose lui aussi le claim Viewer.
|
||||||
|
var isManager = !apiKeyAuth.Succeeded
|
||||||
|
&& User?.Identity?.IsAuthenticated == true
|
||||||
|
&& User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission,
|
||||||
|
ManagerService.Service.Security.Permissions.Viewer);
|
||||||
|
|
||||||
|
if (!isManager && keyInstanceId == null)
|
||||||
|
return new ObjectResult("Authentication required") { StatusCode = 401 };
|
||||||
|
|
||||||
|
if (!isManager && keyInstanceId != configuration.InstanceId)
|
||||||
|
return new ObjectResult("This API key does not grant access to this configuration") { StatusCode = 403 };
|
||||||
|
|
||||||
// Les entités, pas seulement leurs DTO : la collecte des ressources passe
|
// Les entités, pas seulement leurs DTO : la collecte des ressources passe
|
||||||
// par GetReferencedResourceIds, qui vit sur le sous-type.
|
// par GetReferencedResourceIds, qui vit sur le sous-type.
|
||||||
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
|
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
|
||||||
@ -436,20 +471,19 @@ namespace ManagerService.Controllers
|
|||||||
FileDownloadName = fileName
|
FileDownloadName = fileName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Les trois `catch` renvoyaient `null` : l'app recevait un 200 vide et croyait
|
||||||
|
// la visite exportée. Les codes ci-dessous étaient déjà écrits, en commentaire.
|
||||||
catch (ArgumentNullException ex)
|
catch (ArgumentNullException ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new BadRequestObjectResult(ex.Message);
|
||||||
//return new BadRequestObjectResult(ex.Message) { };
|
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException ex)
|
catch (KeyNotFoundException ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new NotFoundObjectResult(ex.Message);
|
||||||
//return new NotFoundObjectResult(ex.Message) { };
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return null;
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||||
//return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
// Une URL relative afficherait une image cassée dans la boîte de
|
||||||
|
// réception : mieux vaut le bandeau sans logo que ça.
|
||||||
|
LogoUrl = logoUrl != null && logoUrl.StartsWith("http") ? logoUrl : "";
|
||||||
|
LandingUrl = landingUrl ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
public static string Render(string title, string bodyHtml, string ctaText = null, string ctaUrl = null)
|
public static string Render(string title, string bodyHtml, string ctaText = null, string ctaUrl = null)
|
||||||
{
|
{
|
||||||
|
var hasCta = !string.IsNullOrEmpty(ctaText) && !string.IsNullOrEmpty(ctaUrl);
|
||||||
|
|
||||||
|
// 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 = "";
|
var ctaHtml = "";
|
||||||
if (!string.IsNullOrEmpty(ctaText) && !string.IsNullOrEmpty(ctaUrl))
|
if (hasCta)
|
||||||
{
|
{
|
||||||
ctaHtml = $@"
|
ctaHtml = $@"
|
||||||
<table role=""presentation"" cellpadding=""0"" cellspacing=""0"" style=""margin: 28px 0 8px;"">
|
<table role=""presentation"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""margin:32px 0 0;"">
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""border-radius: 12px; background: #0df2df;"">
|
<td align=""center"" bgcolor=""#0df2df"" style=""border-radius:10px;"">
|
||||||
<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>
|
<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>
|
||||||
</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>
|
return $@"<!DOCTYPE html>
|
||||||
<html lang=""fr"">
|
<html lang=""fr"">
|
||||||
<head><meta charset=""utf-8""><meta name=""viewport"" content=""width=device-width, initial-scale=1.0""></head>
|
<head>
|
||||||
<body style=""margin:0; padding:0; background:#f1f5f9; font-family: Arial, sans-serif;"">
|
<meta charset=""utf-8"">
|
||||||
<table role=""presentation"" width=""100%"" cellpadding=""0"" cellspacing=""0"" style=""background:#f1f5f9; padding: 32px 0;"">
|
<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>
|
<tr>
|
||||||
<td align=""center"">
|
<td align=""center"" style=""padding:40px 16px;"">
|
||||||
<table role=""presentation"" width=""560"" cellpadding=""0"" cellspacing=""0"" style=""background:#ffffff; border-radius: 16px; overflow:hidden;"">
|
|
||||||
|
<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>
|
<tr>
|
||||||
<td style=""background:#0a1222; padding: 24px 32px;"">
|
<td bgcolor=""#0a1222"" style=""padding:30px 40px;"">
|
||||||
<span style=""font-size: 18px; font-weight: 800; color:#ffffff;"">MyInfoMate</span>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding: 32px;"">
|
<td height=""4"" bgcolor=""#0df2df"" style=""height:4px; line-height:4px; font-size:0;""> </td>
|
||||||
<h1 style=""margin:0 0 16px; font-size: 20px; color:#0f172a;"">{title}</h1>
|
</tr>
|
||||||
<div style=""font-size: 14px; line-height: 1.6; color:#334155;"">{bodyHtml}</div>
|
|
||||||
|
<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}
|
{ctaHtml}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style=""padding: 20px 32px; border-top: 1px solid #e2e8f0; font-size: 12px; color:#94a3b8;"">
|
<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;"">
|
||||||
MyInfoMate — {DateTime.UtcNow.Year}
|
<strong style=""color:#0a1222;"">MyInfoMate</strong> — la plateforme de contenu des lieux culturels.<br>
|
||||||
|
© {DateTime.UtcNow.Year} Unov{landingHtml}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
BIN
ManagerService/wwwroot/email-logo.png
Normal file
BIN
ManagerService/wwwroot/email-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Loading…
x
Reference in New Issue
Block a user