UNPKG

@lucidcms/core

Version:

The core of the Lucid CMS. It's responsible for spinning up the API and serving the CMS.

1 lines 10.3 kB
{"version":3,"file":"authenticate.mjs","names":["formatter","userPermissionsFormatter","constants","authServices.accessToken.verifyToken"],"sources":["../../../../src/libs/http/middleware/authenticate.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport constants from \"../../../constants/constants.js\";\nimport { getAuthCacheNamespaceToken } from \"../../../services/auth/helpers/auth-cache.js\";\nimport { authServices } from \"../../../services/index.js\";\nimport type {\n\tLucidAccessToken,\n\tLucidAuth,\n\tLucidHonoContext,\n} from \"../../../types/hono.js\";\nimport { LucidAPIError } from \"../../../utils/errors/index.js\";\nimport { multiTenancyEnabled } from \"../../../utils/helpers/index.js\";\nimport formatter, { userPermissionsFormatter } from \"../../formatters/index.js\";\nimport { copy } from \"../../i18n/index.js\";\nimport cacheKeys from \"../../kv/cache-keys.js\";\nimport { UsersRepository } from \"../../repositories/index.js\";\nimport createServiceContext from \"../utils/create-service-context.js\";\n\ntype TenantScope = \"required\" | \"allow-global\";\n\n/**\n * Controls whether a route must be scoped to a tenant.\n * Use `allow-global` for routes that need to work before or outside tenant selection.\n */\ntype AuthenticateOptions = {\n\ttenantScope?: TenantScope;\n};\n\ntype CachedAuthState = Omit<LucidAuth, \"exp\" | \"iat\" | \"nonce\">;\n\n/**\n * Reads the cached live auth state for a user.\n * The namespace token lets us invalidate all user auth cache entries at once.\n */\nconst getCachedAuthState = async (\n\tc: LucidHonoContext,\n\ttoken: LucidAccessToken,\n\ttenantKey?: string | null,\n) => {\n\tconst context = createServiceContext(c);\n\tconst namespaceToken = await getAuthCacheNamespaceToken(context);\n\tconst cacheKey = cacheKeys.auth.user(token.id, namespaceToken, tenantKey);\n\n\treturn {\n\t\tcacheKey,\n\t\tcontext,\n\t\tdata: await context.kv.get<CachedAuthState>(context, {\n\t\t\tkey: cacheKey,\n\t\t\thash: true,\n\t\t}),\n\t};\n};\n\n/**\n * Loads the current user auth state from the database.\n * Access tokens only prove identity; permissions and tenants come from live user data.\n */\nconst fetchAuthState = async (\n\tc: LucidHonoContext,\n\ttoken: LucidAccessToken,\n\ttenantKey?: string | null,\n): Promise<CachedAuthState> => {\n\tconst config = c.get(\"config\");\n\tconst Users = new UsersRepository(c.get(\"database\").client, config.db);\n\n\tconst userRes = await Users.selectAccessTokenUser({\n\t\twhere: [\n\t\t\t{ key: \"id\", operator: \"=\", value: token.id },\n\t\t\t{\n\t\t\t\tkey: \"is_deleted\",\n\t\t\t\toperator: \"=\",\n\t\t\t\tvalue: config.db.getDefault(\"boolean\", \"false\"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: \"is_locked\",\n\t\t\t\toperator: \"=\",\n\t\t\t\tvalue: config.db.getDefault(\"boolean\", \"false\"),\n\t\t\t},\n\t\t],\n\t\ttenantKey,\n\t\tvalidation: {\n\t\t\tenabled: true,\n\t\t\tdefaultError: {\n\t\t\t\ttype: \"authorisation\",\n\t\t\t\tcode: \"authorisation\",\n\t\t\t\tmessage: copy(\"server:core.permissions.unauthorized\"),\n\t\t\t\tstatus: 401,\n\t\t\t},\n\t\t},\n\t});\n\tif (userRes.error) throw new LucidAPIError(userRes.error);\n\n\tconst superAdmin = formatter.formatBoolean(userRes.data.super_admin ?? false);\n\n\tconst { permissions } = userPermissionsFormatter.formatMultiple({\n\t\troles: userRes.data.roles || [],\n\t\tdefaultLocale: config.localization.defaultLocale,\n\t});\n\n\treturn {\n\t\tid: userRes.data.id,\n\t\tusername: userRes.data.username,\n\t\temail: userRes.data.email,\n\t\tpermissions,\n\t\tsuperAdmin,\n\t\ttenantKeys: superAdmin\n\t\t\t? config.tenants.map((tenant) => tenant.key)\n\t\t\t: (userRes.data.tenants ?? []).map((tenant) => tenant.tenant_key),\n\t};\n};\n\n/**\n * Resolves the auth context used by the request.\n * Cached data keeps auth fast while still allowing permission changes to take effect quickly.\n */\nconst resolveAuthState = async (\n\tc: LucidHonoContext,\n\ttoken: LucidAccessToken,\n\ttenantKey?: string | null,\n): Promise<LucidAuth> => {\n\tconst cached = await getCachedAuthState(c, token, tenantKey);\n\tconst authState = cached.data ?? (await fetchAuthState(c, token, tenantKey));\n\n\tif (cached.data == null) {\n\t\tawait cached.context.kv.set(cached.context, {\n\t\t\tkey: cached.cacheKey,\n\t\t\tvalue: authState,\n\t\t\texpirationTtl: constants.authCacheExpiration,\n\t\t\thash: true,\n\t\t});\n\t}\n\n\treturn {\n\t\t...authState,\n\t\texp: token.exp,\n\t\tiat: token.iat,\n\t\tnonce: token.nonce,\n\t};\n};\n\n/**\n * Verifies the access token and stores the current auth state in Hono context.\n * Soft mode is used by optional auth paths where anonymous requests are allowed.\n */\nexport const authenticationCheck = async (\n\tc: LucidHonoContext,\n\toptions?: { soft?: boolean; tenantKey?: string | null },\n) => {\n\tconst accessTokenRes = await authServices.accessToken.verifyToken(c);\n\tif (accessTokenRes.error) {\n\t\tif (options?.soft !== true) throw new LucidAPIError(accessTokenRes.error);\n\t\treturn;\n\t}\n\tif (!accessTokenRes.data) return;\n\n\ttry {\n\t\tc.set(\n\t\t\t\"auth\",\n\t\t\tawait resolveAuthState(c, accessTokenRes.data, options?.tenantKey),\n\t\t);\n\t} catch (error) {\n\t\tif (options?.soft === true) return;\n\t\tthrow error;\n\t}\n};\n\n/**\n * Determines the tenant key that should scope auth permissions.\n * Full tenant access validation still happens after auth is loaded.\n */\nconst resolveAuthTenantKey = (c: LucidHonoContext) => {\n\tconst config = c.get(\"config\");\n\tif (!multiTenancyEnabled(config)) return null;\n\n\tconst tenantKey = c.req.header(constants.headers.tenant);\n\tif (!tenantKey) return null;\n\n\treturn config.tenants.some((tenant) => tenant.key === tenantKey)\n\t\t? tenantKey\n\t\t: null;\n};\n\n/**\n * Resolves the request tenant after authentication.\n * Tenant membership is checked against the live auth state, not the access token.\n */\nconst resolveTenantCheck = (\n\tc: LucidHonoContext,\n\toptions?: AuthenticateOptions,\n) => {\n\tconst config = c.get(\"config\");\n\tif (!multiTenancyEnabled(config)) {\n\t\tc.set(\"tenant\", null);\n\t\treturn;\n\t}\n\n\tconst tenantKey = c.req.header(constants.headers.tenant);\n\tconst auth = c.get(\"auth\");\n\tif (!tenantKey) {\n\t\tif (options?.tenantScope === \"allow-global\" || auth.superAdmin) {\n\t\t\tc.set(\"tenant\", null);\n\t\t\treturn;\n\t\t}\n\n\t\tthrow new LucidAPIError({\n\t\t\ttype: \"authorisation\",\n\t\t\tmessage: copy(\"server:core.tenants.no.access\"),\n\t\t\tstatus: 403,\n\t\t});\n\t}\n\n\tconst tenantExists = config.tenants.some(\n\t\t(tenant) => tenant.key === tenantKey,\n\t);\n\tif (!tenantExists) {\n\t\tthrow new LucidAPIError({\n\t\t\ttype: \"basic\",\n\t\t\tmessage: copy(\"server:core.tenants.unknown\", {\n\t\t\t\tdata: { key: tenantKey },\n\t\t\t}),\n\t\t\tstatus: 400,\n\t\t});\n\t}\n\n\tif (!auth.superAdmin && !auth.tenantKeys.includes(tenantKey)) {\n\t\tthrow new LucidAPIError({\n\t\t\ttype: \"authorisation\",\n\t\t\tmessage: copy(\"server:core.tenants.no.access\"),\n\t\t\tstatus: 403,\n\t\t});\n\t}\n\n\tc.set(\"tenant\", { key: tenantKey });\n};\n\n/**\n * Authenticates an admin request and, by default, requires a tenant when tenancy is enabled.\n * Pass `tenantScope: \"allow-global\"` for global routes such as account/bootstrap data.\n */\nconst authenticate = (options?: AuthenticateOptions) =>\n\tcreateMiddleware(async (c: LucidHonoContext, next) => {\n\t\tawait authenticationCheck(c, { tenantKey: resolveAuthTenantKey(c) });\n\t\tresolveTenantCheck(c, options);\n\t\treturn await next();\n\t});\n\nexport default authenticate;\n"],"mappings":"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"}