Files
appointment-booking-software/src/routes/api/admin/tenant/+server.ts
T
5965fada38 Make descriptions multilingual (#89)
* Make descriptions multilingual

* Format and test fixes

* Update src/lib/server/db/tenant-schema.ts

* Fix tenant-switcher to update instantly

* Fix sql migrations for description

* Add languages to tenant table

* Fix tenant schema

* Use objects for translated fields

* Fix format

* Fix lint

* Allow longer descriptions for channels

* Field Localizations are based on tenant locales not supportedLocales

* Fix agent image list preview

* Improve language tab keyboard navigation

* Fix falsy redirect after store init

---------

Co-authored-by: Karl Ludwig Weise <office@ludwigweise.de>
Co-authored-by: Karl Ludwig Weise <ludwig@ludwigweise.de>
2025-10-01 18:43:01 +02:00

202 lines
5.6 KiB
TypeScript

import { json, type RequestHandler } from "@sveltejs/kit";
import { z } from "zod";
import { centralDb } from "$lib/server/db";
import { tenant, userSession } from "$lib/server/db/central-schema";
import { eq } from "drizzle-orm";
import {
BackendError,
InternalError,
logError,
NotFoundError,
ValidationError,
} from "$lib/server/utils/errors";
import { UserService } from "$lib/server/services/user-service";
import { generateAccessToken } from "$lib/server/auth/jwt-utils";
import { UniversalLogger } from "$lib/logger";
import { registerOpenAPIRoute } from "$lib/server/openapi";
import { checkPermission } from "$lib/server/utils/permissions";
import { ERRORS } from "$lib/errors";
const logger = new UniversalLogger().setContext("AdminTenantSwitch");
const tenantSwitchSchema = z.object({
tenantId: z.string().uuid(),
});
/**
* POST /api/admin/tenant
* Switch active tenant for global admin
*/
export const POST: RequestHandler = async ({ request, locals, cookies }) => {
try {
// Verify user is authenticated and is global admin
checkPermission(locals, null, true);
if (!locals.user?.sessionId) {
throw new ValidationError(ERRORS.SECURITY.SESSION_MISSING);
}
const body = await request.json();
const validation = tenantSwitchSchema.safeParse(body);
if (!validation.success) {
logger.warn("Invalid tenant switch request", {
userId: locals.user?.id,
errors: validation.error.errors,
});
throw new ValidationError(ERRORS.VALIDATION.INVALID_REQUEST_BODY);
}
const { tenantId } = validation.data;
// If tenantId is provided, verify the tenant exists
const tenantExists = await centralDb
.select({ id: tenant.id })
.from(tenant)
.where(eq(tenant.id, tenantId))
.limit(1);
if (tenantExists.length === 0) {
logger.warn("Tenant not found for switching", {
userId: locals.user?.id,
tenantId,
});
throw new NotFoundError(ERRORS.TENANTS.NOT_FOUND);
}
// Current user is already authenticated via authHandle
// Update user's active tenant in the database
const updatedUser = await UserService.updateUser(locals.user?.userId as string, {
tenantId: tenantId || null,
});
if (!updatedUser) {
logger.error("Failed to update user tenant", {
userId: locals.user.userId,
tenantId,
});
throw new InternalError(ERRORS.USERS.FAILED_TO_UPDATE);
}
// Generate new access token with updated tenant context
const newAccessToken = await generateAccessToken(updatedUser, locals.user.sessionId);
// Update session with new access token
await centralDb
.update(userSession)
.set({
accessToken: newAccessToken,
lastUsedAt: new Date(),
})
.where(eq(userSession.id, locals.user.sessionId));
// Set new access token cookie
cookies.set("access_token", newAccessToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
});
logger.info("Tenant switched successfully", {
userId: locals.user?.userId,
fromTenant: locals.user?.tenantId,
toTenant: tenantId,
});
return json({
success: true,
message: tenantId ? "Tenant switched successfully" : "Switched to global admin mode",
tenantId,
user: {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
role: updatedUser.role,
tenantId: updatedUser.tenantId,
},
});
} catch (err) {
logError(logger)("Error in tenant switch", err, locals.user?.userId);
if (err instanceof BackendError) {
return err.toJson();
}
return new InternalError().toJson();
}
};
// Register OpenAPI documentation
registerOpenAPIRoute("/admin/tenant", "POST", {
summary: "Switch active tenant for global admin",
description:
"Allows a global admin to switch their active tenant context or return to global admin mode",
tags: ["Admin"],
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
tenantId: {
type: "string",
format: "uuid",
description: "Target tenant ID",
},
},
example: {
tenantId: "123e4567-e89b-12d3-a456-426614174000",
},
},
},
},
},
responses: {
200: {
description: "Tenant switched successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
message: { type: "string" },
tenantId: {
type: "string",
format: "uuid",
},
user: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
email: { type: "string", format: "email" },
name: { type: "string" },
role: { type: "string", enum: ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"] },
tenantId: { type: "string", format: "uuid" },
},
},
},
},
},
},
},
400: {
description: "Invalid request body",
},
401: {
description: "Authentication required",
},
403: {
description: "Only global admins can switch tenants",
},
404: {
description: "Tenant not found",
},
500: {
description: "Internal server error",
},
},
});