Files
publisher-dashboard/apps/api-server/src/router.ts

310 lines
8.4 KiB
TypeScript

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,
);
await 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",
])
.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,
};
});
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",
])
.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,
},
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,
});