mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
mail service restructuring
This commit is contained in:
@@ -1 +1 @@
|
||||
377
|
||||
378
|
||||
|
||||
+28
-11
@@ -115,10 +115,11 @@ public class PreviewWhitelistFunction
|
||||
}
|
||||
|
||||
// Fire-and-forget: send welcome email if notification email is configured
|
||||
if (!string.IsNullOrWhiteSpace(tenantConfig.PreviewNotificationEmail))
|
||||
var notificationEmail = await _previewWhitelistService.GetNotificationEmailAsync(tenantId);
|
||||
if (!string.IsNullOrWhiteSpace(notificationEmail))
|
||||
{
|
||||
_ = _resendEmailService.SendPreviewApprovedEmailAsync(
|
||||
tenantConfig.PreviewNotificationEmail, tenantConfig.DomainName)
|
||||
notificationEmail, tenantConfig.DomainName)
|
||||
.ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException,
|
||||
"Fire-and-forget welcome email failed for tenant {TenantId}", tenantId),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
@@ -161,6 +162,24 @@ public class PreviewWhitelistFunction
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /api/preview/notification-email/{tenantId}
|
||||
/// Returns the notification email for a tenant. Galactic Admin only.
|
||||
/// </summary>
|
||||
[Function("GetPreviewNotificationEmail")]
|
||||
[Authorize]
|
||||
public async Task<HttpResponseData> GetNotificationEmail(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "preview/notification-email/{tenantId}")] HttpRequestData req,
|
||||
string tenantId,
|
||||
FunctionContext context)
|
||||
{
|
||||
var email = await _previewWhitelistService.GetNotificationEmailAsync(tenantId);
|
||||
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new { email = email ?? "" });
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PUT /api/preview/notification-email
|
||||
/// Saves the caller's notification email for Private Preview approval.
|
||||
@@ -192,9 +211,7 @@ public class PreviewWhitelistFunction
|
||||
return bad;
|
||||
}
|
||||
|
||||
var tenantConfig = await _tenantConfigurationService.GetConfigurationAsync(tenantId);
|
||||
tenantConfig.PreviewNotificationEmail = email;
|
||||
await _tenantConfigurationService.SaveConfigurationAsync(tenantConfig);
|
||||
await _previewWhitelistService.SaveNotificationEmailAsync(tenantId, email);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Preview notification email updated for tenant {TenantId}: {Email}",
|
||||
@@ -208,7 +225,7 @@ public class PreviewWhitelistFunction
|
||||
/// <summary>
|
||||
/// POST /api/preview/send-welcome-email/{tenantId}
|
||||
/// Sends (or resends) the Private Preview welcome email. Galactic Admin only.
|
||||
/// Accepts optional { email } in body — if provided, saves it to tenant config before sending.
|
||||
/// Accepts optional { email } in body — if provided, saves it to PreviewWhitelist table before sending.
|
||||
/// </summary>
|
||||
[Function("SendPreviewWelcomeEmail")]
|
||||
[Authorize]
|
||||
@@ -219,18 +236,17 @@ public class PreviewWhitelistFunction
|
||||
{
|
||||
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
|
||||
|
||||
var tenantConfig = await _tenantConfigurationService.GetConfigurationAsync(tenantId);
|
||||
|
||||
// If the caller provides an email in the body, save it first
|
||||
var body = await req.ReadFromJsonAsync<SaveNotificationEmailRequest>();
|
||||
var bodyEmail = body?.Email?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(bodyEmail))
|
||||
{
|
||||
tenantConfig.PreviewNotificationEmail = bodyEmail;
|
||||
await _tenantConfigurationService.SaveConfigurationAsync(tenantConfig);
|
||||
await _previewWhitelistService.SaveNotificationEmailAsync(tenantId, bodyEmail);
|
||||
}
|
||||
|
||||
var email = tenantConfig.PreviewNotificationEmail;
|
||||
var email = !string.IsNullOrWhiteSpace(bodyEmail)
|
||||
? bodyEmail
|
||||
: await _previewWhitelistService.GetNotificationEmailAsync(tenantId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
@@ -239,6 +255,7 @@ public class PreviewWhitelistFunction
|
||||
return bad;
|
||||
}
|
||||
|
||||
var tenantConfig = await _tenantConfigurationService.GetConfigurationAsync(tenantId);
|
||||
await _resendEmailService.SendPreviewApprovedEmailAsync(email, tenantConfig.DomainName);
|
||||
|
||||
var principal = context.GetUser();
|
||||
|
||||
@@ -152,6 +152,7 @@ public static class EndpointAccessPolicyCatalog
|
||||
new("GET", "preview/whitelist", EndpointPolicy.GalacticAdminOnly),
|
||||
new("POST", "preview/whitelist/{tenantId}", EndpointPolicy.GalacticAdminOnly),
|
||||
new("DELETE", "preview/whitelist/{tenantId}", EndpointPolicy.GalacticAdminOnly),
|
||||
new("GET", "preview/notification-email/{tenantId}", EndpointPolicy.GalacticAdminOnly),
|
||||
new("POST", "preview/send-welcome-email/{tenantId}", EndpointPolicy.GalacticAdminOnly),
|
||||
new("GET", "galactic/sessions", EndpointPolicy.GalacticAdminOnly),
|
||||
new("GET", "galactic/audit/logs", EndpointPolicy.GalacticAdminOnly),
|
||||
|
||||
@@ -113,6 +113,52 @@ public class PreviewWhitelistService
|
||||
|
||||
return results.OrderBy(e => e.PartitionKey).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the notification email for a tenant (stored in PreviewWhitelist table).
|
||||
/// </summary>
|
||||
public async Task<string?> GetNotificationEmailAsync(string tenantId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.PreviewWhitelist);
|
||||
var entity = await tableClient.GetEntityAsync<PreviewNotificationEntity>(tenantId, "notification-email");
|
||||
return entity?.Value?.Email;
|
||||
}
|
||||
catch (RequestFailedException ex) when (ex.Status == 404)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read notification email for tenant {TenantId}", tenantId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves (or clears) the notification email for a tenant.
|
||||
/// </summary>
|
||||
public async Task SaveNotificationEmailAsync(string tenantId, string? email)
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.PreviewWhitelist);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
// Clear: delete the row if it exists
|
||||
try { await tableClient.DeleteEntityAsync(tenantId, "notification-email"); }
|
||||
catch (RequestFailedException ex) when (ex.Status == 404) { /* already gone */ }
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = new PreviewNotificationEntity
|
||||
{
|
||||
PartitionKey = tenantId,
|
||||
RowKey = "notification-email",
|
||||
Email = email.Trim()
|
||||
};
|
||||
await tableClient.UpsertEntityAsync(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,3 +174,18 @@ public class PreviewWhitelistEntity : ITableEntity
|
||||
public DateTime ApprovedAt { get; set; }
|
||||
public string ApprovedBy { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entity storing the notification email for a tenant in the PreviewWhitelist table.
|
||||
/// PartitionKey = TenantId, RowKey = "notification-email".
|
||||
/// Temporary — remove after GA.
|
||||
/// </summary>
|
||||
public class PreviewNotificationEntity : ITableEntity
|
||||
{
|
||||
public string PartitionKey { get; set; } = string.Empty; // TenantId
|
||||
public string RowKey { get; set; } = "notification-email";
|
||||
public DateTimeOffset? Timestamp { get; set; }
|
||||
public ETag ETag { get; set; }
|
||||
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -33,14 +33,6 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// </summary>
|
||||
public string UpdatedBy { get; set; }
|
||||
|
||||
// ===== PRIVATE PREVIEW =====
|
||||
|
||||
/// <summary>
|
||||
/// Email address for Private Preview approval notification.
|
||||
/// Entered by the user on the waitlist page or by a Galactic Admin.
|
||||
/// </summary>
|
||||
public string PreviewNotificationEmail { get; set; }
|
||||
|
||||
// ===== TENANT STATUS =====
|
||||
|
||||
/// <summary>
|
||||
|
||||
+18
-7
@@ -13,7 +13,6 @@ export interface TenantConfiguration {
|
||||
disabled: boolean;
|
||||
disabledReason?: string;
|
||||
disabledUntil?: string;
|
||||
previewNotificationEmail?: string;
|
||||
rateLimitRequestsPerMinute: number;
|
||||
manufacturerWhitelist: string;
|
||||
modelWhitelist: string;
|
||||
@@ -60,6 +59,7 @@ export function TenantManagementSection({
|
||||
// Preview Whitelist state
|
||||
const [togglingPreviewTenant, setTogglingPreviewTenant] = useState<string | null>(null);
|
||||
const [sendingWelcomeEmail, setSendingWelcomeEmail] = useState(false);
|
||||
const [notificationEmail, setNotificationEmail] = useState("");
|
||||
|
||||
// Filter and sort tenants
|
||||
const filteredTenants = tenants.filter(t => {
|
||||
@@ -403,8 +403,19 @@ export function TenantManagementSection({
|
||||
: "Approve"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
setEditingTenant(tenant);
|
||||
setNotificationEmail("");
|
||||
try {
|
||||
const resp = await authenticatedFetch(
|
||||
`${API_BASE_URL}/api/preview/notification-email/${tenant.tenantId}`,
|
||||
getAccessToken
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
setNotificationEmail(data.email || "");
|
||||
}
|
||||
} catch { /* best-effort */ }
|
||||
}}
|
||||
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
@@ -529,14 +540,14 @@ export function TenantManagementSection({
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={editingTenant.previewNotificationEmail || ''}
|
||||
onChange={(e) => setEditingTenant({ ...editingTenant, previewNotificationEmail: e.target.value })}
|
||||
value={notificationEmail}
|
||||
onChange={(e) => setNotificationEmail(e.target.value)}
|
||||
placeholder="user@example.com"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSendWelcomeEmail(editingTenant.tenantId, editingTenant.previewNotificationEmail)}
|
||||
disabled={sendingWelcomeEmail || !editingTenant.previewNotificationEmail?.trim()}
|
||||
onClick={() => handleSendWelcomeEmail(editingTenant.tenantId, notificationEmail)}
|
||||
disabled={sendingWelcomeEmail || !notificationEmail.trim()}
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap flex items-center gap-1.5"
|
||||
title="Send or resend the Private Preview welcome email"
|
||||
>
|
||||
@@ -556,7 +567,7 @@ export function TenantManagementSection({
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-indigo-600 mt-2">
|
||||
Save changes first if you edited the email. The welcome email is also sent automatically on approval if set.
|
||||
The email is saved and sent in one step. Also sent automatically on approval if set.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user