diff --git a/.ast-grep/rules/no-void-output.yml b/.ast-grep/rules/no-void-output.yml new file mode 100644 index 0000000..7a44f86 --- /dev/null +++ b/.ast-grep/rules/no-void-output.yml @@ -0,0 +1,15 @@ +id: no-void-output +language: typescript +severity: error +message: Do not use z.void() for output - use successResponseSchema instead +note: | + Endpoints should return `{ success: true }` instead of void. + This makes the API more explicit and avoids issues with TypeScript + expecting void-returning Promises. + + Replace `.output(z.void())` with `.output(successResponseSchema)` and ensure + the handler returns `{ success: true }`. +rule: + pattern: $EXPR.output(z.void()) +files: + - packages/api-contract/**/*.ts diff --git a/apps/api-server/eslint.config.js b/apps/api-server/eslint.config.js index d452929..a5ef326 100644 --- a/apps/api-server/eslint.config.js +++ b/apps/api-server/eslint.config.js @@ -13,4 +13,12 @@ export default [ "@typescript-eslint/require-await": "off", }, }, + { + // Disable certain rules for test files that have issues with expect().rejects + files: ["**/__tests__/**/*.ts"], + rules: { + "@typescript-eslint/await-thenable": "off", + "@typescript-eslint/no-confusing-void-expression": "off", + }, + }, ]; diff --git a/apps/api-server/src/__tests__/e2e/me.test.ts b/apps/api-server/src/__tests__/e2e/me.test.ts index 95aedf5..c65f102 100644 --- a/apps/api-server/src/__tests__/e2e/me.test.ts +++ b/apps/api-server/src/__tests__/e2e/me.test.ts @@ -31,9 +31,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, @@ -76,7 +76,9 @@ function createAPIContext(options?: { cookies.push(`${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`); } if (options?.deviceFingerprint) { - cookies.push(`${COOKIE_NAMES.DEVICE_FINGERPRINT}=${options.deviceFingerprint}`); + cookies.push( + `${COOKIE_NAMES.DEVICE_FINGERPRINT}=${options.deviceFingerprint}`, + ); } if (cookies.length > 0) { reqHeaders.set("cookie", cookies.join("; ")); @@ -102,7 +104,7 @@ async function createSession( userId: number, options?: { ipAddress?: string; userAgent?: string }, ): Promise<{ token: string; sessionId: number }> { - 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); @@ -137,6 +139,9 @@ async function createUserAPIContext( return { context, token }; } +// Export to suppress unused warning - helper available for future tests +void createUserAPIContext; + /** * Create a device in the database and return the fingerprint */ @@ -151,7 +156,7 @@ async function createDevice( ): Promise<{ fingerprint: string; deviceId: number }> { const fingerprint = options?.fingerprint ?? - "test-fp-" + String(Date.now()) + String(Math.random()); + `test-fp-${String(Date.now())}${String(Math.random())}`; const result = await getDb() .insertInto("user_devices") @@ -176,8 +181,7 @@ async function createDevice( 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); @@ -633,6 +637,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" + await expect( call( router.me.setPassword, @@ -741,10 +746,10 @@ describe("me.sessions.list", () => { }); // Create multiple sessions - const { token: sessionToken1, sessionId: id1 } = await createSession( - user.id, - { ipAddress: "192.168.1.1", userAgent: "Chrome/1.0" }, - ); + const { token: sessionToken1 } = await createSession(user.id, { + ipAddress: "192.168.1.1", + userAgent: "Chrome/1.0", + }); await createSession(user.id, { ipAddress: "192.168.1.2", userAgent: "Firefox/1.0", @@ -838,7 +843,11 @@ describe("me.sessions.revoke", () => { const { sessionId: sessionId2 } = await createSession(user.id); const context = createAPIContext({ sessionToken: sessionToken1 }); - await call(router.me.sessions.revoke, { sessionId: sessionId2 }, { context }); + await call( + router.me.sessions.revoke, + { sessionId: sessionId2 }, + { context }, + ); // Verify session is revoked const session = await getDb() @@ -1265,9 +1274,9 @@ describe("me.devices.revokeAll", () => { email: "revokealldevices@example.com", }); - const { deviceId: id1 } = await createDevice(user.id, { isTrusted: true }); - const { deviceId: id2 } = await createDevice(user.id, { isTrusted: true }); - const { deviceId: id3 } = await createDevice(user.id, { isTrusted: false }); + await createDevice(user.id, { isTrusted: true }); + await createDevice(user.id, { isTrusted: true }); + await createDevice(user.id, { isTrusted: false }); const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); @@ -1282,7 +1291,7 @@ describe("me.devices.revokeAll", () => { .execute(); expect(devices).toHaveLength(3); - expect(devices.every((d) => d.is_trusted === false)).toBe(true); + expect(devices.every((d) => !d.is_trusted)).toBe(true); }); test("works when no devices exist", async () => { diff --git a/apps/api-server/src/__tests__/e2e/webauthn.test.ts b/apps/api-server/src/__tests__/e2e/webauthn.test.ts index 996a414..7deb061 100644 --- a/apps/api-server/src/__tests__/e2e/webauthn.test.ts +++ b/apps/api-server/src/__tests__/e2e/webauthn.test.ts @@ -10,7 +10,7 @@ import type { Database } from "@reviq/db-schema"; import type { Kysely } from "kysely"; import type { APIContext } from "../../context.js"; -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { call } from "@orpc/server"; import { VirtualAuthenticator } from "@reviq/virtual-authenticator"; import { router } from "../../router.js"; @@ -64,7 +64,7 @@ function createAPIContext(sessionToken?: string): APIContext { * Create a real session in the database and return the token */ async function createSession(userId: number): Promise { - 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); @@ -90,7 +90,7 @@ async function createLoginRequest( userId: number, email: string, ): Promise<{ id: number; token: string }> { - const token = "test-login-" + String(Date.now()) + String(Math.random()); + const token = `test-login-${String(Date.now())}${String(Math.random())}`; const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes const result = await getDb() @@ -104,7 +104,7 @@ async function createLoginRequest( .returning("id") .executeTakeFirstOrThrow(); - return { id: result.id, token }; + return { id: Number(result.id), token }; } /** diff --git a/apps/api-server/src/__tests__/helpers/test-db.ts b/apps/api-server/src/__tests__/helpers/test-db.ts index 77bb019..0c21325 100644 --- a/apps/api-server/src/__tests__/helpers/test-db.ts +++ b/apps/api-server/src/__tests__/helpers/test-db.ts @@ -3,9 +3,10 @@ */ import type { Database } from "@reviq/db-schema"; +import type { Kysely } from "kysely"; +import { existsSync } from "node:fs"; import { join } from "node:path"; import { createDb } from "@reviq/db"; -import type { Kysely } from "kysely"; import { sql } from "kysely"; import pg from "pg"; @@ -135,7 +136,6 @@ async function ensureTestDatabaseExists(): Promise { * @throws Error if repo root cannot be found */ function findRepoRoot(): string { - const { existsSync } = require("node:fs"); let current = import.meta.dir; // Walk up to 10 levels to find the repo root diff --git a/apps/api-server/src/procedures/admin/auth/complete-login.ts b/apps/api-server/src/procedures/admin/auth/complete-login.ts index 74e49d7..bb25325 100644 --- a/apps/api-server/src/procedures/admin/auth/complete-login.ts +++ b/apps/api-server/src/procedures/admin/auth/complete-login.ts @@ -46,4 +46,6 @@ export const adminAuthCompleteLogin = os.admin.auth.completeLogin .set({ completed_at: new Date() }) .where("id", "=", anyRequest.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/delete.ts b/apps/api-server/src/procedures/admin/orgs/delete.ts index 8b1e609..1cfd440 100644 --- a/apps/api-server/src/procedures/admin/orgs/delete.ts +++ b/apps/api-server/src/procedures/admin/orgs/delete.ts @@ -33,4 +33,6 @@ export const adminOrgsDelete = os.admin.orgs.delete .execute(); await trx.deleteFrom("orgs").where("id", "=", org.id).execute(); }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/sites.ts b/apps/api-server/src/procedures/admin/orgs/sites.ts index 250ebe6..aa0fc7b 100644 --- a/apps/api-server/src/procedures/admin/orgs/sites.ts +++ b/apps/api-server/src/procedures/admin/orgs/sites.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/admin/orgs/update.ts b/apps/api-server/src/procedures/admin/orgs/update.ts index d2b2bb1..27f4aa3 100644 --- a/apps/api-server/src/procedures/admin/orgs/update.ts +++ b/apps/api-server/src/procedures/admin/orgs/update.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/admin/users/confirm-email.ts b/apps/api-server/src/procedures/admin/users/confirm-email.ts index a2fa6f1..782306f 100644 --- a/apps/api-server/src/procedures/admin/users/confirm-email.ts +++ b/apps/api-server/src/procedures/admin/users/confirm-email.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts index 1e431a4..a23029e 100644 --- a/apps/api-server/src/procedures/admin/users/create.ts +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -60,4 +60,6 @@ export const adminUsersCreate = os.admin.users.create .execute(); } }); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts index ddfc353..6e8f3e6 100644 --- a/apps/api-server/src/procedures/admin/users/update.ts +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/auth/forgot-password.ts b/apps/api-server/src/procedures/auth/forgot-password.ts index 55a676b..40a57c6 100644 --- a/apps/api-server/src/procedures/auth/forgot-password.ts +++ b/apps/api-server/src/procedures/auth/forgot-password.ts @@ -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 }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password-confirm.ts b/apps/api-server/src/procedures/auth/login-password-confirm.ts index 64f6262..655f4ae 100644 --- a/apps/api-server/src/procedures/auth/login-password-confirm.ts +++ b/apps/api-server/src/procedures/auth/login-password-confirm.ts @@ -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 }; }, ); diff --git a/apps/api-server/src/procedures/auth/login-password.ts b/apps/api-server/src/procedures/auth/login-password.ts index 456fd97..6db8663 100644 --- a/apps/api-server/src/procedures/auth/login-password.ts +++ b/apps/api-server/src/procedures/auth/login-password.ts @@ -111,6 +111,6 @@ export const loginPassword = os.auth.loginPassword.handler( await sendLoginConfirmationEmail(result.email, result.token); } - // Return void (success) + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/logout.ts b/apps/api-server/src/procedures/auth/logout.ts index 1fa5762..c964d7c 100644 --- a/apps/api-server/src/procedures/auth/logout.ts +++ b/apps/api-server/src/procedures/auth/logout.ts @@ -23,4 +23,6 @@ export const logout = os.auth.logout // Clear the session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/auth/resend-verification.ts b/apps/api-server/src/procedures/auth/resend-verification.ts index 5f2eff7..66bade8 100644 --- a/apps/api-server/src/procedures/auth/resend-verification.ts +++ b/apps/api-server/src/procedures/auth/resend-verification.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/auth/reset-password.ts b/apps/api-server/src/procedures/auth/reset-password.ts index af51079..f98baf9 100644 --- a/apps/api-server/src/procedures/auth/reset-password.ts +++ b/apps/api-server/src/procedures/auth/reset-password.ts @@ -84,6 +84,6 @@ export const resetPassword = os.auth.resetPassword.handler( .where("revoked_at", "is", null) .execute(); - // Return void on success + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/auth/signup.ts b/apps/api-server/src/procedures/auth/signup.ts index 33338d5..3015c8f 100644 --- a/apps/api-server/src/procedures/auth/signup.ts +++ b/apps/api-server/src/procedures/auth/signup.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/auth/verify-email.ts b/apps/api-server/src/procedures/auth/verify-email.ts index bdc4139..a39c206 100644 --- a/apps/api-server/src/procedures/auth/verify-email.ts +++ b/apps/api-server/src/procedures/auth/verify-email.ts @@ -54,5 +54,7 @@ export const verifyEmail = os.auth.verifyEmail.handler( .deleteFrom("email_verifications") .where("id", "=", verification.id) .execute(); + + return { success: true }; }, ); diff --git a/apps/api-server/src/procedures/me/delete.ts b/apps/api-server/src/procedures/me/delete.ts index a58f0c4..e5cf875 100644 --- a/apps/api-server/src/procedures/me/delete.ts +++ b/apps/api-server/src/procedures/me/delete.ts @@ -47,4 +47,6 @@ export const meDelete = os.me.delete // Clear session cookie deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/devices.ts b/apps/api-server/src/procedures/me/devices.ts index 2941480..8edaf9e 100644 --- a/apps/api-server/src/procedures/me/devices.ts +++ b/apps/api-server/src/procedures/me/devices.ts @@ -13,7 +13,7 @@ import { defaultDeviceName, requireDeviceFingerprint } from "./helpers.js"; * @throws BAD_REQUEST if no device fingerprint found * @throws NOT_FOUND if device doesn't exist */ -export const getDeviceInfo = os.me.getDeviceInfo +export const getDeviceInfo = os.me.devices.getInfo .use(authMiddleware) .handler(async ({ context }) => { const fingerprint = requireDeviceFingerprint(context.reqHeaders); @@ -48,7 +48,7 @@ export const getDeviceInfo = os.me.getDeviceInfo * @throws BAD_REQUEST if no device fingerprint found * @throws NOT_FOUND if device doesn't exist */ -export const trustDevice = os.me.trustDevice +export const trustDevice = os.me.devices.trust .use(authMiddleware) .handler(async ({ input, context }) => { const { name } = input; @@ -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 }; }); /** @@ -71,7 +73,7 @@ export const trustDevice = os.me.trustDevice * - Requires authentication * - Returns all trusted devices for the current user */ -export const listTrustedDevices = os.me.listTrustedDevices +export const listTrustedDevices = os.me.devices.listTrusted .use(authMiddleware) .handler(async ({ context }) => { const devices = await context.db @@ -100,7 +102,7 @@ export const listTrustedDevices = os.me.listTrustedDevices * - Marks device as untrusted by ID * @throws NOT_FOUND if device doesn't exist */ -export const untrustDevice = os.me.untrustDevice +export const untrustDevice = os.me.devices.untrust .use(authMiddleware) .handler(async ({ input, context }) => { const result = await context.db @@ -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 }; }); /** @@ -120,7 +124,7 @@ export const untrustDevice = os.me.untrustDevice * - Requires authentication * - Marks all devices as untrusted */ -export const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices +export const revokeAllTrustedDevices = os.me.devices.revokeAll .use(authMiddleware) .handler(async ({ context }) => { await context.db @@ -128,4 +132,6 @@ export const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices .set({ is_trusted: false }) .where("user_id", "=", context.user.id) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/passkeys.ts b/apps/api-server/src/procedures/me/passkeys.ts index e6ea66d..b093580 100644 --- a/apps/api-server/src/procedures/me/passkeys.ts +++ b/apps/api-server/src/procedures/me/passkeys.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/me/sessions.ts b/apps/api-server/src/procedures/me/sessions.ts index f8d1fdd..4de90e8 100644 --- a/apps/api-server/src/procedures/me/sessions.ts +++ b/apps/api-server/src/procedures/me/sessions.ts @@ -11,7 +11,7 @@ import { authMiddleware, os } from "../base.js"; * - Returns all sessions for the current user * - Includes isCurrent flag to identify active session */ -export const listSessions = os.me.listSessions +export const listSessions = os.me.sessions.list .use(authMiddleware) .handler(async ({ context }) => { const sessions = await context.db @@ -42,7 +42,7 @@ export const listSessions = os.me.listSessions * @throws NOT_FOUND if session doesn't exist * @throws BAD_REQUEST if trying to revoke current session */ -export const revokeSession = os.me.revokeSession +export const revokeSession = os.me.sessions.revoke .use(authMiddleware) .handler(async ({ input, context }) => { const { sessionId } = input; @@ -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 }; }); /** @@ -72,7 +74,7 @@ export const revokeSession = os.me.revokeSession * - Requires authentication * - Revokes all sessions except current */ -export const revokeAllSessions = os.me.revokeAllSessions +export const revokeAllSessions = os.me.sessions.revokeAll .use(authMiddleware) .handler(async ({ context }) => { // Revoke all sessions except current @@ -83,4 +85,6 @@ export const revokeAllSessions = os.me.revokeAllSessions .where("id", "!=", context.session.id) .where("revoked_at", "is", null) .execute(); + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/me/set-password.ts b/apps/api-server/src/procedures/me/set-password.ts index ba4663d..beacc87 100644 --- a/apps/api-server/src/procedures/me/set-password.ts +++ b/apps/api-server/src/procedures/me/set-password.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/me/update-profile.ts b/apps/api-server/src/procedures/me/update-profile.ts index 3d59510..b23c090 100644 --- a/apps/api-server/src/procedures/me/update-profile.ts +++ b/apps/api-server/src/procedures/me/update-profile.ts @@ -36,4 +36,6 @@ export const updateProfile = os.me.updateProfile .where("id", "=", context.user.id) .execute(); } + + return { success: true }; }); diff --git a/apps/api-server/src/procedures/orgs/invites.ts b/apps/api-server/src/procedures/orgs/invites.ts index 4089ad2..842c90f 100644 --- a/apps/api-server/src/procedures/orgs/invites.ts +++ b/apps/api-server/src/procedures/orgs/invites.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/orgs/management.ts b/apps/api-server/src/procedures/orgs/management.ts index 57ff4cf..00a72ae 100644 --- a/apps/api-server/src/procedures/orgs/management.ts +++ b/apps/api-server/src/procedures/orgs/management.ts @@ -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 }; }); diff --git a/apps/api-server/src/procedures/orgs/members.ts b/apps/api-server/src/procedures/orgs/members.ts index 1a39aba..8bcc681 100644 --- a/apps/api-server/src/procedures/orgs/members.ts +++ b/apps/api-server/src/procedures/orgs/members.ts @@ -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 }; }); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index eca1c48..1a7a140 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -153,6 +153,15 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication message: "Authentication failed", }); } + + // Mark the login request as completed - passkey verification is equivalent to email verification + await context.db + .updateTable("login_requests") + .set({ completed_at: new Date() }) + .where("id", "=", String(context.loginRequestId)) + .execute(); + + return { success: true }; }); // Me procedures @@ -238,6 +247,8 @@ const setupProfile = os.me.setupProfile }) .where("id", "=", context.user.id) .execute(); + + return { success: true }; }); // Me procedures imported from ./procedures/me/* diff --git a/apps/publisher-dashboard/package.json b/apps/publisher-dashboard/package.json index c52025c..84796ab 100644 --- a/apps/publisher-dashboard/package.json +++ b/apps/publisher-dashboard/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@internationalized/date": "^3.10.1", - "@lucide/svelte": "^0.562.0", + "@lucide/svelte": "^0.561.0", "@macalinao/eslint-config": "catalog:", "@macalinao/tsconfig": "catalog:", "@sveltejs/adapter-static": "^3.0.8", diff --git a/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte b/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte index 349d6a1..051aef0 100644 --- a/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte +++ b/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte @@ -1,8 +1,9 @@ diff --git a/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte b/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte index c0c0afa..a192c94 100644 --- a/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte +++ b/apps/publisher-dashboard/src/lib/components/layout/mobile-nav.svelte @@ -1,5 +1,9 @@ @@ -86,35 +150,10 @@ function handleNavClick() { /> - {/if} - {item.label} - - {/each} - - - - -
- {#each bottomItems as item} - {@const isActive = $page.url.pathname === item.href} - - {#if item.icon === "settings"} + {:else if item.icon === "building"} - - + + {/if} {item.label} @@ -126,14 +165,47 @@ function handleNavClick() { diff --git a/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte b/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte new file mode 100644 index 0000000..c3b2afb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/layout/org-switcher.svelte @@ -0,0 +1,89 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Organizations + + {#if orgsQuery.isPending} + Loading... + {:else if orgs.length === 0} + No organizations + {:else} + {#each orgs as org} + {@const isActive = currentSlug === org.slug} + handleOrgSelect(org.slug)} + class={cn(isActive && "bg-accent")} + > +
+ {#if org.logoUrl} + + {:else} +
+ {org.displayName.charAt(0).toUpperCase()} +
+ {/if} + {org.displayName} + {#if isActive} + + + + {/if} +
+
+ {/each} + {/if} + + goto("/dashboard/new")}> +
+ + + + + Create New Organization +
+
+
+
diff --git a/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte b/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte new file mode 100644 index 0000000..f5d62bd --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/layout/user-menu.svelte @@ -0,0 +1,112 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + +
+ {#if user?.avatarUrl} + + {:else} +
+ {initials} +
+ {/if} +
+ {user?.displayName ?? user?.email ?? "Loading..."} + {#if currentUserRole} + {currentUserRole} + {:else if user?.email && user?.displayName} + {user.email} + {/if} +
+
+ + goto("/account")}> + + + + + Account Settings + + + + + + + + + Sign out + +
+
diff --git a/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-fallback.svelte b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..16bb678 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-image.svelte b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..ab2969a --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar.svelte b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..75bf628 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/avatar/index.ts b/apps/publisher-dashboard/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..a8ad6d7 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Fallback from "./avatar-fallback.svelte"; +import Image from "./avatar-image.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/checkbox/checkbox.svelte b/apps/publisher-dashboard/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..14dc90b --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/checkbox/index.ts b/apps/publisher-dashboard/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..d1b2485 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..5fe30f7 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..49be469 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,43 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..ef29949 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..a84920a --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..9b8c8e1 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..e1278b4 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..9ab03be --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..79e897c --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..f1dced8 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..d4f7ef3 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,33 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..f7b3d5f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..a27ab29 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..67f6303 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..cc5bc78 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..48d664b --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..51774fa --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..f1b7449 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..41ef3f9 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts new file mode 100644 index 0000000..2de5dd2 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/index.ts @@ -0,0 +1,7 @@ +import Root from "./phone-number-input.svelte"; + +export { + Root, + // + Root as PhoneNumberInput, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte new file mode 100644 index 0000000..661d7f9 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/phone-number-input/phone-number-input.svelte @@ -0,0 +1,111 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts b/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..2c3e67d --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/index.ts @@ -0,0 +1,19 @@ +import Root from "./popover.svelte"; +import Close from "./popover-close.svelte"; +import Content from "./popover-content.svelte"; +import Portal from "./popover-portal.svelte"; +import Trigger from "./popover-trigger.svelte"; + +export { + Root, + Content, + Trigger, + Close, + Portal, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose, + Portal as PopoverPortal, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 0000000..77cef23 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..c42998f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 0000000..d76241e --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..567deeb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte b/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 0000000..018f415 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/index.ts b/apps/publisher-dashboard/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..99160bb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Root from "./select.svelte"; +import Content from "./select-content.svelte"; +import Group from "./select-group.svelte"; +import GroupHeading from "./select-group-heading.svelte"; +import Item from "./select-item.svelte"; +import Label from "./select-label.svelte"; +import Portal from "./select-portal.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import Separator from "./select-separator.svelte"; +import Trigger from "./select-trigger.svelte"; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..d07be69 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,45 @@ + + + + + + + {@render children?.()} + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-group-heading.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..91f4085 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-group.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..1a12fae --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-item.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..992d361 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-label.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..13d913f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 0000000..099e8cb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..0d8719a --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..c367d14 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-separator.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..dc66875 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..b92b3af --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/select/select.svelte b/apps/publisher-dashboard/src/lib/components/ui/select/select.svelte new file mode 100644 index 0000000..c9bb6d3 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/select/select.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/skeleton/index.ts b/apps/publisher-dashboard/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..cb26b2c --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/skeleton/skeleton.svelte b/apps/publisher-dashboard/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..50afe9c --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/switch/index.ts b/apps/publisher-dashboard/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..99620eb --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/switch/switch.svelte b/apps/publisher-dashboard/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..37300f2 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/textarea/index.ts b/apps/publisher-dashboard/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..c14b903 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from "./textarea.svelte"; + +export { + Root, + // + Root as Textarea, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/textarea/textarea.svelte b/apps/publisher-dashboard/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..dec8688 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts b/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..a7865f5 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Content from "./tooltip-content.svelte"; +import Portal from "./tooltip-portal.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Trigger from "./tooltip-trigger.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..118ccac --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
+ {/snippet} +
+
+
diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..9b32bbf --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..7bea75e --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..57370f8 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..90e83e6 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/publisher-dashboard/src/routes/+page.svelte b/apps/publisher-dashboard/src/routes/+page.svelte index 06c4862..d33f9bc 100644 --- a/apps/publisher-dashboard/src/routes/+page.svelte +++ b/apps/publisher-dashboard/src/routes/+page.svelte @@ -1,20 +1,38 @@ - Publisher Dashboard + Publisher Dashboard -
-

Publisher Dashboard

-

Welcome to the Publisher Dashboard

- - +
+
diff --git a/apps/publisher-dashboard/src/routes/account/+page.svelte b/apps/publisher-dashboard/src/routes/account/+page.svelte index bf721f2..bd46a13 100644 --- a/apps/publisher-dashboard/src/routes/account/+page.svelte +++ b/apps/publisher-dashboard/src/routes/account/+page.svelte @@ -17,6 +17,7 @@ import { import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; import { LoadingButton } from "$lib/components/ui/loading-button"; +import { PhoneNumberInput } from "$lib/components/ui/phone-number-input"; import { Separator } from "$lib/components/ui/separator"; import { cn } from "$lib/utils"; import { validatePhone } from "$lib/utils/validation"; @@ -224,9 +225,8 @@ function getInitials(name: string | null | undefined): string {
- ({ queryKey: ["trustedDevices"], - queryFn: () => api.me.listTrustedDevices(), + queryFn: () => api.me.devices.listTrusted(), })); const currentDeviceQuery = createQuery(() => ({ queryKey: ["deviceInfo"], - queryFn: () => api.me.getDeviceInfo(), + queryFn: () => api.me.devices.getInfo(), })); // Get current device fingerprint from comparison @@ -106,7 +106,7 @@ async function handleRemoveTrust() { isRemoving = true; try { - await api.me.untrustDevice({ deviceId: selectedDeviceId }); + await api.me.devices.untrust({ deviceId: selectedDeviceId }); await queryClient.invalidateQueries({ queryKey: ["trustedDevices"] }); toast.success("Device trust removed"); confirmDialogOpen = false; @@ -125,7 +125,7 @@ async function handleRemoveAllTrust() { isRemovingAll = true; try { - await api.me.revokeAllTrustedDevices(); + await api.me.devices.revokeAll(); await queryClient.invalidateQueries({ queryKey: ["trustedDevices"] }); toast.success("All trusted devices removed"); confirmAllDialogOpen = false; diff --git a/apps/publisher-dashboard/src/routes/account/sessions/+page.svelte b/apps/publisher-dashboard/src/routes/account/sessions/+page.svelte index f714443..05a5f6b 100644 --- a/apps/publisher-dashboard/src/routes/account/sessions/+page.svelte +++ b/apps/publisher-dashboard/src/routes/account/sessions/+page.svelte @@ -30,7 +30,7 @@ const queryClient = useQueryClient(); const sessionsQuery = createQuery(() => ({ queryKey: ["sessions"], - queryFn: () => api.me.listSessions(), + queryFn: () => api.me.sessions.list(), })); let confirmDialogOpen = $state(false); @@ -121,7 +121,7 @@ async function handleRevoke() { isRevoking = true; try { - await api.me.revokeSession({ sessionId: selectedSessionId }); + await api.me.sessions.revoke({ sessionId: selectedSessionId }); await queryClient.invalidateQueries({ queryKey: ["sessions"] }); toast.success("Session revoked"); confirmDialogOpen = false; @@ -140,7 +140,7 @@ async function handleRevokeAll() { isRevokingAll = true; try { - await api.me.revokeAllSessions(); + await api.me.sessions.revokeAll(); await queryClient.invalidateQueries({ queryKey: ["sessions"] }); toast.success("All other sessions revoked"); confirmAllDialogOpen = false; diff --git a/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte index 916ca58..f98027f 100644 --- a/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte +++ b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte @@ -1,12 +1,5 @@ + + + Reports - Publisher Dashboard + + + +
+
+
+ + + + +
+

Coming Soon

+

+ Advanced reporting features are currently in development. +

+
+
+
diff --git a/apps/publisher-dashboard/src/routes/performance/+layout.ts b/apps/publisher-dashboard/src/routes/performance/+layout.ts deleted file mode 100644 index 89da957..0000000 --- a/apps/publisher-dashboard/src/routes/performance/+layout.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const ssr = false; -export const prerender = true; diff --git a/apps/publisher-dashboard/src/routes/settings/+page.svelte b/apps/publisher-dashboard/src/routes/settings/+page.svelte deleted file mode 100644 index 20d3d2a..0000000 --- a/apps/publisher-dashboard/src/routes/settings/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - Settings - Publisher Dashboard - - -
-

Settings

-

Configure your publisher settings here.

- - -
diff --git a/bun.lock b/bun.lock index 392b2e3..b063665 100644 --- a/bun.lock +++ b/bun.lock @@ -91,7 +91,7 @@ }, "devDependencies": { "@internationalized/date": "^3.10.1", - "@lucide/svelte": "^0.562.0", + "@lucide/svelte": "^0.561.0", "@macalinao/eslint-config": "catalog:", "@macalinao/tsconfig": "catalog:", "@sveltejs/adapter-static": "^3.0.8", @@ -334,7 +334,7 @@ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], - "@lucide/svelte": ["@lucide/svelte@0.562.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-wDMULwtTFN2Sc/TFBm6gfuVCNb4Y5P9LDrwxNnUbV52+IEU7NXZmvxwXoz+vrrpad6Xupq+Hw5eUlqIHEGhouw=="], + "@lucide/svelte": ["@lucide/svelte@0.561.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A=="], "@macalinao/biome-config": ["@macalinao/biome-config@0.1.7", "", { "peerDependencies": { "@biomejs/biome": "^2.3.10" } }, "sha512-JijaB/REJr6D3fGV36d1XGsf2WFofgnMS1WbOYcNJCQpic2XmFALV7GNL28z7rDCN3/DeSovPuW/1yImce7kPA=="], diff --git a/db/schema.sql b/db/schema.sql index fcda6b6..f33aec6 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,4 +1,4 @@ -\restrict JcXyipc16dugUGJvd2oDJgA4cUi3A29rdzMF11XH6GPR94bG05YyvDzwhuyfGSd +\restrict CIj4ub2A9kD8NQM2nKa1cg31hNutT3jXdOch0DnJ2bT48qpQKbe9XxNtViPwfYR -- Dumped from database version 17.7 -- Dumped by pg_dump version 17.7 @@ -1084,7 +1084,7 @@ ALTER TABLE ONLY public.user_devices -- PostgreSQL database dump complete -- -\unrestrict JcXyipc16dugUGJvd2oDJgA4cUi3A29rdzMF11XH6GPR94bG05YyvDzwhuyfGSd +\unrestrict CIj4ub2A9kD8NQM2nKa1cg31hNutT3jXdOch0DnJ2bT48qpQKbe9XxNtViPwfYR -- diff --git a/package.json b/package.json index c2f32e2..716ea30 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "dev": "turbo dev", "build": "turbo build", "build:watch:packages": "turbo watch build --filter=./packages/*", + "build:packages": "turbo build --filter=./packages/*", "lint": "biome check && turbo run lint", "lint:fix": "biome check --write --unsafe && turbo run lint -- --fix", "typecheck": "turbo typecheck", "clean": "turbo clean", + "test": "turbo test", "db:codegen": "bun run --cwd packages/db-schema generate" }, "devDependencies": { diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index b2e477f..5229bae 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -22,7 +22,11 @@ import { signupInputSchema, verifyEmailInputSchema, } from "./schemas/auth.js"; -import { emailSchema, slugSchema } from "./schemas/common.js"; +import { + emailSchema, + slugSchema, + successResponseSchema, +} from "./schemas/common.js"; import { createInviteInputSchema, createOrgInputSchema, @@ -51,26 +55,32 @@ import { export const contract = oc.router({ auth: oc.router({ // Signup and verification - signup: oc.input(signupInputSchema).output(z.void()), - verifyEmail: oc.input(verifyEmailInputSchema).output(z.void()), - resendVerificationEmail: oc.output(z.void()), + signup: oc.input(signupInputSchema).output(successResponseSchema), + verifyEmail: oc.input(verifyEmailInputSchema).output(successResponseSchema), + resendVerificationEmail: oc.output(successResponseSchema), // Login flow createLoginRequest: oc .input(loginRequestInputSchema) .output(loginRequestOutputSchema), - loginPassword: oc.input(loginPasswordInputSchema).output(z.void()), + loginPassword: oc + .input(loginPasswordInputSchema) + .output(successResponseSchema), loginPasswordConfirm: oc .input(z.object({ token: z.string() })) - .output(z.void()), + .output(successResponseSchema), loginIfRequestIsCompleted: oc.output(loginStatusOutputSchema), // Password reset - forgotPassword: oc.input(forgotPasswordInputSchema).output(z.void()), - resetPassword: oc.input(resetPasswordInputSchema).output(z.void()), + forgotPassword: oc + .input(forgotPasswordInputSchema) + .output(successResponseSchema), + resetPassword: oc + .input(resetPasswordInputSchema) + .output(successResponseSchema), // Logout - logout: oc.output(z.void()), + logout: oc.output(successResponseSchema), // WebAuthn procedures webauthn: oc.router({ @@ -103,45 +113,59 @@ export const contract = oc.router({ response: z.custom(), }), ) - .output(z.void()), + .output(successResponseSchema), }), }), me: oc.router({ // Profile get: oc.output(userProfileSchema), - setupProfile: oc.input(setupProfileInputSchema).output(z.void()), - updateProfile: oc.input(updateProfileInputSchema).output(z.void()), - delete: oc.input(z.object({ password: z.string() })).output(z.void()), + setupProfile: oc + .input(setupProfileInputSchema) + .output(successResponseSchema), + updateProfile: oc + .input(updateProfileInputSchema) + .output(successResponseSchema), + delete: oc + .input(z.object({ password: z.string() })) + .output(successResponseSchema), // Auth status (for CLI and debugging) authStatus: oc.output(authStatusOutputSchema), // Authentication settings - setPassword: oc.input(setPasswordInputSchema).output(z.void()), + setPassword: oc.input(setPasswordInputSchema).output(successResponseSchema), // Passkeys passkeys: oc.router({ list: oc.output(z.array(passkeyOutputSchema)), rename: oc .input(z.object({ passkeyId: z.number(), name: z.string() })) - .output(z.void()), - delete: oc.input(z.object({ passkeyId: z.number() })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ passkeyId: z.number() })) + .output(successResponseSchema), }), - // Sessions & devices - listSessions: oc.output(z.array(sessionOutputSchema)), - revokeSession: oc - .input(z.object({ sessionId: z.number() })) - .output(z.void()), - revokeAllSessions: oc.output(z.void()), - getDeviceInfo: oc.output(deviceOutputSchema), - trustDevice: oc.input(trustDeviceInputSchema).output(z.void()), - listTrustedDevices: oc.output(z.array(deviceOutputSchema)), - untrustDevice: oc - .input(z.object({ deviceId: z.number() })) - .output(z.void()), - revokeAllTrustedDevices: oc.output(z.void()), + // Sessions + sessions: oc.router({ + list: oc.output(z.array(sessionOutputSchema)), + revoke: oc + .input(z.object({ sessionId: z.number() })) + .output(successResponseSchema), + revokeAll: oc.output(successResponseSchema), + }), + + // Devices + devices: oc.router({ + getInfo: oc.output(deviceOutputSchema), + trust: oc.input(trustDeviceInputSchema).output(successResponseSchema), + listTrusted: oc.output(z.array(deviceOutputSchema)), + untrust: oc + .input(z.object({ deviceId: z.number() })) + .output(successResponseSchema), + revokeAll: oc.output(successResponseSchema), + }), }), orgs: oc.router({ @@ -159,19 +183,25 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), - leave: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), + leave: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), // Members members: oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgMemberOutputSchema)), - updateRole: oc.input(updateMemberRoleInputSchema).output(z.void()), + updateRole: oc + .input(updateMemberRoleInputSchema) + .output(successResponseSchema), remove: oc .input(z.object({ slug: slugSchema, userId: z.number() })) - .output(z.void()), + .output(successResponseSchema), }), // Invites @@ -179,11 +209,13 @@ export const contract = oc.router({ list: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgInviteOutputSchema)), - create: oc.input(createInviteInputSchema).output(z.void()), + create: oc.input(createInviteInputSchema).output(successResponseSchema), cancel: oc .input(z.object({ slug: slugSchema, inviteId: z.number() })) - .output(z.void()), - accept: oc.input(z.object({ token: z.string() })).output(z.void()), + .output(successResponseSchema), + accept: oc + .input(z.object({ token: z.string() })) + .output(successResponseSchema), }), // Sites @@ -210,31 +242,39 @@ export const contract = oc.router({ logoUrl: z.string().optional(), }), ) - .output(z.void()), - delete: oc.input(z.object({ slug: slugSchema })).output(z.void()), + .output(successResponseSchema), + delete: oc + .input(z.object({ slug: slugSchema })) + .output(successResponseSchema), listSites: oc .input(z.object({ slug: slugSchema })) .output(z.array(orgSiteOutputSchema)), - addSite: oc.input(adminAddSiteInputSchema).output(z.void()), + addSite: oc.input(adminAddSiteInputSchema).output(successResponseSchema), removeSite: oc .input(z.object({ slug: slugSchema, domain: z.string() })) - .output(z.void()), + .output(successResponseSchema), }), // Admin user management users: oc.router({ list: oc.output(z.array(userProfileSchema)), get: oc.input(z.object({ email: emailSchema })).output(userProfileSchema), - create: oc.input(adminCreateUserInputSchema).output(z.void()), - update: oc.input(adminUpdateUserInputSchema).output(z.void()), - confirmEmail: oc.input(z.object({ email: emailSchema })).output(z.void()), + create: oc + .input(adminCreateUserInputSchema) + .output(successResponseSchema), + update: oc + .input(adminUpdateUserInputSchema) + .output(successResponseSchema), + confirmEmail: oc + .input(z.object({ email: emailSchema })) + .output(successResponseSchema), }), // Admin auth management auth: oc.router({ completeLogin: oc .input(z.object({ email: emailSchema })) - .output(z.void()), + .output(successResponseSchema), }), }), }); diff --git a/packages/api-contract/src/schemas/common.ts b/packages/api-contract/src/schemas/common.ts index a4cd986..782c97c 100644 --- a/packages/api-contract/src/schemas/common.ts +++ b/packages/api-contract/src/schemas/common.ts @@ -58,3 +58,9 @@ export const phoneSchema = z .refine((val) => !val || isValidPhoneNumber(val), { message: "Invalid phone number", }); + +/** + * Success response schema for operations that don't return data + * Use instead of void to make responses more explicit + */ +export const successResponseSchema = z.object({ success: z.literal(true) }); diff --git a/turbo.json b/turbo.json index 9e0a980..4e37ec2 100644 --- a/turbo.json +++ b/turbo.json @@ -29,6 +29,11 @@ "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "src/**/*.test.ts"], "cache": false + }, + "test": { + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "src/**/*.test.ts"], + "cache": false } } }