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

@@ -3,9 +3,6 @@
* First step in the login flow - validates email and returns available auth methods * First step in the login flow - validates email and returns available auth methods
*/ */
import type { APIContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { import {
COOKIE_DURATIONS, COOKIE_DURATIONS,
COOKIE_NAMES, COOKIE_NAMES,
@@ -19,8 +16,7 @@ import {
} from "../../utils/crypto.js"; } from "../../utils/crypto.js";
import { getGeoInfo, getUserAgent } from "../../utils/geo.js"; import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
import { isDeviceTrusted } from "../../utils/session.js"; import { isDeviceTrusted } from "../../utils/session.js";
import { os } from "../base.js";
const os = implement(contract);
/** /**
* Create login request handler * Create login request handler
@@ -33,7 +29,6 @@ const os = implement(contract);
*/ */
export const createLoginRequest = os.auth.createLoginRequest.handler( export const createLoginRequest = os.auth.createLoginRequest.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { email: rawEmail } = input; const { email: rawEmail } = input;
// Normalize email to lowercase // Normalize email to lowercase
@@ -41,14 +36,14 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
// Read or generate device fingerprint // Read or generate device fingerprint
let deviceFingerprint = getCookie( let deviceFingerprint = getCookie(
ctx.reqHeaders, context.reqHeaders,
COOKIE_NAMES.DEVICE_FINGERPRINT, COOKIE_NAMES.DEVICE_FINGERPRINT,
); );
if (!deviceFingerprint) { if (!deviceFingerprint) {
deviceFingerprint = generateDeviceFingerprint(); deviceFingerprint = generateDeviceFingerprint();
setCookie( setCookie(
ctx.resHeaders, context.resHeaders,
COOKIE_NAMES.DEVICE_FINGERPRINT, COOKIE_NAMES.DEVICE_FINGERPRINT,
deviceFingerprint, deviceFingerprint,
COOKIE_OPTIONS.device, COOKIE_OPTIONS.device,
@@ -56,7 +51,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
} }
// Look up user by email // Look up user by email
const user = await ctx.db const user = await context.db
.selectFrom("users") .selectFrom("users")
.select(["id", "password_hash"]) .select(["id", "password_hash"])
.where("email", "=", email) .where("email", "=", email)
@@ -70,7 +65,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
// Set placeholder login request token cookie // Set placeholder login request token cookie
setCookie( setCookie(
ctx.resHeaders, context.resHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN, COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
placeholderToken, placeholderToken,
COOKIE_OPTIONS.loginRequest, COOKIE_OPTIONS.loginRequest,
@@ -89,13 +84,13 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
// Check if device is trusted // Check if device is trusted
const isTrustedDevice = await isDeviceTrusted( const isTrustedDevice = await isDeviceTrusted(
ctx.db, context.db,
userId, userId,
deviceFingerprint, deviceFingerprint,
); );
// Check if user has passkey // Check if user has passkey
const passkey = await ctx.db const passkey = await context.db
.selectFrom("passkeys") .selectFrom("passkeys")
.select(["id"]) .select(["id"])
.where("user_id", "=", userId) .where("user_id", "=", userId)
@@ -106,13 +101,13 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
const hasPassword = user.password_hash !== null; const hasPassword = user.password_hash !== null;
// Get geo info and user agent // Get geo info and user agent
const geo = getGeoInfo(ctx.reqHeaders); const geo = getGeoInfo(context.reqHeaders);
const userAgent = getUserAgent(ctx.reqHeaders); const userAgent = getUserAgent(context.reqHeaders);
// Create login request // Create login request
const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST); const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST);
const loginRequest = await ctx.db const loginRequest = await context.db
.insertInto("login_requests") .insertInto("login_requests")
.values({ .values({
user_id: userId, user_id: userId,
@@ -132,7 +127,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
// Set login request token cookie with the real login request ID // Set login request token cookie with the real login request ID
setCookie( setCookie(
ctx.resHeaders, context.resHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN, COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
loginRequestId, loginRequestId,
COOKIE_OPTIONS.loginRequest, COOKIE_OPTIONS.loginRequest,

View File

@@ -1,12 +1,3 @@
import type { APIContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import { sendPasswordResetEmail } from "../../utils/email.js";
const os = implement(contract);
/** /**
* Forgot password handler * Forgot password handler
* Public procedure (no authentication required) * Public procedure (no authentication required)
@@ -14,16 +5,21 @@ const os = implement(contract);
* Anti-enumeration: Always returns success even if user doesn't exist * Anti-enumeration: Always returns success even if user doesn't exist
* This prevents attackers from determining which emails are registered * This prevents attackers from determining which emails are registered
*/ */
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import { sendPasswordResetEmail } from "../../utils/email.js";
import { os } from "../base.js";
export const forgotPassword = os.auth.forgotPassword.handler( export const forgotPassword = os.auth.forgotPassword.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { email } = input; const { email } = input;
// Normalize email to lowercase // Normalize email to lowercase
const normalizedEmail = email.toLowerCase(); const normalizedEmail = email.toLowerCase();
// Look up user by email // Look up user by email
const user = await ctx.db const user = await context.db
.selectFrom("users") .selectFrom("users")
.select(["id", "email"]) .select(["id", "email"])
.where("email", "=", normalizedEmail) .where("email", "=", normalizedEmail)
@@ -32,7 +28,7 @@ export const forgotPassword = os.auth.forgotPassword.handler(
// If user exists, create password reset token and send email // If user exists, create password reset token and send email
if (user) { if (user) {
// Delete any existing password reset tokens for this user (security measure) // Delete any existing password reset tokens for this user (security measure)
await ctx.db await context.db
.deleteFrom("password_resets") .deleteFrom("password_resets")
.where("user_id", "=", user.id) .where("user_id", "=", user.id)
.execute(); .execute();
@@ -43,7 +39,7 @@ export const forgotPassword = os.auth.forgotPassword.handler(
// Create password reset record with 1 hour expiry // Create password reset record with 1 hour expiry
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET); const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
await ctx.db await context.db
.insertInto("password_resets") .insertInto("password_resets")
.values({ .values({
user_id: user.id, user_id: user.id,

View File

@@ -16,9 +16,6 @@
* e. Return { status: 'completed', redirectTo: '/dashboard' or '/auth/trust-device' } * e. Return { status: 'completed', redirectTo: '/dashboard' or '/auth/trust-device' }
*/ */
import type { APIContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { import {
COOKIE_NAMES, COOKIE_NAMES,
COOKIE_OPTIONS, COOKIE_OPTIONS,
@@ -32,8 +29,7 @@ import {
isDeviceTrusted, isDeviceTrusted,
upsertUserDevice, upsertUserDevice,
} from "../../utils/session.js"; } from "../../utils/session.js";
import { os } from "../base.js";
const os = implement(contract);
/** /**
* Check if a string looks like a UUID (fake token) * Check if a string looks like a UUID (fake token)
@@ -50,11 +46,9 @@ const isUUID = (str: string): boolean => {
*/ */
export const loginIfRequestIsCompleted = export const loginIfRequestIsCompleted =
os.auth.loginIfRequestIsCompleted.handler(async ({ context }) => { os.auth.loginIfRequestIsCompleted.handler(async ({ context }) => {
const ctx = context as APIContext;
// Read login request token from cookie // Read login request token from cookie
const loginRequestToken = getCookie( const loginRequestToken = getCookie(
ctx.reqHeaders, context.reqHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN, COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
); );
@@ -78,7 +72,7 @@ export const loginIfRequestIsCompleted =
} }
// Fetch login request from database // Fetch login request from database
const loginRequest = await ctx.db const loginRequest = await context.db
.selectFrom("login_requests") .selectFrom("login_requests")
.select([ .select([
"id", "id",
@@ -115,12 +109,12 @@ export const loginIfRequestIsCompleted =
} }
// Get current request info // Get current request info
const geo = getGeoInfo(ctx.reqHeaders); const geo = getGeoInfo(context.reqHeaders);
const userAgent = getUserAgent(ctx.reqHeaders); const userAgent = getUserAgent(context.reqHeaders);
// Upsert user device // Upsert user device
const deviceId = await upsertUserDevice( const deviceId = await upsertUserDevice(
ctx.db, context.db,
userId, userId,
deviceFingerprint, deviceFingerprint,
geo, geo,
@@ -129,13 +123,13 @@ export const loginIfRequestIsCompleted =
// Check if device is already trusted // Check if device is already trusted
const deviceTrusted = await isDeviceTrusted( const deviceTrusted = await isDeviceTrusted(
ctx.db, context.db,
userId, userId,
deviceFingerprint, deviceFingerprint,
); );
// Create session with trusted mode = true (email-confirmed login) // Create session with trusted mode = true (email-confirmed login)
const session = await createSession(ctx.db, { const session = await createSession(context.db, {
userId, userId,
deviceId, deviceId,
trustedMode: true, trustedMode: true,
@@ -144,21 +138,21 @@ export const loginIfRequestIsCompleted =
}); });
// Delete the login request (it's been consumed) // Delete the login request (it's been consumed)
await ctx.db await context.db
.deleteFrom("login_requests") .deleteFrom("login_requests")
.where("id", "=", String(loginRequestId)) .where("id", "=", String(loginRequestId))
.execute(); .execute();
// Set session cookie // Set session cookie
setCookie( setCookie(
ctx.resHeaders, context.resHeaders,
COOKIE_NAMES.SESSION_TOKEN, COOKIE_NAMES.SESSION_TOKEN,
session.token, session.token,
COOKIE_OPTIONS.session, COOKIE_OPTIONS.session,
); );
// Clear login request cookie // Clear login request cookie
deleteCookie(ctx.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN); deleteCookie(context.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN);
// Determine redirect path based on device trust status // Determine redirect path based on device trust status
const redirectTo = deviceTrusted ? "/dashboard" : "/auth/trust-device"; const redirectTo = deviceTrusted ? "/dashboard" : "/auth/trust-device";

View File

@@ -1,9 +1,3 @@
import type { APIContext } from "../../context.js";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
const os = implement(contract);
/** /**
* Confirm password login via email link token * Confirm password login via email link token
* Public procedure - no authentication required * Public procedure - no authentication required
@@ -17,13 +11,16 @@ const os = implement(contract);
* This is called when user clicks the confirmation link in their email * This is called when user clicks the confirmation link in their email
* for untrusted device login attempts. * for untrusted device login attempts.
*/ */
import { ORPCError } from "@orpc/server";
import { os } from "../base.js";
export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler( export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { token } = input; const { token } = input;
// Find the login request by token // Find the login request by token
const loginRequest = await ctx.db const loginRequest = await context.db
.selectFrom("login_requests") .selectFrom("login_requests")
.select(["id", "expires_at", "completed_at"]) .select(["id", "expires_at", "completed_at"])
.where("token", "=", token) .where("token", "=", token)
@@ -48,7 +45,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler(
} }
// Mark as completed // Mark as completed
await ctx.db await context.db
.updateTable("login_requests") .updateTable("login_requests")
.set({ completed_at: new Date() }) .set({ completed_at: new Date() })
.where("id", "=", loginRequest.id) .where("id", "=", loginRequest.id)

View File

@@ -3,16 +3,13 @@
* Second step in the login flow - verifies password and completes/confirms login * Second step in the login flow - verifies password and completes/confirms login
*/ */
import type { APIContext } from "../../context.js"; import { ORPCError } from "@orpc/server";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js"; import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js";
import { generateSecureToken } from "../../utils/crypto.js"; import { generateSecureToken } from "../../utils/crypto.js";
import { sendLoginConfirmationEmail } from "../../utils/email.js"; import { sendLoginConfirmationEmail } from "../../utils/email.js";
import { verifyPassword } from "../../utils/password.js"; import { verifyPassword } from "../../utils/password.js";
import { isDeviceTrusted } from "../../utils/session.js"; import { isDeviceTrusted } from "../../utils/session.js";
import { os } from "../base.js";
const os = implement(contract);
/** /**
* Check if a string is a valid login request ID (numeric) * Check if a string is a valid login request ID (numeric)
@@ -34,12 +31,11 @@ const isValidLoginRequestId = (value: string): boolean => {
*/ */
export const loginPassword = os.auth.loginPassword.handler( export const loginPassword = os.auth.loginPassword.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { password } = input; const { password } = input;
// Read login request token from cookie // Read login request token from cookie
const loginRequestToken = getCookie( const loginRequestToken = getCookie(
ctx.reqHeaders, context.reqHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN, COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
); );
@@ -65,7 +61,7 @@ export const loginPassword = os.auth.loginPassword.handler(
const loginRequestId = loginRequestToken; const loginRequestId = loginRequestToken;
// Fetch login request with user data in single query (optimized JOIN) // Fetch login request with user data in single query (optimized JOIN)
const result = await ctx.db const result = await context.db
.selectFrom("login_requests") .selectFrom("login_requests")
.innerJoin("users", "users.id", "login_requests.user_id") .innerJoin("users", "users.id", "login_requests.user_id")
.select([ .select([
@@ -114,12 +110,12 @@ export const loginPassword = os.auth.loginPassword.handler(
// Password is valid - check if device is trusted // Password is valid - check if device is trusted
// If no device fingerprint, treat as untrusted // If no device fingerprint, treat as untrusted
const deviceTrusted = result.device_fingerprint const deviceTrusted = result.device_fingerprint
? await isDeviceTrusted(ctx.db, result.user_id, result.device_fingerprint) ? await isDeviceTrusted(context.db, result.user_id, result.device_fingerprint)
: false; : false;
if (deviceTrusted) { if (deviceTrusted) {
// Device is trusted - complete login immediately // Device is trusted - complete login immediately
await ctx.db await context.db
.updateTable("login_requests") .updateTable("login_requests")
.set({ .set({
completed_at: new Date(), completed_at: new Date(),
@@ -130,7 +126,7 @@ export const loginPassword = os.auth.loginPassword.handler(
// Device is untrusted - generate confirmation token and send email // Device is untrusted - generate confirmation token and send email
const confirmationToken = generateSecureToken(); const confirmationToken = generateSecureToken();
await ctx.db await context.db
.updateTable("login_requests") .updateTable("login_requests")
.set({ .set({
token: confirmationToken, token: confirmationToken,

View File

@@ -2,12 +2,8 @@
* Logout procedure - revokes the current session and clears the session cookie * Logout procedure - revokes the current session and clears the session cookie
*/ */
import type { AuthenticatedContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { COOKIE_NAMES, deleteCookie } from "../../utils/cookies.js"; import { COOKIE_NAMES, deleteCookie } from "../../utils/cookies.js";
import { authMiddleware, os } from "../base.js";
const os = implement(contract);
/** /**
* Logout handler * Logout handler
@@ -15,18 +11,16 @@ const os = implement(contract);
* - Revokes the current session by setting revoked_at to now() * - Revokes the current session by setting revoked_at to now()
* - Clears the session cookie from the response * - Clears the session cookie from the response
*/ */
export const logout = os.auth.logout.handler( export const logout = os.auth.logout
async ({ context }: { context: unknown }) => { .use(authMiddleware)
const ctx = context as AuthenticatedContext; .handler(async ({ context }) => {
// Revoke the current session // Revoke the current session
await ctx.db await context.db
.updateTable("sessions") .updateTable("sessions")
.set({ revoked_at: new Date() }) .set({ revoked_at: new Date() })
.where("id", "=", String(ctx.session.id)) .where("id", "=", String(context.session.id))
.execute(); .execute();
// Clear the session cookie // Clear the session cookie
deleteCookie(ctx.resHeaders, COOKIE_NAMES.SESSION_TOKEN); deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
}, });
);

View File

@@ -1,12 +1,3 @@
import type { AuthenticatedContext } from "../../context.js";
import { implement } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import { sendVerificationEmail } from "../../utils/email.js";
const os = implement(contract);
/** /**
* Resend email verification to authenticated user * Resend email verification to authenticated user
* Requires authentication * Requires authentication
@@ -18,20 +9,25 @@ const os = implement(contract);
* 4. Create new email_verifications record with 24 hour expiry * 4. Create new email_verifications record with 24 hour expiry
* 5. Send verification email (stubbed) * 5. Send verification email (stubbed)
*/ */
export const resendVerificationEmail = os.auth.resendVerificationEmail.handler(
async ({ context }) => {
const ctx = context as AuthenticatedContext;
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import { sendVerificationEmail } from "../../utils/email.js";
import { authMiddleware, os } from "../base.js";
export const resendVerificationEmail = os.auth.resendVerificationEmail
.use(authMiddleware)
.handler(async ({ context }) => {
// Check if email is already verified // Check if email is already verified
if (ctx.user.emailVerifiedAt !== null) { if (context.user.emailVerifiedAt !== null) {
// Email already verified, return early // Email already verified, return early
return; return;
} }
// Delete any existing verification tokens for this user // Delete any existing verification tokens for this user
await ctx.db await context.db
.deleteFrom("email_verifications") .deleteFrom("email_verifications")
.where("user_id", "=", ctx.user.id) .where("user_id", "=", context.user.id)
.execute(); .execute();
// Generate new secure token // Generate new secure token
@@ -39,16 +35,15 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail.handler(
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION); const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Create new verification record // Create new verification record
await ctx.db await context.db
.insertInto("email_verifications") .insertInto("email_verifications")
.values({ .values({
user_id: ctx.user.id, user_id: context.user.id,
token, token,
expires_at: expiresAt, expires_at: expiresAt,
}) })
.execute(); .execute();
// Send verification email (stubbed) // Send verification email (stubbed)
await sendVerificationEmail(ctx.user.email, token); await sendVerificationEmail(context.user.email, token);
}, });
);

View File

@@ -1,10 +1,3 @@
import type { APIContext } from "../../context.js";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { hashPassword, validatePassword } from "../../utils/password.js";
const os = implement(contract);
/** /**
* Reset password handler * Reset password handler
* Public procedure (no authentication required) * Public procedure (no authentication required)
@@ -12,13 +5,17 @@ const os = implement(contract);
* Validates the reset token, checks password strength, updates password, * Validates the reset token, checks password strength, updates password,
* marks token as used, and revokes all existing sessions * marks token as used, and revokes all existing sessions
*/ */
import { ORPCError } from "@orpc/server";
import { hashPassword, validatePassword } from "../../utils/password.js";
import { os } from "../base.js";
export const resetPassword = os.auth.resetPassword.handler( export const resetPassword = os.auth.resetPassword.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { token, newPassword } = input; const { token, newPassword } = input;
// Find the password reset token // Find the password reset token
const passwordReset = await ctx.db const passwordReset = await context.db
.selectFrom("password_resets") .selectFrom("password_resets")
.select(["id", "user_id", "expires_at", "used_at"]) .select(["id", "user_id", "expires_at", "used_at"])
.where("token", "=", token) .where("token", "=", token)
@@ -59,7 +56,7 @@ export const resetPassword = os.auth.resetPassword.handler(
const passwordHash = await hashPassword(newPassword); const passwordHash = await hashPassword(newPassword);
// Update user's password // Update user's password
await ctx.db await context.db
.updateTable("users") .updateTable("users")
.set({ .set({
password_hash: passwordHash, password_hash: passwordHash,
@@ -69,7 +66,7 @@ export const resetPassword = os.auth.resetPassword.handler(
.execute(); .execute();
// Mark the reset token as used // Mark the reset token as used
await ctx.db await context.db
.updateTable("password_resets") .updateTable("password_resets")
.set({ .set({
used_at: now, used_at: now,
@@ -78,7 +75,7 @@ export const resetPassword = os.auth.resetPassword.handler(
.execute(); .execute();
// Revoke ALL sessions for this user (security measure) // Revoke ALL sessions for this user (security measure)
await ctx.db await context.db
.updateTable("sessions") .updateTable("sessions")
.set({ .set({
revoked_at: now, revoked_at: now,

View File

@@ -8,11 +8,10 @@ import type {
} from "@simplewebauthn/types"; } from "@simplewebauthn/types";
import type { Kysely } from "kysely"; import type { Kysely } from "kysely";
import type { DB } from "@reviq/db-schema"; import type { DB } from "@reviq/db-schema";
import type { APIContext } from "../../context.js";
import type { RPInfo } from "../../utils/webauthn.js"; import type { RPInfo } from "../../utils/webauthn.js";
import { implement, ORPCError } from "@orpc/server"; import { ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { verifyRegistrationResponse } from "@simplewebauthn/server"; import { verifyRegistrationResponse } from "@simplewebauthn/server";
import { os } from "../base.js";
import { import {
COOKIE_NAMES, COOKIE_NAMES,
COOKIE_OPTIONS, COOKIE_OPTIONS,
@@ -26,8 +25,6 @@ import { hashPassword, validatePassword } from "../../utils/password.js";
import { createSession } from "../../utils/session.js"; import { createSession } from "../../utils/session.js";
import { getRPInfo, KNOWN_AAGUIDS } from "../../utils/webauthn.js"; import { getRPInfo, KNOWN_AAGUIDS } from "../../utils/webauthn.js";
const os = implement(contract);
/** /**
* Create user with password authentication * Create user with password authentication
* Validates password strength and creates user record * Validates password strength and creates user record
@@ -208,14 +205,13 @@ export async function signupWithPasskey(
* - Sends verification email (stubbed) * - Sends verification email (stubbed)
*/ */
export const signup = os.auth.signup.handler(async ({ input, context }) => { export const signup = os.auth.signup.handler(async ({ input, context }) => {
const ctx = context as APIContext;
const { email: rawEmail, password, passkeyInfo } = input; const { email: rawEmail, password, passkeyInfo } = input;
// Normalize email to lowercase // Normalize email to lowercase
const email = rawEmail.toLowerCase(); const email = rawEmail.toLowerCase();
// Check if email already exists (anti-enumeration: return generic error) // Check if email already exists (anti-enumeration: return generic error)
const existingUser = await ctx.db const existingUser = await context.db
.selectFrom("users") .selectFrom("users")
.select(["id"]) .select(["id"])
.where("email", "=", email) .where("email", "=", email)
@@ -226,17 +222,17 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
} }
// Get geo info and user agent for session creation // Get geo info and user agent for session creation
const geo = getGeoInfo(ctx.reqHeaders); const geo = getGeoInfo(context.reqHeaders);
const userAgent = getUserAgent(ctx.reqHeaders); const userAgent = getUserAgent(context.reqHeaders);
let userId: number; let userId: number;
// Delegate to appropriate signup function // Delegate to appropriate signup function
if (password) { if (password) {
userId = await signupWithPassword(ctx.db, email, password); userId = await signupWithPassword(context.db, email, password);
} else if (passkeyInfo) { } else if (passkeyInfo) {
const rpInfo = getRPInfo(ctx.origin, ctx.allowedOrigins, ctx.rpName); const rpInfo = getRPInfo(context.origin, context.allowedOrigins, context.rpName);
userId = await signupWithPasskey(ctx.db, email, passkeyInfo, rpInfo); userId = await signupWithPasskey(context.db, email, passkeyInfo, rpInfo);
} else { } else {
// Should never reach here due to schema validation // Should never reach here due to schema validation
throw new ORPCError("BAD_REQUEST", { throw new ORPCError("BAD_REQUEST", {
@@ -245,7 +241,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
} }
// Create session (7 days, trusted mode false initially, no device) // Create session (7 days, trusted mode false initially, no device)
const session = await createSession(ctx.db, { const session = await createSession(context.db, {
userId, userId,
deviceId: null, deviceId: null,
trustedMode: false, trustedMode: false,
@@ -255,7 +251,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
// Set session cookie // Set session cookie
setCookie( setCookie(
ctx.resHeaders, context.resHeaders,
COOKIE_NAMES.SESSION_TOKEN, COOKIE_NAMES.SESSION_TOKEN,
session.token, session.token,
COOKIE_OPTIONS.session, COOKIE_OPTIONS.session,
@@ -266,7 +262,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION); const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Store verification token (store raw token, not hash - it's already high-entropy) // Store verification token (store raw token, not hash - it's already high-entropy)
await ctx.db await context.db
.insertInto("email_verifications") .insertInto("email_verifications")
.values({ .values({
user_id: userId, user_id: userId,

View File

@@ -1,9 +1,3 @@
import type { APIContext } from "../../context.js";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
const os = implement(contract);
/** /**
* Verify user email with token from URL * Verify user email with token from URL
* Public procedure - no authentication required * Public procedure - no authentication required
@@ -14,13 +8,16 @@ const os = implement(contract);
* 3. Update user's email_verified_at timestamp * 3. Update user's email_verified_at timestamp
* 4. Delete the verification record * 4. Delete the verification record
*/ */
import { ORPCError } from "@orpc/server";
import { os } from "../base.js";
export const verifyEmail = os.auth.verifyEmail.handler( export const verifyEmail = os.auth.verifyEmail.handler(
async ({ input, context }) => { async ({ input, context }) => {
const ctx = context as APIContext;
const { token } = input; const { token } = input;
// Find the verification record // Find the verification record
const verification = await ctx.db const verification = await context.db
.selectFrom("email_verifications") .selectFrom("email_verifications")
.select(["id", "user_id", "expires_at"]) .select(["id", "user_id", "expires_at"])
.where("token", "=", token) .where("token", "=", token)
@@ -35,7 +32,7 @@ export const verifyEmail = os.auth.verifyEmail.handler(
// Check if token is expired // Check if token is expired
if (new Date() > verification.expires_at) { if (new Date() > verification.expires_at) {
// Clean up expired token // Clean up expired token
await ctx.db await context.db
.deleteFrom("email_verifications") .deleteFrom("email_verifications")
.where("id", "=", verification.id) .where("id", "=", verification.id)
.execute(); .execute();
@@ -46,14 +43,14 @@ export const verifyEmail = os.auth.verifyEmail.handler(
} }
// Update user's email_verified_at // Update user's email_verified_at
await ctx.db await context.db
.updateTable("users") .updateTable("users")
.set({ email_verified_at: new Date() }) .set({ email_verified_at: new Date() })
.where("id", "=", verification.user_id) .where("id", "=", verification.user_id)
.execute(); .execute();
// Delete the verification record // Delete the verification record
await ctx.db await context.db
.deleteFrom("email_verifications") .deleteFrom("email_verifications")
.where("id", "=", verification.id) .where("id", "=", verification.id)
.execute(); .execute();

View File

@@ -0,0 +1,215 @@
/**
* Base procedures with typed context for oRPC handlers
*
* Uses implement(contract).$context<T>() to provide proper type safety.
* All procedure handlers should import from this file.
*/
import type {
APIContext,
AuthenticatedContext,
LoginRequestContext,
Session,
SessionUser,
} from "../context.js";
import { implement, ORPCError } from "@orpc/server";
import { contract } from "@reviq/api-contract";
import { COOKIE_NAMES, getCookie } from "../utils/cookies.js";
import { hashToken } from "../utils/crypto.js";
/**
* Base implementer with typed APIContext
* All procedures should be derived from this
*/
export const os = implement(contract).$context<APIContext>();
/**
* Auth middleware - validates session/API token and adds user to context
* Use with os.use(authMiddleware) to create authenticated procedures
*/
export const authMiddleware = os.middleware(async ({ context, next }) => {
const { db, reqHeaders } = context;
// Try session cookie first
let tokenHash: string | undefined;
const sessionToken = getCookie(reqHeaders, COOKIE_NAMES.SESSION_TOKEN);
if (sessionToken) {
tokenHash = hashToken(sessionToken);
}
// Fall back to API key header (for CLI)
const apiKey = reqHeaders.get("x-api-key");
if (!tokenHash && apiKey) {
tokenHash = hashToken(apiKey);
}
if (!tokenHash) {
throw new ORPCError("UNAUTHORIZED", { message: "No session or API key" });
}
// Look up session (check not expired and not revoked)
const session = await db
.selectFrom("sessions")
.where("token_hash", "=", tokenHash)
.where("expires_at", ">", new Date())
.where("revoked_at", "is", null)
.selectAll()
.executeTakeFirst();
// Fall back to API token if no session found
const apiToken = !session
? await db
.selectFrom("api_tokens")
.where("token_hash", "=", tokenHash)
.where("expires_at", ">", new Date())
.selectAll()
.executeTakeFirst()
: undefined;
const userId = session?.user_id ?? apiToken?.user_id;
if (!userId) {
throw new ORPCError("UNAUTHORIZED", {
message: "Invalid or expired token",
});
}
// Update last_used_at for API tokens
if (apiToken) {
await db
.updateTable("api_tokens")
.set({ last_used_at: new Date() })
.where("id", "=", apiToken.id)
.execute();
}
// Fetch user details
const user = await db
.selectFrom("users")
.where("id", "=", userId)
.select([
"id",
"email",
"display_name",
"email_verified_at",
"is_superuser",
])
.executeTakeFirst();
if (!user) {
throw new ORPCError("UNAUTHORIZED", {
message: "User not found",
});
}
const sessionUser: SessionUser = {
id: user.id,
email: user.email,
displayName: user.display_name,
emailVerifiedAt: user.email_verified_at,
isSuperuser: user.is_superuser,
};
const sessionInfo: Session = session
? {
id: Number(session.id),
trustedMode: session.trusted_mode,
createdAt: session.created_at,
}
: {
// For API token auth, create a synthetic session object
id: 0,
trustedMode: true,
createdAt: apiToken?.created_at ?? new Date(),
};
return next({
context: {
user: sessionUser,
session: sessionInfo,
},
});
});
/**
* Login request middleware - validates login request token from cookie
*/
export const loginRequestMiddleware = os.middleware(async ({ context, next }) => {
const { db, reqHeaders } = context;
// Read login request token from cookie
const loginRequestToken = getCookie(
reqHeaders,
COOKIE_NAMES.LOGIN_REQUEST_TOKEN,
);
if (!loginRequestToken) {
throw new ORPCError("BAD_REQUEST", {
message: "No login request found",
});
}
// Check if token is a valid login request ID (numeric)
const num = Number(loginRequestToken);
if (Number.isNaN(num) || !Number.isInteger(num) || num <= 0) {
throw new ORPCError("BAD_REQUEST", {
message: "Invalid login request",
});
}
const loginRequestId = loginRequestToken;
// Fetch login request with user data
const result = await db
.selectFrom("login_requests")
.innerJoin("users", "users.id", "login_requests.user_id")
.select([
"login_requests.id",
"login_requests.user_id",
"login_requests.expires_at",
"users.email",
"users.display_name",
"users.email_verified_at",
"users.is_superuser",
])
.where("login_requests.id", "=", loginRequestId)
.where("login_requests.expires_at", ">", new Date())
.executeTakeFirst();
if (!result) {
throw new ORPCError("BAD_REQUEST", {
message: "Login request expired or not found",
});
}
const sessionUser: SessionUser = {
id: result.user_id,
email: result.email,
displayName: result.display_name,
emailVerifiedAt: result.email_verified_at,
isSuperuser: result.is_superuser,
};
return next({
context: {
loginRequestId: Number(result.id),
user: sessionUser,
},
});
});
/**
* Superuser middleware - requires admin access (must be used after authMiddleware)
*/
export const superuserMiddleware = os.middleware(async ({ context, next }) => {
// This middleware should be used after authMiddleware
const ctx = context as AuthenticatedContext;
if (!ctx.user.isSuperuser) {
throw new ORPCError("FORBIDDEN", {
message: "Superuser access required",
});
}
return next();
});
// Type exports for use in procedure files
export type { APIContext, AuthenticatedContext, LoginRequestContext };

View File

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