import { ORPCError } from "@orpc/server"; import { adminRoutes } from "./procedures/admin/_routes.js"; import { createLoginRequest as createLoginRequestHandler } from "./procedures/auth/create-login-request.js"; import { forgotPassword as forgotPasswordHandler } from "./procedures/auth/forgot-password.js"; import { loginIfRequestIsCompleted as loginIfRequestIsCompletedHandler } from "./procedures/auth/login-if-completed.js"; import { loginPassword as loginPasswordHandler } from "./procedures/auth/login-password.js"; import { loginPasswordConfirm as loginPasswordConfirmHandler } from "./procedures/auth/login-password-confirm.js"; import { logout as logoutHandler } from "./procedures/auth/logout.js"; import { resendVerificationEmail as resendVerificationHandler } from "./procedures/auth/resend-verification.js"; import { resetPassword as resetPasswordHandler } from "./procedures/auth/reset-password.js"; import { signup as signupHandler } from "./procedures/auth/signup.js"; import { verifyEmail as verifyEmailHandler } from "./procedures/auth/verify-email.js"; import { authMiddleware, loginRequestMiddleware, os, } from "./procedures/base.js"; import { meDelete } from "./procedures/me/delete.js"; import { getDeviceInfo, listTrustedDevices, revokeAllTrustedDevices, trustDevice, untrustDevice, } from "./procedures/me/devices.js"; import { deletePasskey, listPasskeys, renamePasskey, } from "./procedures/me/passkeys.js"; import { listSessions, revokeAllSessions, revokeSession, } from "./procedures/me/sessions.js"; import { setPassword } from "./procedures/me/set-password.js"; import { updateProfile } from "./procedures/me/update-profile.js"; import { invitesAccept, invitesCancel, invitesCreate, invitesList, membersList, membersRemove, membersUpdateRole, orgsCreate, orgsDelete, orgsGet, orgsLeave, orgsList, orgsUpdate, sitesList, } from "./procedures/orgs/index.js"; import { createAuthenticationOptions as createAuthOptions, createRegistrationOptions as createRegOptions, getRPInfo, verifyAuthentication as verifyAuth, verifyRegistration as verifyReg, } from "./utils/webauthn.js"; // Auth procedures (imported from procedure files) const signup = signupHandler; const verifyEmail = verifyEmailHandler; const resendVerificationEmail = resendVerificationHandler; const createLoginRequest = createLoginRequestHandler; const loginPassword = loginPasswordHandler; const loginPasswordConfirm = loginPasswordConfirmHandler; const loginIfRequestIsCompleted = loginIfRequestIsCompletedHandler; const forgotPassword = forgotPasswordHandler; const resetPassword = resetPasswordHandler; const logout = logoutHandler; // WebAuthn procedures const createRegistrationOptions = os.auth.webauthn.createRegistrationOptions.handler( async ({ input, context }) => { const { email } = input; // Look up existing user by email to exclude their credentials const existingUser = await context.db .selectFrom("users") .select(["id", "display_name"]) .where("email", "=", email) .executeTakeFirst(); const rpInfo = getRPInfo( context.origin, context.allowedOrigins, context.rpName, ); const result = await createRegOptions(context.db, rpInfo, { id: existingUser?.id, email, displayName: existingUser?.display_name, }); return result; }, ); const verifyRegistration = os.auth.webauthn.verifyRegistration .use(authMiddleware) .handler(async ({ input, context }) => { const { challengeId, response } = input; const rpInfo = getRPInfo( context.origin, context.allowedOrigins, context.rpName, ); return verifyReg( context.db, rpInfo, context.user.id, challengeId, response, ); }); const createAuthenticationOptions = os.auth.webauthn.createAuthenticationOptions .use(loginRequestMiddleware) .handler(async ({ context }) => { const rpInfo = getRPInfo( context.origin, context.allowedOrigins, context.rpName, ); const result = await createAuthOptions(context.db, rpInfo, context.user.id); return result; }); const verifyAuthentication = os.auth.webauthn.verifyAuthentication .use(loginRequestMiddleware) .handler(async ({ input, context }) => { const { challengeId, response } = input; const rpInfo = getRPInfo( context.origin, context.allowedOrigins, context.rpName, ); const verified = await verifyAuth( context.db, rpInfo, context.user.id, challengeId, response, ); if (!verified) { throw new ORPCError("BAD_REQUEST", { message: "Authentication failed", }); } }); // Me procedures const meGet = os.me.get.use(authMiddleware).handler(async ({ context }) => { const user = await context.db .selectFrom("users") .select([ "id", "email", "display_name", "full_name", "phone_number", "avatar_url", "email_verified_at", "is_superuser", "password_hash", ]) .where("id", "=", context.user.id) .executeTakeFirstOrThrow(); return { id: user.id, email: user.email, displayName: user.display_name, fullName: user.full_name, phoneNumber: user.phone_number, avatarUrl: user.avatar_url, emailVerified: user.email_verified_at !== null, needsSetup: user.display_name === null, isSuperuser: user.is_superuser, hasPassword: user.password_hash !== null, }; }); const meAuthStatus = os.me.authStatus .use(authMiddleware) .handler(async ({ context }) => { const user = await context.db .selectFrom("users") .select([ "id", "email", "display_name", "full_name", "phone_number", "avatar_url", "email_verified_at", "is_superuser", "password_hash", ]) .where("id", "=", context.user.id) .executeTakeFirstOrThrow(); return { user: { id: user.id, email: user.email, displayName: user.display_name, fullName: user.full_name, phoneNumber: user.phone_number, avatarUrl: user.avatar_url, emailVerified: user.email_verified_at !== null, needsSetup: user.display_name === null, isSuperuser: user.is_superuser, hasPassword: user.password_hash !== null, }, auth: context.auth, }; }); const setupProfile = os.me.setupProfile .use(authMiddleware) .handler(async ({ input, context }) => { const { displayName, fullName, phoneNumber } = input; await context.db .updateTable("users") .set({ display_name: displayName, full_name: fullName ?? null, phone_number: phoneNumber ?? null, updated_at: new Date(), }) .where("id", "=", context.user.id) .execute(); }); // Me procedures imported from ./procedures/me/* // - updateProfile, setPassword, meDelete // - listPasskeys, renamePasskey, deletePasskey // - listSessions, revokeSession, revokeAllSessions // - getDeviceInfo, trustDevice, listTrustedDevices, untrustDevice, revokeAllTrustedDevices // Orgs procedures - imported from ./procedures/orgs/index.js // - orgsList, orgsCreate, orgsGet, orgsUpdate, orgsDelete, orgsLeave // - membersList, membersUpdateRole, membersRemove // - invitesList, invitesCreate, invitesCancel, invitesAccept // - sitesList // Build the router export const router = os.router({ auth: { signup, verifyEmail, resendVerificationEmail, createLoginRequest, loginPassword, loginPasswordConfirm, loginIfRequestIsCompleted, forgotPassword, resetPassword, logout, webauthn: { createRegistrationOptions, verifyRegistration, createAuthenticationOptions, verifyAuthentication, }, }, me: { get: meGet, authStatus: meAuthStatus, setupProfile, updateProfile, delete: meDelete, setPassword, passkeys: { list: listPasskeys, rename: renamePasskey, delete: deletePasskey, }, listSessions, revokeSession, revokeAllSessions, getDeviceInfo, trustDevice, listTrustedDevices, untrustDevice, revokeAllTrustedDevices, }, orgs: { list: orgsList, create: orgsCreate, get: orgsGet, update: orgsUpdate, delete: orgsDelete, leave: orgsLeave, members: { list: membersList, updateRole: membersUpdateRole, remove: membersRemove, }, invites: { list: invitesList, create: invitesCreate, cancel: invitesCancel, accept: invitesAccept, }, sites: { list: sitesList, }, }, admin: adminRoutes, });