Add typed context and middleware for oRPC procedures

Use implement(contract).$context<APIContext>() for proper type safety
in all procedure handlers. Create authMiddleware and loginRequestMiddleware
using os.middleware() and apply with .use() on routes requiring auth.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 15:36:26 +08:00
parent 829d365e80
commit a4d1f28f3d
12 changed files with 483 additions and 334 deletions

View File

@@ -1,10 +1,4 @@
import type {
APIContext,
AuthenticatedContext,
LoginRequestContext,
} from "./context.js";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { ORPCError } from "@orpc/server";
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";
@@ -15,6 +9,7 @@ import { resendVerificationEmail as resendVerificationHandler } from "./procedur
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 {
createAuthenticationOptions as createAuthOptions,
createRegistrationOptions as createRegOptions,
@@ -24,74 +19,60 @@ import {
verifyRegistration as verifyReg,
} from "./utils/webauthn.js";
const os = implement(contract);
// Auth procedures
// 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 ctx = context as APIContext;
const { email } = input;
// For signup flow, we don't have a user yet
// The user will be created when signup is called with the passkeyInfo
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName);
const rpInfo = getRPInfo(context.origin, context.allowedOrigins, context.rpName);
const result = await createRegOptions(ctx.db, rpInfo, { email });
const result = await createRegOptions(context.db, rpInfo, { email });
return result;
},
);
const verifyRegistration = os.auth.webauthn.verifyRegistration.handler(
async ({ input, context }) => {
const ctx = context as AuthenticatedContext;
const verifyRegistration = os.auth.webauthn.verifyRegistration
.use(authMiddleware)
.handler(async ({ input, context }) => {
const { challengeId, response } = input;
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName);
await verifyReg(ctx.db, rpInfo, ctx.user.id, challengeId, response);
},
);
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.handler(async ({ context }) => {
const ctx = context as LoginRequestContext;
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName);
const result = await createAuthOptions(ctx.db, rpInfo, ctx.user.id);
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.handler(
async ({ input, context }) => {
const ctx = context as LoginRequestContext;
const verifyAuthentication = os.auth.webauthn.verifyAuthentication
.use(loginRequestMiddleware)
.handler(async ({ input, context }) => {
const { challengeId, response } = input;
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName);
const rpInfo = getRPInfo(context.origin, context.allowedOrigins, context.rpName);
const verified = await verifyAuth(
ctx.db,
context.db,
rpInfo,
ctx.user.id,
context.user.id,
challengeId,
response,
);
@@ -101,256 +82,252 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication.handler(
message: "Authentication failed",
});
}
},
);
});
// Me procedures
const meGet = os.me.get.handler(async () => {
throw new Error("Not implemented");
const meGet = os.me.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const setupProfile = os.me.setupProfile.handler(async () => {
throw new Error("Not implemented");
const setupProfile = os.me.setupProfile.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const updateProfile = os.me.updateProfile.handler(async () => {
throw new Error("Not implemented");
const updateProfile = os.me.updateProfile.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const meDelete = os.me.delete.handler(async () => {
throw new Error("Not implemented");
const meDelete = os.me.delete.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const setPassword = os.me.setPassword.handler(async () => {
throw new Error("Not implemented");
const setPassword = os.me.setPassword.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const listPasskeys = os.me.listPasskeys.handler(async ({ context }) => {
const ctx = context as AuthenticatedContext;
const listPasskeys = os.me.listPasskeys
.use(authMiddleware)
.handler(async ({ context }) => {
const passkeys = await getUserPasskeys(context.db, context.user.id);
const passkeys = await getUserPasskeys(ctx.db, ctx.user.id);
return passkeys.map((p) => ({
id: p.id,
name: p.name,
createdAt: p.createdAt,
lastUsedAt: p.lastUsedAt,
}));
});
return passkeys.map((p) => ({
id: p.id,
name: p.name,
createdAt: p.createdAt,
lastUsedAt: p.lastUsedAt,
}));
});
const createPasskey = os.me.createPasskey.handler(
async ({ input, context }) => {
const ctx = context as AuthenticatedContext;
const createPasskey = os.me.createPasskey
.use(authMiddleware)
.handler(async ({ input, context }) => {
const { name: _name } = input;
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName);
const result = await createRegOptions(ctx.db, rpInfo, {
id: ctx.user.id,
email: ctx.user.email,
displayName: ctx.user.displayName,
const rpInfo = getRPInfo(context.origin, context.allowedOrigins, context.rpName);
const result = await createRegOptions(context.db, rpInfo, {
id: context.user.id,
email: context.user.email,
displayName: context.user.displayName,
});
return result;
},
);
});
const renamePasskey = os.me.renamePasskey.handler(
async ({ input, context }) => {
const ctx = context as AuthenticatedContext;
const renamePasskey = os.me.renamePasskey
.use(authMiddleware)
.handler(async ({ input, context }) => {
const { passkeyId, name } = input;
await ctx.db
await context.db
.updateTable("passkeys")
.set({ name })
.where("id", "=", String(passkeyId))
.where("user_id", "=", ctx.user.id)
.where("user_id", "=", context.user.id)
.execute();
},
);
});
const deletePasskey = os.me.deletePasskey.handler(
async ({ input, context }) => {
const ctx = context as AuthenticatedContext;
const deletePasskey = os.me.deletePasskey
.use(authMiddleware)
.handler(async ({ input, context }) => {
const { passkeyId } = input;
// Check if this is the last passkey and user has no password
const user = await ctx.db
const user = await context.db
.selectFrom("users")
.select(["password_hash"])
.where("id", "=", ctx.user.id)
.where("id", "=", context.user.id)
.executeTakeFirst();
const passkeyCount = await ctx.db
const passkeyCount = await context.db
.selectFrom("passkeys")
.select(ctx.db.fn.countAll().as("count"))
.where("user_id", "=", ctx.user.id)
.select(context.db.fn.countAll().as("count"))
.where("user_id", "=", context.user.id)
.executeTakeFirst();
if (!user?.password_hash && Number(passkeyCount?.count ?? 0) <= 1) {
throw new Error(
"Cannot delete the last passkey when you have no password set",
);
throw new ORPCError("BAD_REQUEST", {
message: "Cannot delete the last passkey when you have no password set",
});
}
await ctx.db
await context.db
.deleteFrom("passkeys")
.where("id", "=", String(passkeyId))
.where("user_id", "=", ctx.user.id)
.where("user_id", "=", context.user.id)
.execute();
},
);
});
const listSessions = os.me.listSessions.handler(async () => {
throw new Error("Not implemented");
const listSessions = os.me.listSessions.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const revokeSession = os.me.revokeSession.handler(async () => {
throw new Error("Not implemented");
const revokeSession = os.me.revokeSession.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const revokeAllSessions = os.me.revokeAllSessions.handler(async () => {
throw new Error("Not implemented");
const revokeAllSessions = os.me.revokeAllSessions.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const getDeviceInfo = os.me.getDeviceInfo.handler(async () => {
throw new Error("Not implemented");
const getDeviceInfo = os.me.getDeviceInfo.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const trustDevice = os.me.trustDevice.handler(async () => {
throw new Error("Not implemented");
const trustDevice = os.me.trustDevice.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const listTrustedDevices = os.me.listTrustedDevices.handler(async () => {
throw new Error("Not implemented");
const listTrustedDevices = os.me.listTrustedDevices.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const untrustDevice = os.me.untrustDevice.handler(async () => {
throw new Error("Not implemented");
const untrustDevice = os.me.untrustDevice.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices.handler(
async () => {
throw new Error("Not implemented");
},
);
const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Orgs procedures
const orgsList = os.orgs.list.handler(async () => {
throw new Error("Not implemented");
// Orgs procedures (all require auth)
const orgsList = os.orgs.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const orgsCreate = os.orgs.create.handler(async () => {
throw new Error("Not implemented");
const orgsCreate = os.orgs.create.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const orgsGet = os.orgs.get.handler(async () => {
throw new Error("Not implemented");
const orgsGet = os.orgs.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const orgsUpdate = os.orgs.update.handler(async () => {
throw new Error("Not implemented");
const orgsUpdate = os.orgs.update.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const orgsDelete = os.orgs.delete.handler(async () => {
throw new Error("Not implemented");
const orgsDelete = os.orgs.delete.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const orgsLeave = os.orgs.leave.handler(async () => {
throw new Error("Not implemented");
const orgsLeave = os.orgs.leave.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Orgs members procedures
const membersList = os.orgs.members.list.handler(async () => {
throw new Error("Not implemented");
const membersList = os.orgs.members.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const membersUpdateRole = os.orgs.members.updateRole.handler(async () => {
throw new Error("Not implemented");
const membersUpdateRole = os.orgs.members.updateRole.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const membersRemove = os.orgs.members.remove.handler(async () => {
throw new Error("Not implemented");
const membersRemove = os.orgs.members.remove.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Orgs invites procedures
const invitesList = os.orgs.invites.list.handler(async () => {
throw new Error("Not implemented");
const invitesList = os.orgs.invites.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const invitesCreate = os.orgs.invites.create.handler(async () => {
throw new Error("Not implemented");
const invitesCreate = os.orgs.invites.create.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const invitesCancel = os.orgs.invites.cancel.handler(async () => {
throw new Error("Not implemented");
const invitesCancel = os.orgs.invites.cancel.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const invitesAccept = os.orgs.invites.accept.handler(async () => {
throw new Error("Not implemented");
const invitesAccept = os.orgs.invites.accept.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Orgs sites procedures
const sitesList = os.orgs.sites.list.handler(async () => {
throw new Error("Not implemented");
const sitesList = os.orgs.sites.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Admin orgs procedures
const adminOrgsList = os.admin.orgs.list.handler(async () => {
throw new Error("Not implemented");
// Admin orgs procedures (require superuser - for now just auth, will add superuser middleware later)
const adminOrgsList = os.admin.orgs.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsGet = os.admin.orgs.get.handler(async () => {
throw new Error("Not implemented");
const adminOrgsGet = os.admin.orgs.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsCreate = os.admin.orgs.create.handler(async () => {
throw new Error("Not implemented");
const adminOrgsCreate = os.admin.orgs.create.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsUpdate = os.admin.orgs.update.handler(async () => {
throw new Error("Not implemented");
const adminOrgsUpdate = os.admin.orgs.update.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsDelete = os.admin.orgs.delete.handler(async () => {
throw new Error("Not implemented");
const adminOrgsDelete = os.admin.orgs.delete.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsListSites = os.admin.orgs.listSites.handler(async () => {
throw new Error("Not implemented");
const adminOrgsListSites = os.admin.orgs.listSites.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsAddSite = os.admin.orgs.addSite.handler(async () => {
throw new Error("Not implemented");
const adminOrgsAddSite = os.admin.orgs.addSite.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsRemoveSite = os.admin.orgs.removeSite.handler(async () => {
throw new Error("Not implemented");
const adminOrgsRemoveSite = os.admin.orgs.removeSite.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Admin users procedures
const adminUsersList = os.admin.users.list.handler(async () => {
throw new Error("Not implemented");
const adminUsersList = os.admin.users.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersGet = os.admin.users.get.handler(async () => {
throw new Error("Not implemented");
const adminUsersGet = os.admin.users.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersCreate = os.admin.users.create.handler(async () => {
throw new Error("Not implemented");
const adminUsersCreate = os.admin.users.create.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersUpdate = os.admin.users.update.handler(async () => {
throw new Error("Not implemented");
const adminUsersUpdate = os.admin.users.update.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersConfirmEmail = os.admin.users.confirmEmail.handler(async () => {
throw new Error("Not implemented");
const adminUsersConfirmEmail = os.admin.users.confirmEmail.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Admin auth procedures
const adminAuthCompleteLogin = os.admin.auth.completeLogin.handler(async () => {
throw new Error("Not implemented");
const adminAuthCompleteLogin = os.admin.auth.completeLogin.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Build the router