mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-30 20:54:49 +02:00
Simplify and fix generateBaseUrl
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
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(() => ({
|
||||
@@ -22,365 +21,16 @@ describe("generateBaseUrl", () => {
|
||||
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",
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
};
|
||||
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("http://192.168.1.100:8080");
|
||||
});
|
||||
it("should return localhost with port", () => {
|
||||
const requestUrl = new URL("http://localhost:5173");
|
||||
const result = generateBaseUrl(requestUrl);
|
||||
expect(result).toBe("http://localhost:5173");
|
||||
});
|
||||
|
||||
describe("Production Environment", () => {
|
||||
beforeEach(() => {
|
||||
mockEnv.NODE_ENV = "production";
|
||||
});
|
||||
it("should return tenant domain", () => {
|
||||
const requestUrl = new URL("https://example.com");
|
||||
|
||||
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",
|
||||
domain: "acme",
|
||||
longName: "ACME Corp",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "acme",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "new-tenant",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://new-tenant.old-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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "new-tenant",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://new-tenant.old-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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const result = generateBaseUrl(requestUrl, tenant);
|
||||
expect(result).toBe("https://tenant.admin.api.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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "acme",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "acme",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
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",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "",
|
||||
setupState: "SETTINGS",
|
||||
logo: null,
|
||||
links: { website: "", imprint: "", privacyStatement: "" },
|
||||
domain: "tenant.example.com",
|
||||
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");
|
||||
});
|
||||
const result = generateBaseUrl(requestUrl);
|
||||
expect(result).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,7 +147,7 @@ export async function sendPinResetEmail(
|
||||
locale,
|
||||
user,
|
||||
tenant,
|
||||
loginUrl: generateBaseUrl(requestUrl, tenant) ?? "http://localhost:5173",
|
||||
loginUrl: generateBaseUrl(requestUrl),
|
||||
},
|
||||
});
|
||||
const html = renderOutputToHtml(emailRender);
|
||||
@@ -399,23 +399,10 @@ export async function sendAppointmentUpdatedEmail(
|
||||
* @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.
|
||||
// Exclude the system tenant when determining if we should use the tenant's domain for the URL, as the system tenant does not have a domain and should use the main domain.
|
||||
if (tenant?.domain && tenant.id !== "system") {
|
||||
return `${protocol}//${tenant.domain}.${hostname}${port}`;
|
||||
}
|
||||
|
||||
// For global admin or no tenant, use main domain
|
||||
export function generateBaseUrl(url: URL): string {
|
||||
const protocol = url.protocol;
|
||||
const port = url.port ? `:${url.port}` : "";
|
||||
const hostname = url.hostname;
|
||||
return `${protocol}//${hostname}${port}`;
|
||||
}
|
||||
|
||||
@@ -437,7 +424,7 @@ export async function sendConfirmationEmail(
|
||||
requestUrl: URL,
|
||||
): Promise<void> {
|
||||
// Generate appropriate base URL if request URL is provided
|
||||
const baseUrl = requestUrl ? generateBaseUrl(requestUrl, tenant) : "http://localhost:5173";
|
||||
const baseUrl = generateBaseUrl(requestUrl);
|
||||
const confirmUrl = `${baseUrl}/confirm/${confirmationCode}`;
|
||||
const recipient = user;
|
||||
// Generate email
|
||||
|
||||
Reference in New Issue
Block a user