Backend: - Add me.invites endpoints (list, get, accept, decline) to API contract - Create invites procedures for fetching user's pending invites - Only show invites if email matches and is verified - Refactor me routes into me/_routes.ts for consistency Frontend: - Add pending invitations section to /dashboard page - Create /account/org-invites/[inviteId] page for accept/decline - Show invite details (org, role, inviter, dates) - Redirect to org dashboard after accepting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
193 lines
5.4 KiB
TypeScript
193 lines
5.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 { meRoutes } from "./procedures/me/_routes.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",
|
|
});
|
|
}
|
|
|
|
// Mark the login request as completed - passkey verification is equivalent to email verification
|
|
await context.db
|
|
.updateTable("login_requests")
|
|
.set({ completed_at: new Date() })
|
|
.where("id", "=", String(context.loginRequestId))
|
|
.execute();
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
// 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: meRoutes,
|
|
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,
|
|
});
|