diff --git a/.env.example b/.env.example index 8ae2ba8..b7fa742 100644 --- a/.env.example +++ b/.env.example @@ -21,5 +21,6 @@ NODE_ENV=development APP_PORT=5173 JWT_SECRET=devsecretforjwtencryptionchangeforproduction +MANAGEMENT_DOMAIN=management.example.com DATABASE_URL="postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/$POSTGRES_DB" diff --git a/.github/workflows/main-deploy.yml b/.github/workflows/main-deploy.yml index 28fa644..ca8e0eb 100644 --- a/.github/workflows/main-deploy.yml +++ b/.github/workflows/main-deploy.yml @@ -42,6 +42,7 @@ jobs: SMTP_FROM_NAME: "Open Reception" SMTP_FROM_EMAIL: "noreply@example.com" JWT_SECRET: "test" + MANAGEMENT_DOMAIN: "example.com" - name: Build application run: npm run build @@ -55,6 +56,7 @@ jobs: SMTP_FROM_NAME: "Open Reception" SMTP_FROM_EMAIL: "noreply@example.com" JWT_SECRET: "test" + MANAGEMENT_DOMAIN: "example.com" # - name: Install Playwright browsers # run: npx playwright install --with-deps @@ -70,3 +72,4 @@ jobs: # SMTP_FROM_NAME: "Open Reception" # SMTP_FROM_EMAIL: "noreply@example.com" # JWT_SECRET: "test" + # MANAGEMENT_DOMAIN: "example.com" diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index d899ebe..a0ac18d 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -70,6 +70,7 @@ jobs: SMTP_FROM_NAME: "Open Reception" SMTP_FROM_EMAIL: "noreply@example.com" JWT_SECRET: "test" + MANAGEMENT_DOMAIN: "example.com" build: runs-on: ubuntu-latest @@ -99,3 +100,4 @@ jobs: SMTP_FROM_NAME: "Open Reception" SMTP_FROM_EMAIL: "noreply@example.com" JWT_SECRET: "test" + MANAGEMENT_DOMAIN: "example.com" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9601294..4f8486f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,12 +50,14 @@ jobs: SMTP_FROM_NAME: "Open Reception" SMTP_FROM_EMAIL: "noreply@example.com" JWT_SECRET: "test" + MANAGEMENT_DOMAIN: "example.com" - name: Build application run: npm run build env: DATABASE_URL: "postgresql://test:test@localhost:5432/test" JWT_SECRET: "build" + MANAGEMENT_DOMAIN: "example.com" - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index db303b1..dff263c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -61,6 +61,7 @@ services: - smtp_from_name - smtp_from_email - jwt_secret + - management_domain depends_on: postgres: condition: service_healthy @@ -93,6 +94,7 @@ services: export SMTP_FROM_NAME=$$(cat /run/secrets/smtp_from_name) export SMTP_FROM_EMAIL=$$(cat /run/secrets/smtp_from_email) export JWT_SECRET=$$(cat /run/secrets/jwt_secret) + export MANAGEMENT_DOMAIN=$$(cat /run/secrets/management_domain) exec node build/index.js " read_only: true diff --git a/src/lib/server/auth/__tests__/registration-bootstrap.test.ts b/src/lib/server/auth/__tests__/registration-bootstrap.test.ts index f29467a..fc7a7e3 100644 --- a/src/lib/server/auth/__tests__/registration-bootstrap.test.ts +++ b/src/lib/server/auth/__tests__/registration-bootstrap.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, vi } from "vitest"; vi.mock("$env/dynamic/private", () => ({ env: { JWT_SECRET: "test-jwt-secret-for-registration-bootstrap-unit-tests-minimum-32-chars", + MANAGEMENT_DOMAIN: "example.com", }, })); diff --git a/src/lib/server/auth/booking-access-token.ts b/src/lib/server/auth/booking-access-token.ts index b5b3823..1d874a5 100644 --- a/src/lib/server/auth/booking-access-token.ts +++ b/src/lib/server/auth/booking-access-token.ts @@ -14,6 +14,10 @@ export type BookingAccessScope = | typeof EXISTING_CLIENT_BOOKING_SCOPE | typeof NEW_CLIENT_BOOTSTRAP_SCOPE; +if (!process.env.BUILDING && !env.MANAGEMENT_DOMAIN) { + throw new Error("Mandatory ENV variable MANAGEMENT_DOMAIN is missing!"); +} + if (!process.env.BUILDING && !env.JWT_SECRET) { throw new Error("Mandatory ENV variable JWT_SECRET is missing!"); } diff --git a/src/lib/server/services/user-service.ts b/src/lib/server/services/user-service.ts index ef042cc..605aa09 100644 --- a/src/lib/server/services/user-service.ts +++ b/src/lib/server/services/user-service.ts @@ -13,6 +13,7 @@ import { hashPassphrase, validatePassphraseStrength, } from "../utils/passphrase"; +import { env } from "$env/dynamic/private"; import { sendConfirmationEmail } from "../email/email-service"; import type { SelectTenant } from "../db/central-schema"; import { TenantAdminService } from "./tenant-admin-service"; @@ -182,16 +183,23 @@ export class UserService { hasRecoveryPassphrase: !!insertedUser.recoveryPassphrase, }); + if (!env.MANAGEMENT_DOMAIN) { + throw new InternalError(`MANAGEMENT_DOMAIN is missing`); + } + // Send confirmation email to user (token is used as confirmation code) try { if (insertedUser?.email && inviteResult?.inviteCode) { const tenant = await getTenantForUser(insertedUser); + const linkUrl = dev + ? requestUrl + : new URL(`https://${tenant.id === "system" ? env.MANAGEMENT_DOMAIN : tenant.domain}`); await sendConfirmationEmail( insertedUser, tenant, inviteResult.inviteCode, 10, // 10 minutes expiration to match tokenValidUntil - requestUrl, + linkUrl, ); log.debug("Confirmation email sent successfully", { userId: insertedUser.id, @@ -239,10 +247,16 @@ export class UserService { throw new NotFoundError(`Could not resend confirmation mail for unknown user ${email}`); } + if (!env.MANAGEMENT_DOMAIN) { + throw new InternalError(`MANAGEMENT_DOMAIN is missing`); + } + // Send confirmation email with new token - use tenant-specific branding if available try { const tenant = await getTenantForUser(user); - const linkUrl = dev ? requestUrl : new URL(`https://${tenant.domain}`); + const linkUrl = dev + ? requestUrl + : new URL(`https://${tenant.id === "system" ? env.MANAGEMENT_DOMAIN : tenant.domain}`); await sendConfirmationEmail( user, tenant, diff --git a/src/routes/api/auth/challenge/__tests__/challenge-bootstrap.test.ts b/src/routes/api/auth/challenge/__tests__/challenge-bootstrap.test.ts index 491ca39..1b07281 100644 --- a/src/routes/api/auth/challenge/__tests__/challenge-bootstrap.test.ts +++ b/src/routes/api/auth/challenge/__tests__/challenge-bootstrap.test.ts @@ -6,6 +6,7 @@ vi.mock("$env/dynamic/private", () => ({ env: { JWT_SECRET: "test-jwt-secret-for-registration-bootstrap-unit-tests-minimum-32-chars", NODE_ENV: "test", + MANAGEMENT_DOMAIN: "example.com", }, })); diff --git a/src/routes/api/auth/register/__tests__/register-bootstrap.test.ts b/src/routes/api/auth/register/__tests__/register-bootstrap.test.ts index 783fc67..3bff0e6 100644 --- a/src/routes/api/auth/register/__tests__/register-bootstrap.test.ts +++ b/src/routes/api/auth/register/__tests__/register-bootstrap.test.ts @@ -6,6 +6,7 @@ vi.mock("$env/dynamic/private", () => ({ env: { JWT_SECRET: "test-jwt-secret-for-registration-bootstrap-unit-tests-minimum-32-chars", NODE_ENV: "test", + MANAGEMENT_DOMAIN: "example.com", }, }));