Fix Session.id type and restore nested passkey routes

- Change Session.id from number to string to match DB bigint type
- Restore me.passkeys.{list,rename,delete} nested route structure
- Remove unnecessary String() conversion in logout procedure
- Auto-formatted procedure files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 15:44:45 +08:00
parent 1858ea9783
commit 617fa78046
7 changed files with 285 additions and 213 deletions

View File

@@ -110,7 +110,11 @@ export const loginPassword = os.auth.loginPassword.handler(
// Password is valid - check if device is trusted
// If no device fingerprint, treat as untrusted
const deviceTrusted = result.device_fingerprint
? await isDeviceTrusted(context.db, result.user_id, result.device_fingerprint)
? await isDeviceTrusted(
context.db,
result.user_id,
result.device_fingerprint,
)
: false;
if (deviceTrusted) {

View File

@@ -14,13 +14,13 @@ import { authMiddleware, os } from "../base.js";
export const logout = os.auth.logout
.use(authMiddleware)
.handler(async ({ context }) => {
// Revoke the current session
await context.db
.updateTable("sessions")
.set({ revoked_at: new Date() })
.where("id", "=", String(context.session.id))
.execute();
// Revoke the current session
await context.db
.updateTable("sessions")
.set({ revoked_at: new Date() })
.where("id", "=", context.session.id)
.execute();
// Clear the session cookie
deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
});
// Clear the session cookie
deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
});

View File

@@ -18,32 +18,32 @@ import { authMiddleware, os } from "../base.js";
export const resendVerificationEmail = os.auth.resendVerificationEmail
.use(authMiddleware)
.handler(async ({ context }) => {
// Check if email is already verified
if (context.user.emailVerifiedAt !== null) {
// Email already verified, return early
return;
}
// Check if email is already verified
if (context.user.emailVerifiedAt !== null) {
// Email already verified, return early
return;
}
// Delete any existing verification tokens for this user
await context.db
.deleteFrom("email_verifications")
.where("user_id", "=", context.user.id)
.execute();
// Delete any existing verification tokens for this user
await context.db
.deleteFrom("email_verifications")
.where("user_id", "=", context.user.id)
.execute();
// Generate new secure token
const token = generateSecureToken();
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Generate new secure token
const token = generateSecureToken();
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Create new verification record
await context.db
.insertInto("email_verifications")
.values({
user_id: context.user.id,
token,
expires_at: expiresAt,
})
.execute();
// Create new verification record
await context.db
.insertInto("email_verifications")
.values({
user_id: context.user.id,
token,
expires_at: expiresAt,
})
.execute();
// Send verification email (stubbed)
await sendVerificationEmail(context.user.email, token);
});
// Send verification email (stubbed)
await sendVerificationEmail(context.user.email, token);
});

View File

@@ -2,16 +2,15 @@
* Signup procedure - creates a new user account with email + password or passkey
*/
import type { DB } from "@reviq/db-schema";
import type {
PublicKeyCredentialCreationOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/types";
import type { Kysely } from "kysely";
import type { DB } from "@reviq/db-schema";
import type { RPInfo } from "../../utils/webauthn.js";
import { ORPCError } from "@orpc/server";
import { verifyRegistrationResponse } from "@simplewebauthn/server";
import { os } from "../base.js";
import {
COOKIE_NAMES,
COOKIE_OPTIONS,
@@ -24,6 +23,7 @@ import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
import { hashPassword, validatePassword } from "../../utils/password.js";
import { createSession } from "../../utils/session.js";
import { getRPInfo, KNOWN_AAGUIDS } from "../../utils/webauthn.js";
import { os } from "../base.js";
/**
* Create user with password authentication
@@ -231,7 +231,11 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
if (password) {
userId = await signupWithPassword(context.db, email, password);
} else if (passkeyInfo) {
const rpInfo = getRPInfo(context.origin, context.allowedOrigins, context.rpName);
const rpInfo = getRPInfo(
context.origin,
context.allowedOrigins,
context.rpName,
);
userId = await signupWithPasskey(context.db, email, passkeyInfo, rpInfo);
} else {
// Should never reach here due to schema validation

View File

@@ -111,13 +111,13 @@ export const authMiddleware = os.middleware(async ({ context, next }) => {
const sessionInfo: Session = session
? {
id: Number(session.id),
id: session.id,
trustedMode: session.trusted_mode,
createdAt: session.created_at,
}
: {
// For API token auth, create a synthetic session object
id: 0,
id: "0",
trustedMode: true,
createdAt: apiToken?.created_at ?? new Date(),
};
@@ -133,69 +133,71 @@ export const authMiddleware = os.middleware(async ({ context, next }) => {
/**
* Login request middleware - validates login request token from cookie
*/
export const loginRequestMiddleware = os.middleware(async ({ context, next }) => {
const { db, reqHeaders } = context;
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,
);
// 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",
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,
},
});
}
// 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)