diff --git a/src/lib/server/email/__tests__/tenant-admin-invite.test.ts b/src/lib/server/email/__tests__/tenant-admin-invite.test.ts deleted file mode 100644 index bc16a0c..0000000 --- a/src/lib/server/email/__tests__/tenant-admin-invite.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { sendTenantAdminInviteEmail } from "../email-service"; - -describe("sendTenantAdminInviteEmail", () => { - const mockTenant = { - id: "test-tenant-id", - shortName: "testcorp", - longName: "Test Corporation GmbH", - descriptions: { en: "A test corporation" }, - languages: ["en"], - defaultLanguage: "en", - databaseUrl: "postgresql://test", - setupState: "SETTINGS" as const, - createdAt: new Date(), - updatedAt: new Date(), - logo: null, - links: { - website: "https://testcorp.com", - imprint: "https://testcorp.com/imprint", - privacyStatement: "https://testcorp.com/privacy", - }, - }; - - it("should accept correct parameters and not throw for German language", () => { - const adminEmail = "admin@testcorp.com"; - const adminName = "Test Admin"; - const registrationUrl = "http://localhost:5173/register?tenant=test"; - - // This test just ensures the function accepts the right parameters - // and doesn't throw on setup (actual email sending will fail without SMTP config) - expect(() => { - sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "de"); - }).not.toThrow(); - }); - - it("should accept correct parameters and not throw for English language", () => { - const adminEmail = "admin@testcorp.com"; - const adminName = "Test Admin"; - const registrationUrl = "http://localhost:5173/register?tenant=test"; - - expect(() => { - sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "en"); - }).not.toThrow(); - }); - - it("should accept correct parameters and not throw with default language", () => { - const adminEmail = "admin@testcorp.com"; - const adminName = "Test Admin"; - const registrationUrl = "http://localhost:5173/register?tenant=test"; - - expect(() => { - sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl); - }).not.toThrow(); - }); -}); diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts index b0c30d4..8e43231 100644 --- a/src/lib/server/email/email-service.ts +++ b/src/lib/server/email/email-service.ts @@ -254,37 +254,6 @@ export async function sendConfirmationEmail( }); } -/** - * Send tenant administrator invitation email - * @param {string} adminEmail - Email address of the invited administrator - * @param {string} adminName - Name of the invited administrator - * @param {SelectTenant} tenant - Tenant information for branding - * @param {string} registrationUrl - URL for administrator to register - * @param {Language} [language="en"] - Email language - * @throws {Error} When email sending fails - * @returns {Promise} - */ -export async function sendTenantAdminInviteEmail( - adminEmail: string, - adminName: string, - tenant: SelectTenant, - registrationUrl: string, - language: Language = "en", -): Promise { - const recipient: EmailRecipient = { - email: adminEmail, - name: adminName, - language, - }; - - const subject = - language === "en" ? "Invitation as Tenant Administrator" : "Einladung als Tenant-Administrator"; - - await sendTemplatedEmail("tenant-admin-invite", recipient, subject, language, tenant, { - registrationUrl, - }); -} - /** * Send user invitation email for existing tenant * @param {string} userEmail - Email address of the invited user diff --git a/src/lib/server/email/templates/tenant-admin-invite.de.html b/src/lib/server/email/templates/tenant-admin-invite.de.html deleted file mode 100644 index 8b7b2e8..0000000 --- a/src/lib/server/email/templates/tenant-admin-invite.de.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - Einladung als Tenant-Administrator - - - - - - diff --git a/src/lib/server/email/templates/tenant-admin-invite.de.txt b/src/lib/server/email/templates/tenant-admin-invite.de.txt deleted file mode 100644 index e3ebb6c..0000000 --- a/src/lib/server/email/templates/tenant-admin-invite.de.txt +++ /dev/null @@ -1,23 +0,0 @@ -Einladung als Tenant-Administrator - -Hallo {{recipient.name}}, - -Sie wurden als Administrator für einen neuen Tenant in Open Reception eingeladen! - -Tenant-Details: -- Name: {{tenant.shortName}} -- Beschreibung: {{tenant.longName}} - -Als Tenant-Administrator können Sie: -- Die Konfiguration Ihres Tenants verwalten -- Mitarbeiter einladen und verwalten -- Kanäle und Services einrichten -- Termine und Buchungen überwachen - -Registrieren Sie sich jetzt, um loszulegen: -{{registrationUrl}} - -Diese Einladung ist 7 Tage gültig. Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren. - --- -Open Reception Team \ No newline at end of file diff --git a/src/lib/server/email/templates/tenant-admin-invite.en.html b/src/lib/server/email/templates/tenant-admin-invite.en.html deleted file mode 100644 index 77ba9d9..0000000 --- a/src/lib/server/email/templates/tenant-admin-invite.en.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - Invitation as Tenant Administrator - - - - - - diff --git a/src/lib/server/email/templates/tenant-admin-invite.en.txt b/src/lib/server/email/templates/tenant-admin-invite.en.txt deleted file mode 100644 index 0ac5240..0000000 --- a/src/lib/server/email/templates/tenant-admin-invite.en.txt +++ /dev/null @@ -1,23 +0,0 @@ -Invitation as Tenant Administrator - -Hello {{recipient.name}}, - -You have been invited as an administrator for a new tenant in Open Reception! - -Tenant Details: -- Name: {{tenant.shortName}} -- Description: {{tenant.longName}} - -As a tenant administrator, you can: -- Manage your tenant's configuration -- Invite and manage staff members -- Set up channels and services -- Monitor appointments and bookings - -Register now to get started: -{{registrationUrl}} - -This invitation is valid for 7 days. If you didn't request this email, you can ignore it. - --- -Open Reception Team \ No newline at end of file diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index c589981..d5223f0 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -9,8 +9,6 @@ import { eq, and, not, count, or } from "drizzle-orm"; import logger from "$lib/logger"; import z from "zod/v4"; import { ValidationError, NotFoundError, ConflictError } from "../utils/errors"; -import { sendTenantAdminInviteEmail } from "../email/email-service"; -import { ERRORS } from "$lib/errors"; if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); @@ -20,7 +18,6 @@ const tenantCreationSchema = z.object({ .min(4) .max(15) .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/), - inviteAdmin: z.email().optional(), }); export type TenantCreationRequest = z.infer; @@ -67,7 +64,6 @@ export class TenantAdminService { // Check if tenant can be created without any duplications. Do not create tenant if: // - short name already exists - // - invited tenant admin email already exists const tenantExists = await centralDb .select() .from(centralSchema.tenant) @@ -75,15 +71,6 @@ export class TenantAdminService { if (tenantExists.length > 0) { throw new ConflictError("Tenant with shortname already exists"); } - if (request.inviteAdmin) { - const adminExists = await centralDb - .select() - .from(centralSchema.user) - .where(eq(centralSchema.user.email, request.inviteAdmin)); - if (adminExists.length > 0) { - throw new ConflictError(ERRORS.USERS.EMAIL_EXISTS); - } - } const configuration = TenantAdminService.getConfigDefaults(); @@ -150,40 +137,6 @@ export class TenantAdminService { log.debug("Tenant service created successfully", { tenantId: tenant[0].id }); - // Send tenant admin invitation email if email is provided - if (request.inviteAdmin) { - try { - // For now, we'll use the email as name. In a real implementation, - // you might want to collect the name separately or parse it from the email - const adminName = request.inviteAdmin.split("@")[0]; - - // Generate registration URL for the tenant admin - // This should point to a registration page that pre-fills tenant info - const registrationUrl = `${env.PUBLIC_APP_URL || "http://localhost:5173"}/register?tenant=${tenant[0].id}&email=${encodeURIComponent(request.inviteAdmin)}&role=TENANT_ADMIN`; - - await sendTenantAdminInviteEmail( - request.inviteAdmin, - adminName, - tenant[0], - registrationUrl, - ); - - log.info("Tenant admin invitation email sent successfully", { - tenantId: tenant[0].id, - adminEmail: request.inviteAdmin, - }); - } catch (emailError) { - log.error("Failed to send tenant admin invitation email", { - tenantId: tenant[0].id, - adminEmail: request.inviteAdmin, - error: String(emailError), - }); - - // Don't fail the tenant creation if email fails - // Just log the error and continue - } - } - return tenantService; } catch (error) { log.error("Failed to create tenant", { diff --git a/src/routes/(pages)/dashboard/tenants/+page.server.ts b/src/routes/(pages)/dashboard/tenants/+page.server.ts index 301c211..cab1995 100644 --- a/src/routes/(pages)/dashboard/tenants/+page.server.ts +++ b/src/routes/(pages)/dashboard/tenants/+page.server.ts @@ -53,7 +53,8 @@ export const actions: Actions = { }); } - const resp = await event.fetch(`/api/tenants`, { + // Create tenant first + const tenantResponse = await event.fetch(`/api/tenants`, { method: "POST", headers: { "Content-Type": "application/json", @@ -61,16 +62,13 @@ export const actions: Actions = { credentials: "same-origin", body: JSON.stringify({ shortName: form.data.shortName, - inviteAdmin: form.data.email, }), }); - if (resp.status < 400) { - return { form }; - } else { + if (tenantResponse.status >= 400) { let error = "Unknown error"; try { - const body = await resp.json(); + const body = await tenantResponse.json(); error = body.error; } catch (e) { log.error("Failed to parse add tenant error response", { error: e }); @@ -80,6 +78,38 @@ export const actions: Actions = { error, }); } + + // If admin invitation requested, create user invitation + if (form.data.inviteAdmin && form.data.email) { + const tenantData = await tenantResponse.json(); + const tenantId = tenantData.tenant?.id; + + if (tenantId) { + const userInviteResponse = await event.fetch(`/api/auth/invite`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin", + body: JSON.stringify({ + email: form.data.email, + name: form.data.email.split("@")[0], + role: "TENANT_ADMIN", + tenantId: tenantId, + }), + }); + + if (userInviteResponse.status >= 400) { + log.warn("Tenant created but user invitation failed", { + tenantId, + email: form.data.email, + }); + // Don't fail the entire operation, just log the warning + } + } + } + + return { form }; }, edit: async (event) => { const form = await superValidate(event, zod(editFormSchema)); diff --git a/src/routes/api/tenants/+server.ts b/src/routes/api/tenants/+server.ts index 891a152..d4f702d 100644 --- a/src/routes/api/tenants/+server.ts +++ b/src/routes/api/tenants/+server.ts @@ -28,12 +28,6 @@ registerOpenAPIRoute("/tenants", "POST", { description: "Short name for the tenant (4-15 characters)", example: "acme-corp", }, - inviteAdmin: { - type: "string", - format: "email", - description: "Email address to invite as tenant admin", - example: "admin@acme-corp.com", - }, }, required: ["shortName"], }, @@ -49,15 +43,23 @@ registerOpenAPIRoute("/tenants", "POST", { type: "object", properties: { message: { type: "string", description: "Success message" }, - tenantId: { type: "string", description: "Generated tenant ID" }, - shortName: { type: "string", description: "Tenant short name" }, + tenant: { + type: "object", + properties: { + id: { type: "string", description: "Generated tenant ID" }, + shortName: { type: "string", description: "Tenant short name" }, + }, + required: ["id", "shortName"], + }, }, - required: ["message", "tenantId", "shortName"], + required: ["message", "tenant"], }, example: { message: "Tenant created successfully", - tenantId: "01234567-89ab-cdef-0123-456789abcdef", - shortName: "acme-corp", + tenant: { + id: "01234567-89ab-cdef-0123-456789abcdef", + shortName: "acme-corp", + }, }, }, }, @@ -157,14 +159,12 @@ export const POST: RequestHandler = async ({ locals, request }) => { log.debug("Creating tenant", { shortName: body.shortName, - hasInviteAdmin: !!body.inviteAdmin, }); checkPermission(locals, null, true); const tenantService = await TenantAdminService.createTenant({ shortName: body.shortName, - inviteAdmin: body.inviteAdmin, }); log.debug("Tenant created successfully", { @@ -175,8 +175,10 @@ export const POST: RequestHandler = async ({ locals, request }) => { return json( { message: "Tenant created successfully", - tenantId: tenantService.tenantId, - shortName: body.shortName, + tenant: { + id: tenantService.tenantId, + shortName: body.shortName, + }, }, { status: 201 }, ); diff --git a/src/routes/api/tenants/tenants.test.ts b/src/routes/api/tenants/tenants.test.ts index 7ec85e1..6faed35 100644 --- a/src/routes/api/tenants/tenants.test.ts +++ b/src/routes/api/tenants/tenants.test.ts @@ -59,7 +59,6 @@ describe("/api/tenants", () => { json: () => Promise.resolve({ shortName: "test-tenant", - inviteAdmin: "admin@test.com", }), }; @@ -87,13 +86,14 @@ describe("/api/tenants", () => { expect(TenantAdminService.createTenant).toHaveBeenCalledWith({ shortName: "test-tenant", - inviteAdmin: "admin@test.com", }); expect(data).toEqual({ message: "Tenant created successfully", - tenantId: "test-tenant-id", - shortName: "test-tenant", + tenant: { + id: "test-tenant-id", + shortName: "test-tenant", + }, }); expect(response.status).toBe(201); }); @@ -142,8 +142,10 @@ describe("/api/tenants", () => { expect(data).toEqual({ message: "Tenant created successfully", - tenantId: "test-tenant-id-2", - shortName: "test-tenant-2", + tenant: { + id: "test-tenant-id-2", + shortName: "test-tenant-2", + }, }); expect(response.status).toBe(201); });