310 lines
8.4 KiB
TypeScript
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,
|
|
});
|