hooks.server.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import crypto from 'node:crypto';
  2. import type { Handle, HandleServerError } from '@sveltejs/kit';
  3. import { redirect } from '@sveltejs/kit';
  4. import { resolve } from '$app/paths';
  5. import {
  6. ForbiddenError,
  7. isAppError,
  8. logger as rootLogger,
  9. NotFoundError,
  10. runWithContext,
  11. server,
  12. serverLogger as log,
  13. UnauthorizedError,
  14. ValidationError,
  15. } from '$lib/server';
  16. import type { User } from '$lib/types/user';
  17. const LOGIN = resolve('/auth/login');
  18. const WORKSPACE_RESOURCES = resolve('/api/workspaces');
  19. const ready = server.init();
  20. const shutdown = async (signal: string) => {
  21. log.info(`[${signal}] Shutting down...`);
  22. await server.shutdown();
  23. rootLogger.close();
  24. process.exit(0);
  25. };
  26. process.once('SIGTERM', () => shutdown('SIGTERM'));
  27. process.once('SIGINT', () => shutdown('SIGINT'));
  28. const handle: Handle = async ({ event, resolve: resolveRequest }) => {
  29. await ready;
  30. const requestId = crypto.randomUUID();
  31. event.locals.requestId = requestId;
  32. return runWithContext({ requestId }, async () => {
  33. const auth = server.auth;
  34. let user: User | undefined;
  35. if (auth) {
  36. const token = event.cookies.get('session');
  37. user = token ? await auth.verifyToken(token) : undefined;
  38. if (!user && event.url.pathname !== LOGIN) {
  39. redirect(302, LOGIN);
  40. }
  41. } else {
  42. user = {
  43. id: 'admin',
  44. name: 'Admin',
  45. email: 'admin@locostack.com',
  46. workspaces: ['*'],
  47. };
  48. }
  49. event.locals.user = user || null;
  50. const { pathname } = event.url;
  51. if (pathname.startsWith(WORKSPACE_RESOURCES) && pathname !== WORKSPACE_RESOURCES) {
  52. if (!user) {
  53. throw new UnauthorizedError('Authentication required');
  54. }
  55. const workspaceId = event.params.workspaceId;
  56. if (workspaceId === undefined) {
  57. throw new ValidationError('Workspace ID is required');
  58. }
  59. if (!user.workspaces.includes(workspaceId) && !user.workspaces.includes('*')) {
  60. throw new ForbiddenError('User does not have access to this workspace');
  61. }
  62. const workspace = server.ws.list().find((ws) => ws.id === workspaceId);
  63. if (!workspace) {
  64. throw new NotFoundError('Workspace not found');
  65. }
  66. event.locals.workspaceId = workspaceId;
  67. event.locals.workspace = workspace;
  68. } else {
  69. event.locals.workspaceId = event.cookies.get('workspaceId') || null;
  70. }
  71. return resolveRequest(event);
  72. });
  73. };
  74. /**
  75. * Catches any error that escapes a load function, action, or endpoint.
  76. * AppErrors are surfaced with their code and message; unexpected errors
  77. * are logged and replaced with a generic message so internals are not leaked.
  78. */
  79. const handleError: HandleServerError = ({ error, event }) => {
  80. const requestId = event.locals.requestId;
  81. if (isAppError(error)) {
  82. log.warn('Application error', {
  83. code: error.code,
  84. status: error.statusCode,
  85. message: error.message,
  86. path: event.url.pathname,
  87. requestId,
  88. });
  89. return { code: error.code, message: error.message };
  90. }
  91. log.error('Unexpected server error', { err: error, path: event.url.pathname, requestId });
  92. return { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred.' };
  93. };
  94. export { handle, handleError };