mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-30 20:54:49 +02:00
* 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>
202 lines
5.6 KiB
TypeScript
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",
|
|
},
|
|
},
|
|
});
|