mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-08 18:27:39 +02:00
Determine correct url for confirmation pages in mail templates.
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { generateBaseUrl } from "../email-service";
|
||||
import type { SelectTenant } from "$lib/server/db/central-schema";
|
||||
|
||||
// Mock NODE_ENV
|
||||
const mockEnv = vi.hoisted(() => ({ NODE_ENV: "development" }));
|
||||
|
||||
vi.mock("$env/dynamic/private", () => ({
|
||||
env: mockEnv
|
||||
}));
|
||||
|
||||
describe("generateBaseUrl", () => {
|
||||
beforeEach(() => {
|
||||
// Reset NODE_ENV to development for each test
|
||||
mockEnv.NODE_ENV = "development";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Development/Local Environment", () => {
|
||||
it("should return localhost URL regardless of tenant", () => {
|
||||
const requestUrl = new URL("http://localhost:5173");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://localhost:5173");
|
||||
});
|
||||
|
||||
it("should return localhost URL for null tenant", () => {
|
||||
const requestUrl = new URL("http://localhost:3000");
|
||||
|
||||
const result = generateBaseUrl(requestUrl, null);
|
||||
expect(result).toBe("http://localhost:3000");
|
||||
});
|
||||
|
||||
it("should preserve port for localhost", () => {
|
||||
const requestUrl = new URL("http://localhost:8080");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://localhost:8080");
|
||||
});
|
||||
|
||||
it("should handle 127.x.x.x addresses", () => {
|
||||
const requestUrl = new URL("http://127.0.0.1:3000");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://127.0.0.1:3000");
|
||||
});
|
||||
|
||||
it("should handle 192.168.x.x addresses", () => {
|
||||
const requestUrl = new URL("http://192.168.1.100:8080");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://192.168.1.100:8080");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Production Environment", () => {
|
||||
beforeEach(() => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
});
|
||||
|
||||
it("should return main domain for null tenant", () => {
|
||||
const requestUrl = new URL("https://example.com");
|
||||
|
||||
const result = generateBaseUrl(requestUrl, null);
|
||||
expect(result).toBe("https://example.com");
|
||||
});
|
||||
|
||||
it("should return main domain with port for null tenant", () => {
|
||||
const requestUrl = new URL("https://example.com:8443");
|
||||
|
||||
const result = generateBaseUrl(requestUrl, null);
|
||||
expect(result).toBe("https://example.com:8443");
|
||||
});
|
||||
|
||||
it("should create subdomain URL for tenant on main domain", () => {
|
||||
const requestUrl = new URL("https://example.com");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://acme.example.com");
|
||||
});
|
||||
|
||||
it("should create subdomain URL with port", () => {
|
||||
const requestUrl = new URL("https://example.com:8443");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://acme.example.com:8443");
|
||||
});
|
||||
|
||||
it("should replace existing subdomain with tenant shortName", () => {
|
||||
const requestUrl = new URL("https://old-tenant.example.com");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-2",
|
||||
shortName: "new-tenant",
|
||||
longName: "New Tenant Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://new-tenant.example.com");
|
||||
});
|
||||
|
||||
it("should replace existing subdomain with port", () => {
|
||||
const requestUrl = new URL("https://old-tenant.example.com:8443");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-2",
|
||||
shortName: "new-tenant",
|
||||
longName: "New Tenant Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://new-tenant.example.com:8443");
|
||||
});
|
||||
|
||||
it("should handle complex subdomains (keep last two parts)", () => {
|
||||
const requestUrl = new URL("https://admin.api.example.com");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "tenant",
|
||||
longName: "Tenant Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://tenant.example.com");
|
||||
});
|
||||
|
||||
it("should handle http protocol", () => {
|
||||
const requestUrl = new URL("http://example.com");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://acme.example.com");
|
||||
});
|
||||
|
||||
it("should return main domain when tenant has no shortName", () => {
|
||||
const requestUrl = new URL("https://example.com");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "", // Empty shortName
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle single domain names in production", () => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
const requestUrl = new URL("https://app");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://acme.app");
|
||||
});
|
||||
|
||||
it("should handle localhost in production (still treated as development)", () => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
const requestUrl = new URL("https://localhost:8443");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://localhost:8443");
|
||||
});
|
||||
|
||||
it("should handle IP addresses as development", () => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
const requestUrl = new URL("https://192.168.1.100:8443");
|
||||
const tenant: SelectTenant = {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
primaryColor: "#000000",
|
||||
backgroundColor: "#ffffff",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://192.168.1.100:8443");
|
||||
});
|
||||
|
||||
it("should handle default HTTP port (80)", () => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
const requestUrl = new URL("http://example.com:80");
|
||||
|
||||
// URL constructor should handle port 80 correctly
|
||||
const result = generateBaseUrl(requestUrl, null);
|
||||
// Default HTTP port shouldn't be included in URL
|
||||
expect(result).toBe("http://example.com");
|
||||
});
|
||||
|
||||
it("should handle default HTTPS port (443)", () => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
const requestUrl = new URL("https://example.com:443");
|
||||
|
||||
// URL constructor should handle port 443 correctly
|
||||
const result = generateBaseUrl(requestUrl, null);
|
||||
// Default HTTPS port shouldn't be included in URL
|
||||
expect(result).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -187,12 +187,47 @@ export async function sendAppointmentUpdatedEmail(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate base URL for email templates based on request URL and tenant
|
||||
* @param {URL} requestUrl - The request URL
|
||||
* @param {SelectTenant | null} tenant - Tenant information (null for global admin)
|
||||
* @returns {string} The appropriate base URL
|
||||
*/
|
||||
export function generateBaseUrl(requestUrl: URL, tenant: SelectTenant | null): string {
|
||||
const protocol = requestUrl.protocol;
|
||||
const port = requestUrl.port ? `:${requestUrl.port}` : '';
|
||||
const hostname = requestUrl.hostname;
|
||||
|
||||
// In development, always use the original hostname regardless of tenant
|
||||
if (hostname === 'localhost' || hostname.startsWith('127.') || hostname.startsWith('192.168.')) {
|
||||
return `${protocol}//${hostname}${port}`;
|
||||
}
|
||||
|
||||
// In production, handle tenant subdomains
|
||||
if (tenant?.shortName) {
|
||||
const parts = hostname.split('.');
|
||||
|
||||
if (parts.length > 2) {
|
||||
// Complex subdomain - use only the last two parts (domain.tld) and add tenant
|
||||
const domain = parts.slice(-2).join('.');
|
||||
return `${protocol}//${tenant.shortName}.${domain}${port}`;
|
||||
} else {
|
||||
// Main domain, prepend tenant subdomain
|
||||
return `${protocol}//${tenant.shortName}.${hostname}${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
// For global admin or no tenant, use main domain
|
||||
return `${protocol}//${hostname}${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send registration confirmation email with one-time code
|
||||
* @param {SelectClient | SelectStaff} user - Database user object
|
||||
* @param {SelectTenant} tenant - Tenant information for branding
|
||||
* @param {string} confirmationCode - One-time confirmation code
|
||||
* @param {number} [expirationMinutes=15] - Code expiration time in minutes
|
||||
* @param {URL} [requestUrl] - Request URL for generating baseUrl
|
||||
* @throws {Error} When email sending fails
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -200,13 +235,23 @@ export async function sendConfirmationEmail(
|
||||
user: { id: string; email: string | null; name: string | null; language?: string | null },
|
||||
tenant: SelectTenant,
|
||||
confirmationCode: string,
|
||||
expirationMinutes: number = 15
|
||||
expirationMinutes: number = 15,
|
||||
requestUrl?: URL
|
||||
): Promise<void> {
|
||||
const recipient = createEmailRecipient(user);
|
||||
const language = (recipient.language as Language) || "en";
|
||||
const subject = language === "en" ? "Confirm Your Registration" : "Registrierung bestätigen";
|
||||
|
||||
await sendTemplatedEmail("confirmation", recipient, subject, language, tenant, {
|
||||
// Generate appropriate base URL if request URL is provided
|
||||
const baseUrl = requestUrl ? generateBaseUrl(requestUrl, tenant) : 'http://localhost:5173';
|
||||
|
||||
// Create enhanced tenant object with baseUrl
|
||||
const tenantWithBaseUrl = {
|
||||
...tenant,
|
||||
baseUrl
|
||||
};
|
||||
|
||||
await sendTemplatedEmail("confirmation", recipient, subject, language, tenantWithBaseUrl, {
|
||||
confirmationCode,
|
||||
expirationMinutes
|
||||
});
|
||||
|
||||
@@ -1,173 +1,174 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Registrierung bestätigen</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Registrierung bestätigen</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
|
||||
background-color: {
|
||||
{
|
||||
tenant.backgroundColor
|
||||
background-color: {
|
||||
{
|
||||
tenant.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
;
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.confirmation-code {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
border: 2px solid {
|
||||
{
|
||||
tenant.primaryColor
|
||||
.header {
|
||||
background-color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
|
||||
color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
.logo {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
;
|
||||
letter-spacing: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
;
|
||||
text-decoration: none;
|
||||
}
|
||||
.confirmation-code {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
.warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
color: #856404;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
border: 2px solid {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
}
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
{{#if tenant.logo}}
|
||||
<img src="data:image/png;base64,{{tenant.logo}}" alt="{{tenant.longName}}"
|
||||
style="max-height: 60px; margin-bottom: 10px" />
|
||||
{{/if}}
|
||||
<div class="logo">{{tenant.longName}}</div>
|
||||
<h1 style="margin: 0; font-size: 28px">Registrierung bestätigen</h1>
|
||||
</div>
|
||||
;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
<div class="content">
|
||||
<p>Hallo,</p>
|
||||
.code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
|
||||
<p>
|
||||
vielen Dank für Ihre Registrierung bei <strong>{{tenant.longName}}</strong>. Um Ihre
|
||||
Registrierung abzuschließen, verwenden Sie bitte den folgenden Bestätigungscode:
|
||||
</p>
|
||||
color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
}
|
||||
|
||||
<div class="confirmation-code">
|
||||
<div>Ihr Bestätigungscode:</div>
|
||||
<div class="code">{{confirmationCode}}</div>
|
||||
<div style="font-size: 14px; color: #666; margin-top: 10px">
|
||||
Geben Sie diesen Code in das Bestätigungsfeld ein
|
||||
;
|
||||
letter-spacing: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: {
|
||||
{
|
||||
tenant.primaryColor
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
color: #856404;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
{{#if tenant.logo}}
|
||||
<img
|
||||
src="data:image/png;base64,{{tenant.logo}}"
|
||||
alt="{{tenant.longName}}"
|
||||
style="max-height: 60px; margin-bottom: 10px"
|
||||
/>
|
||||
{{/if}}
|
||||
<div class="logo">{{tenant.longName}}</div>
|
||||
<h1 style="margin: 0; font-size: 28px">Registrierung bestätigen</h1>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Hallo,</p>
|
||||
|
||||
<p>
|
||||
vielen Dank für Ihre Registrierung bei <strong>{{tenant.longName}}</strong>. Um Ihre
|
||||
Registrierung abzuschließen, verwenden Sie bitte den folgenden Bestätigungscode:
|
||||
</p>
|
||||
|
||||
<div class="confirmation-code">
|
||||
<div>Ihr Bestätigungscode:</div>
|
||||
<div class="code">{{confirmationCode}}</div>
|
||||
<div style="font-size: 14px; color: #666; margin-top: 10px">
|
||||
Geben Sie diesen Code in das Bestätigungsfeld ein
|
||||
</div>
|
||||
<a href="{{tenant.baseUrl}}/confirm/{{confirmationCode}}">Oder hier klicken.</a>
|
||||
</div>
|
||||
{{tenant.baseUrl}}/confirm/{{confirmationCode}}
|
||||
|
||||
<div class="warning">
|
||||
<strong>Wichtiger Hinweis:</strong> Dieser Code ist nur für
|
||||
<strong>{{expirationMinutes}} Minuten</strong> gültig und kann nur einmal verwendet
|
||||
werden.
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Falls Sie sich nicht bei {{tenant.longName}} registriert haben, können Sie diese E-Mail
|
||||
ignorieren.
|
||||
</p>
|
||||
|
||||
<p>Bei Fragen wenden Sie sich gerne an unser Support-Team.</p>
|
||||
|
||||
<p>
|
||||
Mit freundlichen Grüßen<br />
|
||||
Das {{tenant.longName}} Team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
<strong>Wichtiger Hinweis:</strong> Dieser Code ist nur für
|
||||
<strong>{{expirationMinutes}} Minuten</strong> gültig und kann nur einmal verwendet
|
||||
werden.
|
||||
<div class="footer">
|
||||
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail.</p>
|
||||
<p>© {{tenant.longName}} - Sicher, verschlüsselt, vertrauenswürdig</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Falls Sie sich nicht bei {{tenant.longName}} registriert haben, können Sie diese E-Mail
|
||||
ignorieren.
|
||||
</p>
|
||||
|
||||
<p>Bei Fragen wenden Sie sich gerne an unser Support-Team.</p>
|
||||
|
||||
<p>
|
||||
Mit freundlichen Grüßen<br />
|
||||
Das {{tenant.longName}} Team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail.</p>
|
||||
<p>© {{tenant.longName}} - Sicher, verschlüsselt, vertrauenswürdig</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,6 +10,8 @@ vielen Dank für Ihre Registrierung bei {{tenant.longName}}. Um Ihre Registrieru
|
||||
|
||||
Geben Sie diesen Code in das Bestätigungsfeld ein, um Ihre Registrierung zu vervollständigen.
|
||||
|
||||
Alternativ können Sie direkt diese Seite besuchen: {{tenant.baseUrl}}/confirm/{{confirmationCode}}
|
||||
|
||||
WICHTIGER HINWEIS: Dieser Code ist nur für {{expirationMinutes}} Minuten gültig und kann nur einmal verwendet werden.
|
||||
|
||||
Falls Sie sich nicht bei {{tenant.longName}} registriert haben, können Sie diese E-Mail ignorieren.
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
<div style="font-size: 14px; color: #666; margin-top: 10px">
|
||||
Enter this code in the confirmation field
|
||||
</div>
|
||||
<a href="{{tenant.baseUrl}}/confirm/{{confirmationCode}}">Or simply click here.</a>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
|
||||
@@ -10,6 +10,8 @@ Thank you for registering with {{tenant.longName}}. To complete your registratio
|
||||
|
||||
Enter this code in the confirmation field to complete your registration.
|
||||
|
||||
Or simply visit this site: {{tenant.baseUrl}}/confirm/{{confirmationCode}}
|
||||
|
||||
IMPORTANT NOTICE: This code is valid for {{expirationMinutes}} minutes only and can be used only once.
|
||||
|
||||
If you did not register with {{tenant.longName}}, you can safely ignore this email.
|
||||
|
||||
@@ -71,7 +71,7 @@ export class UserService {
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
static async createUser(userData: UserCreation) {
|
||||
static async createUser(userData: UserCreation, requestUrl?: URL) {
|
||||
const log = logger.setContext("UserService");
|
||||
log.debug("Creating new user account", {
|
||||
email: userData.email,
|
||||
@@ -138,7 +138,8 @@ export class UserService {
|
||||
result[0],
|
||||
tenant,
|
||||
result[0].token,
|
||||
10 // 10 minutes expiration to match tokenValidUntil
|
||||
10, // 10 minutes expiration to match tokenValidUntil
|
||||
requestUrl
|
||||
);
|
||||
log.debug("Confirmation email sent successfully", {
|
||||
userId: result[0].id,
|
||||
@@ -165,8 +166,9 @@ export class UserService {
|
||||
/**
|
||||
* Resend the confirmation email for a user
|
||||
* @param email - Email of the user to confirm
|
||||
* @param requestUrl - Optional request URL for generating correct baseUrl
|
||||
*/
|
||||
static async resendConfirmationEmail(email: string): Promise<void> {
|
||||
static async resendConfirmationEmail(email: string, requestUrl?: URL): Promise<void> {
|
||||
const log = logger.setContext("UserService");
|
||||
log.debug("Resending confirmation email", { email });
|
||||
|
||||
@@ -195,7 +197,8 @@ export class UserService {
|
||||
user,
|
||||
tenant,
|
||||
token,
|
||||
10 // 10 minutes expiration to match tokenValidUntil
|
||||
10, // 10 minutes expiration to match tokenValidUntil
|
||||
requestUrl
|
||||
);
|
||||
log.debug("Confirmation email sent successfully", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -24,7 +24,7 @@ export const actions: Actions = {
|
||||
});
|
||||
}
|
||||
|
||||
await UserService.resendConfirmationEmail(form.data.email);
|
||||
await UserService.resendConfirmationEmail(form.data.email, event.url);
|
||||
|
||||
log.debug("Resent confirmation e-mail", {
|
||||
email: form.data.email
|
||||
|
||||
@@ -30,7 +30,7 @@ export const actions: Actions = {
|
||||
email: form.data.email,
|
||||
passphrase: form.data.passphrase,
|
||||
language: form.data.language
|
||||
});
|
||||
}, event.url);
|
||||
const hasPasskey = false;
|
||||
|
||||
log.debug("Admin account created successfully", {
|
||||
|
||||
@@ -105,7 +105,7 @@ registerOpenAPIRoute("/admin/init", "POST", {
|
||||
}
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
export const POST: RequestHandler = async ({ request, cookies, url }) => {
|
||||
const log = logger.setContext("API");
|
||||
|
||||
try {
|
||||
@@ -140,7 +140,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
email: body.email,
|
||||
passphrase: body.passphrase, // Will be undefined if passkey is used
|
||||
language: body.language || "de"
|
||||
});
|
||||
}, url);
|
||||
|
||||
// Add the passkey to the admin account if provided
|
||||
if (hasPasskey) {
|
||||
|
||||
@@ -123,7 +123,7 @@ registerOpenAPIRoute("/auth/register", "POST", {
|
||||
}
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
export const POST: RequestHandler = async ({ request, cookies, url }) => {
|
||||
const log = logger.setContext("API");
|
||||
|
||||
try {
|
||||
@@ -187,7 +187,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
tenantId: finalTenantId,
|
||||
passphrase: body.passphrase,
|
||||
language: inviteUsed?.language || body.language || "de"
|
||||
});
|
||||
}, url);
|
||||
|
||||
// Add the passkey to the user account if provided
|
||||
if (body.passkey) {
|
||||
|
||||
@@ -68,12 +68,12 @@ registerOpenAPIRoute("/auth/resend-confirmation", "POST", {
|
||||
}
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
export const POST: RequestHandler = async ({ request, url }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Resend confirmation email
|
||||
await UserService.resendConfirmationEmail(body.email);
|
||||
await UserService.resendConfirmationEmail(body.email, url);
|
||||
|
||||
return json(
|
||||
{
|
||||
|
||||
@@ -104,6 +104,8 @@ export const authHandle: Handle = async ({ event, resolve }) => {
|
||||
sessionId: sessionData.sessionId
|
||||
};
|
||||
|
||||
logger.debug(`Added user information for ${sessionData.user.id}`);
|
||||
|
||||
if (isGlobalAdminPath && !AuthorizationService.hasRole(sessionData.user, "GLOBAL_ADMIN")) {
|
||||
return new Response(JSON.stringify({ error: "Authentication failed" }), {
|
||||
status: 403,
|
||||
|
||||
Reference in New Issue
Block a user