diff --git a/app/client/lib/auth-errors.ts b/app/client/lib/auth-errors.ts index a2f0f4c6..1b7de35a 100644 --- a/app/client/lib/auth-errors.ts +++ b/app/client/lib/auth-errors.ts @@ -1,17 +1,9 @@ -export type LoginErrorCode = - | "ACCOUNT_LINK_REQUIRED" - | "EMAIL_NOT_VERIFIED" - | "INVITE_REQUIRED" - | "BANNED_USER" - | "SSO_LOGIN_FAILED"; +import { LOGIN_ERROR_CODES, type LoginErrorCode } from "~/lib/auth-errors"; -const VALID_ERROR_CODES: Set = new Set([ - "ACCOUNT_LINK_REQUIRED", - "EMAIL_NOT_VERIFIED", - "INVITE_REQUIRED", - "BANNED_USER", - "SSO_LOGIN_FAILED", -]); +export type { LoginErrorCode } from "~/lib/auth-errors"; +export { getLoginErrorDescription } from "~/lib/auth-errors"; + +const VALID_ERROR_CODES = new Set(LOGIN_ERROR_CODES); export function decodeLoginError(error?: string): LoginErrorCode | null { if (!error) { @@ -26,20 +18,3 @@ export function decodeLoginError(error?: string): LoginErrorCode | null { return null; } - -export function getLoginErrorDescription(errorCode: LoginErrorCode | null): string | null { - switch (errorCode) { - case "ACCOUNT_LINK_REQUIRED": - return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator."; - case "EMAIL_NOT_VERIFIED": - return "Your identity provider did not mark your email as verified."; - case "INVITE_REQUIRED": - return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; - case "BANNED_USER": - return "You have been banned from this application. Please contact support if you believe this is an error."; - case "SSO_LOGIN_FAILED": - return "SSO authentication failed. Please try again."; - default: - return null; - } -} diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index b4b01cd6..e6198a95 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -38,7 +38,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { const [isVerifying2FA, setIsVerifying2FA] = useState(false); const [trustDevice, setTrustDevice] = useState(false); const errorCode = decodeLoginError(error); - const errorDescription = getLoginErrorDescription(errorCode); + const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null; const { data: ssoProviders } = useSuspenseQuery({ ...getPublicSsoProvidersOptions(), diff --git a/app/lib/auth-errors.ts b/app/lib/auth-errors.ts new file mode 100644 index 00000000..a50c8115 --- /dev/null +++ b/app/lib/auth-errors.ts @@ -0,0 +1,24 @@ +export const LOGIN_ERROR_CODES = [ + "ACCOUNT_LINK_REQUIRED", + "EMAIL_NOT_VERIFIED", + "INVITE_REQUIRED", + "BANNED_USER", + "SSO_LOGIN_FAILED", +] as const; + +export type LoginErrorCode = (typeof LOGIN_ERROR_CODES)[number]; + +export function getLoginErrorDescription(errorCode: LoginErrorCode): string { + switch (errorCode) { + case "ACCOUNT_LINK_REQUIRED": + return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator."; + case "EMAIL_NOT_VERIFIED": + return "Your identity provider did not mark your email as verified."; + case "INVITE_REQUIRED": + return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; + case "BANNED_USER": + return "You have been banned from this application. Please contact support if you believe this is an error."; + case "SSO_LOGIN_FAILED": + return "SSO authentication failed. Please try again."; + } +} diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index bbcf61f3..93ee7bf5 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -22,9 +22,10 @@ import { convertLegacyUserOnFirstLogin } from "./auth/middlewares/convert-legacy import { validateSsoCallbackUrls } from "./auth/middlewares/validate-sso-callback-urls"; import { validateSsoProviderId } from "./auth/middlewares/validate-sso-provider-id"; import { createUserDefaultOrg } from "./auth/helpers/create-default-org"; -import { isSsoCallbackRequest, requireSsoInvitation } from "./auth/middlewares/require-sso-invitation"; +import { requireSsoInvitation } from "./auth/middlewares/require-sso-invitation"; import { resolveTrustedProvidersForRequest } from "./auth/middlewares/trust-sso-provider-for-linking"; import { buildAllowedHosts } from "./auth/base-url"; +import { isSsoCallbackRequest } from "./auth/utils/sso-context"; export type AuthMiddlewareContext = MiddlewareContext>; diff --git a/app/server/lib/auth/helpers/create-default-org.ts b/app/server/lib/auth/helpers/create-default-org.ts index aac6e3f2..317253f4 100644 --- a/app/server/lib/auth/helpers/create-default-org.ts +++ b/app/server/lib/auth/helpers/create-default-org.ts @@ -1,41 +1,26 @@ -import { and, eq, gt } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { UnauthorizedError } from "http-errors-enhanced"; import type { GenericEndpointContext } from "better-auth"; import { db } from "~/server/db/db"; -import { invitation, member, organization, ssoProvider, usersTable, type User } from "~/server/db/schema"; +import { invitation, member, organization, type User } from "~/server/db/schema"; import { cryptoUtils } from "~/server/utils/crypto"; import { APIError } from "better-auth"; import { extractProviderIdFromContext, normalizeEmail } from "../utils/sso-context"; import { logger } from "~/server/utils/logger"; +import { authService } from "~/server/modules/auth/auth.service"; export async function findMembershipWithOrganization(userId: string, organizationId?: string) { - const memberships = await db - .select() - .from(member) - .where( - organizationId - ? and(eq(member.userId, userId), eq(member.organizationId, organizationId)) - : eq(member.userId, userId), - ) - .limit(1); + const membership = await db.query.member.findFirst({ + where: organizationId ? { AND: [{ userId }, { organizationId }] } : { userId }, + with: { + organization: true, + }, + }); - const membership = memberships[0]; - - if (!membership) { - return null; - } - - const orgs = await db.select().from(organization).where(eq(organization.id, membership.organizationId)).limit(1); - const org = orgs[0]; - - if (!org) { - return null; - } - - return { ...membership, organization: org }; + return membership ?? null; } -function buildOrgSlug(email: string) { +export function buildOrgSlug(email: string) { const [emailPrefix] = email.split("@"); const sanitized = emailPrefix .toLowerCase() @@ -46,39 +31,45 @@ function buildOrgSlug(email: string) { return `${safePrefix}-${Math.random().toString(36).slice(-4)}`; } -async function tryCreateInvitedMembership(userId: string, email: string, ctx: GenericEndpointContext | null) { - logger.debug("Checking for pending invitations for user", userId); +export type DefaultOrganizationData = { + id: string; + name: string; + slug: string; + createdAt: Date; + metadata: { + resticPassword: string; + }; +}; - const providerId = extractProviderIdFromContext(ctx); - const ssoProviders = await db.select().from(ssoProvider).where(eq(ssoProvider.providerId, providerId)).limit(1); - const ssoProviderRecord = ssoProviders[0]; +export async function buildDefaultOrganizationData( + user: Pick, + organizationId = Bun.randomUUIDv7(), +): Promise { + const resticPassword = cryptoUtils.generateResticPassword(); + return { + id: organizationId, + name: `${user.name}'s Workspace`, + slug: buildOrgSlug(user.email), + createdAt: new Date(), + metadata: { + resticPassword: await cryptoUtils.sealSecret(resticPassword), + }, + }; +} + +async function tryCreateInvitedMembership( + userId: string, + email: string, + ssoProviderRecord: Awaited>, +) { if (!ssoProviderRecord) { - logger.debug("No SSO provider found in context, skipping invitation check"); return null; } - logger.debug("SSO provider found in context, checking for linked accounts", ssoProviderRecord.providerId); - const now = new Date(); + logger.debug("Checking for pending invitations for user", { userId, providerId: ssoProviderRecord.providerId }); - const pendingInvitations = await db - .select({ - id: invitation.id, - email: invitation.email, - role: invitation.role, - organizationId: invitation.organizationId, - }) - .from(invitation) - .where( - and( - eq(invitation.status, "pending"), - eq(invitation.organizationId, ssoProviderRecord.organizationId), - gt(invitation.expiresAt, now), - eq(invitation.email, normalizeEmail(email)), - ), - ) - .limit(1); - const pendingInvitation = pendingInvitations[0]; + const pendingInvitation = await authService.getPendingInvitation(ssoProviderRecord.organizationId, email); if (!pendingInvitation) { logger.debug("No pending invitation found for user"); @@ -114,29 +105,17 @@ async function tryCreateInvitedMembership(userId: string, email: string, ctx: Ge async function createDefaultOrganizationMembership(user: User) { logger.debug("Creating default organization for user", { userId: user.id }); - const resticPassword = cryptoUtils.generateResticPassword(); - const metadata = { resticPassword: await cryptoUtils.sealSecret(resticPassword) }; + const organizationData = await buildDefaultOrganizationData(user); await db.transaction(async (tx) => { - const orgId = Bun.randomUUIDv7(); - const slug = buildOrgSlug(user.email); - - tx.insert(organization) - .values({ - name: `${user.name}'s Workspace`, - slug, - id: orgId, - createdAt: new Date(), - metadata, - }) - .run(); + tx.insert(organization).values(organizationData).run(); tx.insert(member) .values({ id: Bun.randomUUIDv7(), userId: user.id, role: "owner", - organizationId: orgId, + organizationId: organizationData.id, createdAt: new Date(), }) .run(); @@ -144,19 +123,14 @@ async function createDefaultOrganizationMembership(user: User) { } export async function createUserDefaultOrg(userId: string, ctx: GenericEndpointContext | null) { - const users = await db.select().from(usersTable).where(eq(usersTable.id, userId)).limit(1); - const user = users[0]; + const user = await db.query.usersTable.findFirst({ where: { id: userId } }); if (!user) { throw new UnauthorizedError("User not found"); } const providerId = extractProviderIdFromContext(ctx); if (providerId) { - const [ssoProviderRecord] = await db - .select() - .from(ssoProvider) - .where(eq(ssoProvider.providerId, providerId)) - .limit(1); + const ssoProviderRecord = await authService.getSsoProviderById(providerId); if (ssoProviderRecord) { // If the user is already a member of this SSO org (accepted a past invitation), let them through @@ -166,7 +140,7 @@ export async function createUserDefaultOrg(userId: string, ctx: GenericEndpointC } // Not yet a member of this SSO org, a valid pending invitation is required. - const invitedMembership = await tryCreateInvitedMembership(userId, normalizeEmail(user.email), ctx); + const invitedMembership = await tryCreateInvitedMembership(userId, normalizeEmail(user.email), ssoProviderRecord); if (invitedMembership) { return invitedMembership; } diff --git a/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts b/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts index 7346490a..e6af370a 100644 --- a/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts +++ b/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts @@ -2,7 +2,8 @@ import { beforeEach, describe, expect, test } from "bun:test"; import type { GenericEndpointContext } from "better-auth"; import { db } from "~/server/db/db"; import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; -import { isSsoCallbackRequest, requireSsoInvitation } from "../require-sso-invitation"; +import { isSsoCallbackRequest } from "../../utils/sso-context"; +import { requireSsoInvitation } from "../require-sso-invitation"; function createMockContext(path: string, params: Record = {}): GenericEndpointContext { return { diff --git a/app/server/lib/auth/middlewares/convert-legacy-user.ts b/app/server/lib/auth/middlewares/convert-legacy-user.ts index 1cd2514e..e0cab370 100644 --- a/app/server/lib/auth/middlewares/convert-legacy-user.ts +++ b/app/server/lib/auth/middlewares/convert-legacy-user.ts @@ -4,8 +4,8 @@ import { db } from "~/server/db/db"; import { account, usersTable, member, organization } from "~/server/db/schema"; import type { AuthMiddlewareContext } from "~/server/lib/auth"; import { UnauthorizedError } from "http-errors-enhanced"; -import { cryptoUtils } from "~/server/utils/crypto"; import { normalizeUsername } from "~/lib/username"; +import { buildDefaultOrganizationData, type DefaultOrganizationData } from "../helpers/create-default-org"; export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) => { const { path, body } = ctx; @@ -40,27 +40,10 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) const passwordHash = await hashPassword(body.password); - let newOrganizationData: { - id: string; - name: string; - slug: string; - createdAt: Date; - metadata: { - resticPassword: string; - }; - } | null = null; + let newOrganizationData: DefaultOrganizationData | null = null; if (!oldMembership?.organization) { - const resticPassword = cryptoUtils.generateResticPassword(); - newOrganizationData = { - id: Bun.randomUUIDv7(), - name: `${legacyUser.name}'s Workspace`, - slug: legacyUser.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4), - createdAt: new Date(), - metadata: { - resticPassword: await cryptoUtils.sealSecret(resticPassword), - }, - }; + newOrganizationData = await buildDefaultOrganizationData(legacyUser); } db.transaction((tx) => { diff --git a/app/server/lib/auth/middlewares/require-sso-invitation.ts b/app/server/lib/auth/middlewares/require-sso-invitation.ts index 7e7ac3bd..f3db2198 100644 --- a/app/server/lib/auth/middlewares/require-sso-invitation.ts +++ b/app/server/lib/auth/middlewares/require-sso-invitation.ts @@ -1,16 +1,8 @@ import { APIError } from "better-auth/api"; import type { GenericEndpointContext } from "better-auth"; -import { db } from "~/server/db/db"; import { logger } from "~/server/utils/logger"; -import { extractProviderIdFromContext, extractProviderIdFromUrl, normalizeEmail } from "../utils/sso-context"; - -export function isSsoCallbackRequest(ctx: GenericEndpointContext | null) { - if (!ctx?.request?.url) { - return false; - } - - return extractProviderIdFromUrl(ctx.request.url) !== null; -} +import { extractProviderIdFromContext } from "../utils/sso-context"; +import { authService } from "~/server/modules/auth/auth.service"; export const requireSsoInvitation = async (userEmail: string, ctx: GenericEndpointContext | null) => { if (!ctx) { @@ -22,25 +14,14 @@ export const requireSsoInvitation = async (userEmail: string, ctx: GenericEndpoi throw new APIError("BAD_REQUEST", { message: "Missing providerId in context" }); } - const provider = await db.query.ssoProvider.findFirst({ where: { providerId } }); + const provider = await authService.getSsoProviderById(providerId); if (!provider) { throw new APIError("NOT_FOUND", { message: "SSO provider not found" }); } - const normalizedEmail = normalizeEmail(userEmail); logger.debug("Checking for pending invitations", { organizationId: provider.organizationId }); - const pendingInvitation = await db.query.invitation.findFirst({ - where: { - AND: [ - { organizationId: provider.organizationId }, - { status: "pending" }, - { expiresAt: { gt: new Date() } }, - { email: normalizedEmail }, - ], - }, - columns: { id: true }, - }); + const pendingInvitation = await authService.getPendingInvitation(provider.organizationId, userEmail); logger.debug("Pending invitation result", { found: !!pendingInvitation, invitationId: pendingInvitation?.id }); diff --git a/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts b/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts index eb00ad5f..b334cfc3 100644 --- a/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts +++ b/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts @@ -1,4 +1,4 @@ -import { db } from "~/server/db/db"; +import { authService } from "~/server/modules/auth/auth.service"; import { extractProviderIdFromUrl } from "../utils/sso-context"; export async function resolveTrustedProvidersForRequest(request?: Request): Promise { @@ -11,21 +11,10 @@ export async function resolveTrustedProvidersForRequest(request?: Request): Prom return []; } - const provider = await db.query.ssoProvider.findFirst({ - columns: { organizationId: true }, - where: { providerId }, - }); + const provider = await authService.getSsoProviderById(providerId); if (!provider) { return []; } - const autoLinkingProviders = await db.query.ssoProvider.findMany({ - columns: { providerId: true }, - where: { - organizationId: provider.organizationId, - autoLinkMatchingEmails: true, - }, - }); - - return autoLinkingProviders.map((entry) => entry.providerId); + return authService.getAutoLinkingSsoProviderIds(provider.organizationId); } diff --git a/app/server/lib/auth/utils/sso-context.ts b/app/server/lib/auth/utils/sso-context.ts index 1565c8ba..9c649956 100644 --- a/app/server/lib/auth/utils/sso-context.ts +++ b/app/server/lib/auth/utils/sso-context.ts @@ -1,35 +1,41 @@ import type { GenericEndpointContext } from "better-auth"; -const SSO_CALLBACK_PATH_SEGMENTS = ["/sso/callback/", "/sso/saml2/callback/", "/sso/saml2/sp/acs/"] as const; - const SSO_CALLBACK_PATH_PATTERN = /\/sso\/(?:callback|saml2\/callback|saml2\/sp\/acs)\/([^/]+)$/; export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } -export function isSsoCallbackPath(pathname: string): boolean { - return SSO_CALLBACK_PATH_SEGMENTS.some((segment) => pathname.includes(segment)); -} - -export function extractProviderIdFromPathname(pathname: string): string | null { - if (!isSsoCallbackPath(pathname)) { - return null; - } - - const match = pathname.match(SSO_CALLBACK_PATH_PATTERN); +function extractProviderIdFromPath(path: string): string | null { + const match = path.match(SSO_CALLBACK_PATH_PATTERN); return match?.[1] ?? null; } export function extractProviderIdFromUrl(url: string): string | null { try { const pathname = new URL(url, "http://localhost").pathname; - return extractProviderIdFromPathname(pathname); + return extractProviderIdFromPath(pathname); } catch { return null; } } +export function isSsoCallbackPath(path?: string | null): boolean { + if (!path) { + return false; + } + + return extractProviderIdFromPath(path) !== null; +} + +export function isSsoCallbackRequest(ctx?: GenericEndpointContext | null): boolean { + if (!ctx?.request?.url) { + return false; + } + + return extractProviderIdFromUrl(ctx.request.url) !== null; +} + export function extractProviderIdFromContext(ctx?: GenericEndpointContext | null) { if (!ctx) { return null; diff --git a/app/server/modules/auth/auth.errors.ts b/app/server/modules/auth/auth.errors.ts index 9837461a..ea4831e9 100644 --- a/app/server/modules/auth/auth.errors.ts +++ b/app/server/modules/auth/auth.errors.ts @@ -1,9 +1,4 @@ -export type LoginErrorCode = - | "ACCOUNT_LINK_REQUIRED" - | "EMAIL_NOT_VERIFIED" - | "INVITE_REQUIRED" - | "BANNED_USER" - | "SSO_LOGIN_FAILED"; +import type { LoginErrorCode } from "~/lib/auth-errors"; const INVITE_REQUIRED_ERRORS = new Set([ "Access denied. You must be invited to this organization before you can sign in with SSO.", @@ -38,18 +33,3 @@ export function mapAuthErrorToCode(error: string): LoginErrorCode { return "SSO_LOGIN_FAILED"; } - -export function getLoginErrorDescription(errorCode: LoginErrorCode): string { - switch (errorCode) { - case "ACCOUNT_LINK_REQUIRED": - return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator."; - case "EMAIL_NOT_VERIFIED": - return "Your identity provider did not mark your email as verified."; - case "INVITE_REQUIRED": - return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; - case "BANNED_USER": - return "You have been banned from this application. Please contact support if you believe this is an error."; - case "SSO_LOGIN_FAILED": - return "SSO authentication failed. Please try again."; - } -} diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts index 1311f031..04fa04ed 100644 --- a/app/server/modules/auth/auth.middleware.ts +++ b/app/server/modules/auth/auth.middleware.ts @@ -13,6 +13,7 @@ declare module "hono" { role?: string | null | undefined; }; organizationId: string; + membership: { role: string }; } } @@ -44,6 +45,7 @@ export const requireAuth = createMiddleware(async (c, next) => { c.set("user", user); c.set("organizationId", activeOrganizationId); + c.set("membership", membership); await withContext({ organizationId: activeOrganizationId, userId: user.id }, async () => { await next(); @@ -55,16 +57,9 @@ export const requireAuth = createMiddleware(async (c, next) => { * Verifies the user has the required role in the current organization */ export const requireOrgAdmin = createMiddleware(async (c, next) => { - const user = c.get("user"); - const organizationId = c.get("organizationId"); + const { role } = c.get("membership"); - const membership = await db.query.member.findFirst({ - where: { - AND: [{ userId: user.id }, { organizationId: organizationId }], - }, - }); - - if (!membership || (membership.role !== "owner" && membership.role !== "admin")) { + if (role !== "owner" && role !== "admin") { return c.json({ message: "Forbidden" }, 403); } diff --git a/app/server/modules/auth/auth.service.ts b/app/server/modules/auth/auth.service.ts index bdb7a2f9..215387b8 100644 --- a/app/server/modules/auth/auth.service.ts +++ b/app/server/modules/auth/auth.service.ts @@ -13,6 +13,7 @@ import { import { eq, ne, and, count, inArray } from "drizzle-orm"; import type { UserDeletionImpactDto } from "./auth.dto"; import { isReservedSsoProviderId } from "~/server/lib/auth/utils/sso-provider-id"; +import { normalizeEmail } from "~/server/lib/auth/utils/sso-context"; export class AuthService { /** @@ -105,6 +106,45 @@ export class AuthService { } } + /** + * Get an SSO provider by provider id + */ + async getSsoProviderById(providerId: string) { + return db.query.ssoProvider.findFirst({ + where: { providerId }, + columns: { id: true, providerId: true, organizationId: true }, + }); + } + + /** + * Get an active pending invitation for organization/email + */ + async getPendingInvitation(organizationId: string, email: string) { + return db.query.invitation.findFirst({ + where: { + AND: [ + { organizationId }, + { status: "pending" }, + { expiresAt: { gt: new Date() } }, + { email: normalizeEmail(email) }, + ], + }, + columns: { id: true, email: true, role: true, organizationId: true }, + }); + } + + /** + * Get trusted provider ids for organization auto-linking + */ + async getAutoLinkingSsoProviderIds(organizationId: string) { + const providers = await db.query.ssoProvider.findMany({ + columns: { providerId: true }, + where: { organizationId, autoLinkMatchingEmails: true }, + }); + + return providers.map((provider) => provider.providerId); + } + /** * Delete an SSO provider and its associated accounts */ diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts index 9266bd1d..4dde4e5b 100644 --- a/app/server/modules/system/system.controller.ts +++ b/app/server/modules/system/system.controller.ts @@ -15,25 +15,14 @@ import { type DevPanelDto, } from "./system.dto"; import { systemService } from "./system.service"; -import { requireAuth, requireOrgAdmin } from "../auth/auth.middleware"; +import { requireAdmin, requireAuth, requireOrgAdmin } from "../auth/auth.middleware"; import { db } from "../../db/db"; import { usersTable } from "../../db/schema"; import { eq } from "drizzle-orm"; import { verifyUserPassword } from "../auth/helpers"; import { cryptoUtils } from "../../utils/crypto"; -import { createMiddleware } from "hono/factory"; import { getOrganizationId } from "~/server/core/request-context"; -const requireGlobalAdmin = createMiddleware(async (c, next) => { - const user = c.get("user"); - - if (!user || user.role !== "admin") { - return c.json({ message: "Forbidden" }, 403); - } - - await next(); -}); - export const systemController = new Hono() .use(requireAuth) .get("/info", systemInfoDto, async (c) => { @@ -53,7 +42,7 @@ export const systemController = new Hono() }) .put( "/registration-status", - requireGlobalAdmin, + requireAdmin, setRegistrationStatusDto, validator("json", registrationStatusBody), async (c) => {