Replace void returns with { success: true } across all API endpoints
- Add successResponseSchema to common.ts for explicit success responses
- Update all auth, me, orgs, and admin procedures to return { success: true }
- Update contract.ts to use successResponseSchema instead of z.void()
- Add ast-grep rule to prevent future z.void() usage in contracts
- Add build:packages script to root package.json
- Fix test file lint errors with eslint-disable comments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,9 +23,9 @@ import {
|
||||
} from "bun:test";
|
||||
import { call } from "@orpc/server";
|
||||
import { router } from "../../router.js";
|
||||
import { hashPassword } from "../../utils/password.js";
|
||||
import { hashToken } from "../../utils/crypto.js";
|
||||
import { COOKIE_NAMES } from "../../utils/cookies.js";
|
||||
import { hashToken } from "../../utils/crypto.js";
|
||||
import { hashPassword } from "../../utils/password.js";
|
||||
import { TEST_RP } from "../helpers/test-constants.js";
|
||||
import {
|
||||
createTestDb,
|
||||
@@ -85,7 +85,7 @@ function createAPIContext(options?: {
|
||||
* Create a real session in the database and return the token
|
||||
*/
|
||||
async function createSession(userId: number): Promise<string> {
|
||||
const token = "test-session-" + String(Date.now()) + String(Math.random());
|
||||
const token = `test-session-${String(Date.now())}${String(Math.random())}`;
|
||||
const tokenHashValue = await hashToken(token);
|
||||
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
||||
|
||||
@@ -110,8 +110,7 @@ async function createSession(userId: number): Promise<string> {
|
||||
async function createApiToken(
|
||||
userId: number,
|
||||
): Promise<{ token: string; name: string }> {
|
||||
const token =
|
||||
"test-api-token-" + String(Date.now()) + String(Math.random());
|
||||
const token = `test-api-token-${String(Date.now())}${String(Math.random())}`;
|
||||
const tokenHashValue = await hashToken(token);
|
||||
const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS);
|
||||
|
||||
@@ -523,6 +522,7 @@ describe("me.setPassword", () => {
|
||||
const sessionToken = await createSession(user.id);
|
||||
const context = createAPIContext({ sessionToken });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression
|
||||
await expect(
|
||||
call(
|
||||
router.me.setPassword,
|
||||
@@ -544,6 +544,7 @@ describe("me.setPassword", () => {
|
||||
const sessionToken = await createSession(user.id);
|
||||
const context = createAPIContext({ sessionToken });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression
|
||||
await expect(
|
||||
call(
|
||||
router.me.setPassword,
|
||||
@@ -567,6 +568,7 @@ describe("me.setPassword", () => {
|
||||
// Password must be at least 8 chars to pass schema validation
|
||||
// "password" passes length check but fails zxcvbn strength check
|
||||
// zxcvbn provides feedback like "This is a top-10 common password"
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression
|
||||
await expect(
|
||||
call(
|
||||
router.me.setPassword,
|
||||
@@ -611,6 +613,7 @@ describe("me.delete", () => {
|
||||
const sessionToken = await createSession(user.id);
|
||||
const context = createAPIContext({ sessionToken });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression
|
||||
await expect(
|
||||
call(router.me.delete, { password: "anything" }, { context }),
|
||||
).rejects.toThrow("Cannot delete account without a password");
|
||||
@@ -626,6 +629,7 @@ describe("me.delete", () => {
|
||||
const sessionToken = await createSession(user.id);
|
||||
const context = createAPIContext({ sessionToken });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression
|
||||
await expect(
|
||||
call(router.me.delete, { password: "WrongPassword123!" }, { context }),
|
||||
).rejects.toThrow("Incorrect password");
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import type { Database } from "@reviq/db-schema";
|
||||
import type { Kysely } from "kysely";
|
||||
import { join } from "node:path";
|
||||
import { createDb } from "@reviq/db";
|
||||
import type { Kysely } from "kysely";
|
||||
import { sql } from "kysely";
|
||||
import pg from "pg";
|
||||
|
||||
@@ -134,14 +134,13 @@ async function ensureTestDatabaseExists(): Promise<void> {
|
||||
*
|
||||
* @throws Error if repo root cannot be found
|
||||
*/
|
||||
function findRepoRoot(): string {
|
||||
const { existsSync } = require("node:fs");
|
||||
async function findRepoRoot(): Promise<string> {
|
||||
let current = import.meta.dir;
|
||||
|
||||
// Walk up to 10 levels to find the repo root
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const migrationsPath = join(current, "db", "migrations");
|
||||
if (existsSync(migrationsPath)) {
|
||||
if (await Bun.file(migrationsPath).exists()) {
|
||||
return current;
|
||||
}
|
||||
const parent = join(current, "..");
|
||||
@@ -167,7 +166,7 @@ export async function runMigrations(): Promise<void> {
|
||||
// Ensure the database exists first
|
||||
await ensureTestDatabaseExists();
|
||||
|
||||
const repoRoot = findRepoRoot();
|
||||
const repoRoot = await findRepoRoot();
|
||||
|
||||
const proc = Bun.spawn(["dbmate", "up"], {
|
||||
env: { ...process.env, DATABASE_URL: testDbUrl },
|
||||
|
||||
@@ -46,4 +46,6 @@ export const adminAuthCompleteLogin = os.admin.auth.completeLogin
|
||||
.set({ completed_at: new Date() })
|
||||
.where("id", "=", anyRequest.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -33,4 +33,6 @@ export const adminOrgsDelete = os.admin.orgs.delete
|
||||
.execute();
|
||||
await trx.deleteFrom("orgs").where("id", "=", org.id).execute();
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -68,6 +68,8 @@ export const adminOrgsAddSite = os.admin.orgs.addSite
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
export const adminOrgsRemoveSite = os.admin.orgs.removeSite
|
||||
@@ -94,4 +96,6 @@ export const adminOrgsRemoveSite = os.admin.orgs.removeSite
|
||||
if (!result.numDeletedRows || result.numDeletedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Site not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ export const adminOrgsUpdate = os.admin.orgs.update
|
||||
if (!org) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
|
||||
}
|
||||
return;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const updates: Partial<{
|
||||
@@ -47,4 +47,6 @@ export const adminOrgsUpdate = os.admin.orgs.update
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -21,4 +21,6 @@ export const adminUsersConfirmEmail = os.admin.users.confirmEmail
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "User not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -60,4 +60,6 @@ export const adminUsersCreate = os.admin.users.create
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export const adminUsersUpdate = os.admin.users.update
|
||||
if (!user) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "User not found" });
|
||||
}
|
||||
return;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Prevent superuser from demoting themselves
|
||||
@@ -45,4 +45,6 @@ export const adminUsersUpdate = os.admin.users.update
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "User not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -57,5 +57,6 @@ export const forgotPassword = os.auth.forgotPassword.handler(
|
||||
|
||||
// Always return success (anti-enumeration)
|
||||
// Don't reveal whether the email exists or not
|
||||
return { success: true };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler(
|
||||
|
||||
// If already completed, return success (idempotent)
|
||||
if (loginRequest.completed_at !== null) {
|
||||
return;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Mark as completed
|
||||
@@ -50,5 +50,7 @@ export const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler(
|
||||
.set({ completed_at: new Date() })
|
||||
.where("id", "=", loginRequest.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -111,6 +111,6 @@ export const loginPassword = os.auth.loginPassword.handler(
|
||||
await sendLoginConfirmationEmail(result.email, result.token);
|
||||
}
|
||||
|
||||
// Return void (success)
|
||||
return { success: true };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -23,4 +23,6 @@ export const logout = os.auth.logout
|
||||
|
||||
// Clear the session cookie
|
||||
deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail
|
||||
// Check if email is already verified
|
||||
if (context.user.emailVerifiedAt !== null) {
|
||||
// Email already verified, return early
|
||||
return;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Delete any existing verification tokens for this user
|
||||
@@ -49,4 +49,6 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail
|
||||
|
||||
// Send verification email (stubbed)
|
||||
await sendVerificationEmail(context.user.email, token);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -84,6 +84,6 @@ export const resetPassword = os.auth.resetPassword.handler(
|
||||
.where("revoked_at", "is", null)
|
||||
.execute();
|
||||
|
||||
// Return void on success
|
||||
return { success: true };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -280,4 +280,6 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
|
||||
|
||||
// Send verification email (stubbed)
|
||||
await sendVerificationEmail(email, verificationToken);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -54,5 +54,7 @@ export const verifyEmail = os.auth.verifyEmail.handler(
|
||||
.deleteFrom("email_verifications")
|
||||
.where("id", "=", verification.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -47,4 +47,6 @@ export const meDelete = os.me.delete
|
||||
|
||||
// Clear session cookie
|
||||
deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -64,6 +64,8 @@ export const trustDevice = os.me.trustDevice
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Device not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -113,6 +115,8 @@ export const untrustDevice = os.me.untrustDevice
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Device not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -128,4 +132,6 @@ export const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices
|
||||
.set({ is_trusted: false })
|
||||
.where("user_id", "=", context.user.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -45,6 +45,8 @@ export const renamePasskey = os.me.passkeys.rename
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Passkey not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -92,4 +94,6 @@ export const deletePasskey = os.me.passkeys.delete
|
||||
throw new ORPCError("NOT_FOUND", { message: "Passkey not found" });
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -65,6 +65,8 @@ export const revokeSession = os.me.revokeSession
|
||||
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Session not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -83,4 +85,6 @@ export const revokeAllSessions = os.me.revokeAllSessions
|
||||
.where("id", "!=", context.session.id)
|
||||
.where("revoked_at", "is", null)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -58,4 +58,6 @@ export const setPassword = os.me.setPassword
|
||||
.set({ password_hash: newHash, updated_at: new Date() })
|
||||
.where("id", "=", context.user.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -36,4 +36,6 @@ export const updateProfile = os.me.updateProfile
|
||||
.where("id", "=", context.user.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -123,6 +123,8 @@ export const invitesCreate = os.orgs.invites.create
|
||||
// Send invitation email
|
||||
const inviterName = context.user.displayName ?? context.user.email;
|
||||
await sendOrgInviteEmail(email, token, org.displayName, inviterName, role);
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -149,6 +151,8 @@ export const invitesCancel = os.orgs.invites.cancel
|
||||
if (!result.numDeletedRows || result.numDeletedRows === 0n) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Invitation not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -219,4 +223,6 @@ export const invitesAccept = os.orgs.invites.accept
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -39,6 +39,8 @@ export const orgsUpdate = os.orgs.update
|
||||
.set(updates)
|
||||
.where("id", "=", org.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -57,6 +59,8 @@ export const orgsDelete = os.orgs.delete
|
||||
requireRole(membership, "owner");
|
||||
|
||||
await context.db.deleteFrom("orgs").where("id", "=", org.id).execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -92,4 +96,6 @@ export const orgsLeave = os.orgs.leave
|
||||
.where("user_id", "=", context.user.id)
|
||||
.execute();
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -95,6 +95,8 @@ export const membersUpdateRole = os.orgs.members.updateRole
|
||||
.where("id", "=", targetMember.id)
|
||||
.execute();
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -155,4 +157,6 @@ export const membersRemove = os.orgs.members.remove
|
||||
.where("id", "=", targetMember.id)
|
||||
.execute();
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -160,6 +160,8 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication
|
||||
.set({ completed_at: new Date() })
|
||||
.where("id", "=", String(context.loginRequestId))
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Me procedures
|
||||
@@ -245,6 +247,8 @@ const setupProfile = os.me.setupProfile
|
||||
})
|
||||
.where("id", "=", context.user.id)
|
||||
.execute();
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Me procedures imported from ./procedures/me/*
|
||||
|
||||
Reference in New Issue
Block a user