Files
publisher-dashboard/packages/api-contract/src/contract.ts
RevIQ 39863bd947 Add org invites section to dashboard with accept/decline flow
Backend:
- Add me.invites endpoints (list, get, accept, decline) to API contract
- Create invites procedures for fetching user's pending invites
- Only show invites if email matches and is verified
- Refactor me routes into me/_routes.ts for consistency

Frontend:
- Add pending invitations section to /dashboard page
- Create /account/org-invites/[inviteId] page for accept/decline
- Show invite details (org, role, inviter, dates)
- Redirect to org dashboard after accepting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 17:11:22 +08:00

292 lines
8.5 KiB
TypeScript

import type {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/types";
import { oc } from "@orpc/contract";
import * as z from "zod";
import {
adminAddSiteInputSchema,
adminCreateOrgInputSchema,
adminCreateUserInputSchema,
adminUpdateUserInputSchema,
} from "./schemas/admin.js";
import {
forgotPasswordInputSchema,
loginPasswordInputSchema,
loginRequestInputSchema,
loginRequestOutputSchema,
loginStatusOutputSchema,
resetPasswordInputSchema,
signupInputSchema,
verifyEmailInputSchema,
} from "./schemas/auth.js";
import {
emailSchema,
slugSchema,
successResponseSchema,
} from "./schemas/common.js";
import {
createInviteInputSchema,
createOrgInputSchema,
orgInviteOutputSchema,
orgMemberOutputSchema,
orgOutputSchema,
orgSiteOutputSchema,
updateMemberRoleInputSchema,
} from "./schemas/org.js";
import {
authStatusOutputSchema,
deviceOutputSchema,
passkeyOutputSchema,
sessionOutputSchema,
setPasswordInputSchema,
setupProfileInputSchema,
trustDeviceInputSchema,
updateProfileInputSchema,
userInviteOutputSchema,
userProfileSchema,
} from "./schemas/user.js";
/**
* oRPC API Contract for the Publisher Dashboard
* This defines all RPC procedure signatures served at /api/v1/rpc
*/
export const contract = oc.router({
auth: oc.router({
// Signup and verification
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(successResponseSchema),
loginPasswordConfirm: oc
.input(z.object({ token: z.string() }))
.output(successResponseSchema),
loginIfRequestIsCompleted: oc.output(loginStatusOutputSchema),
// Password reset
forgotPassword: oc
.input(forgotPasswordInputSchema)
.output(successResponseSchema),
resetPassword: oc
.input(resetPasswordInputSchema)
.output(successResponseSchema),
// Logout
logout: oc.output(successResponseSchema),
// WebAuthn procedures
webauthn: oc.router({
createRegistrationOptions: oc
.input(z.object({ email: emailSchema }))
.output(
z.object({
challengeId: z.number(),
options: z.custom<PublicKeyCredentialCreationOptionsJSON>(),
}),
),
verifyRegistration: oc
.input(
z.object({
challengeId: z.number(),
response: z.custom<RegistrationResponseJSON>(),
}),
)
.output(z.object({ passkeyId: z.number() })),
createAuthenticationOptions: oc.output(
z.object({
challengeId: z.number(),
options: z.custom<PublicKeyCredentialRequestOptionsJSON>(),
}),
),
verifyAuthentication: oc
.input(
z.object({
challengeId: z.number(),
response: z.custom<AuthenticationResponseJSON>(),
}),
)
.output(successResponseSchema),
}),
}),
me: oc.router({
// Profile
get: oc.output(userProfileSchema),
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(successResponseSchema),
// Passkeys
passkeys: oc.router({
list: oc.output(z.array(passkeyOutputSchema)),
rename: oc
.input(z.object({ passkeyId: z.number(), name: z.string() }))
.output(successResponseSchema),
delete: oc
.input(z.object({ passkeyId: z.number() }))
.output(successResponseSchema),
}),
// Org invites for the current user
invites: oc.router({
list: oc.output(z.array(userInviteOutputSchema)),
get: oc
.input(z.object({ inviteId: z.number() }))
.output(userInviteOutputSchema),
accept: oc
.input(z.object({ inviteId: z.number() }))
.output(successResponseSchema),
decline: oc
.input(z.object({ inviteId: z.number() }))
.output(successResponseSchema),
}),
// Sessions & devices
listSessions: oc.output(z.array(sessionOutputSchema)),
revokeSession: oc
.input(z.object({ sessionId: z.number() }))
.output(successResponseSchema),
revokeAllSessions: oc.output(successResponseSchema),
getDeviceInfo: oc.output(deviceOutputSchema),
trustDevice: oc.input(trustDeviceInputSchema).output(successResponseSchema),
listTrustedDevices: oc.output(z.array(deviceOutputSchema)),
untrustDevice: oc
.input(z.object({ deviceId: z.number() }))
.output(successResponseSchema),
revokeAllTrustedDevices: oc.output(successResponseSchema),
}),
orgs: oc.router({
// Org management
list: oc.output(z.array(orgOutputSchema)),
create: oc
.input(createOrgInputSchema)
.output(z.object({ slug: z.string() })),
get: oc.input(z.object({ slug: slugSchema })).output(orgOutputSchema),
update: oc
.input(
z.object({
slug: slugSchema,
displayName: z.string().optional(),
logoUrl: z.string().optional(),
}),
)
.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(successResponseSchema),
remove: oc
.input(z.object({ slug: slugSchema, userId: z.number() }))
.output(successResponseSchema),
}),
// Invites
invites: oc.router({
list: oc
.input(z.object({ slug: slugSchema }))
.output(z.array(orgInviteOutputSchema)),
create: oc.input(createInviteInputSchema).output(successResponseSchema),
cancel: oc
.input(z.object({ slug: slugSchema, inviteId: z.number() }))
.output(successResponseSchema),
accept: oc
.input(z.object({ token: z.string() }))
.output(successResponseSchema),
}),
// Sites
sites: oc.router({
list: oc
.input(z.object({ slug: slugSchema }))
.output(z.array(orgSiteOutputSchema)),
}),
}),
admin: oc.router({
// Admin org management
orgs: oc.router({
list: oc.output(z.array(orgOutputSchema)),
get: oc.input(z.object({ slug: slugSchema })).output(orgOutputSchema),
create: oc
.input(adminCreateOrgInputSchema)
.output(z.object({ slug: z.string() })),
update: oc
.input(
z.object({
slug: slugSchema,
displayName: z.string().optional(),
logoUrl: z.string().optional(),
}),
)
.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(successResponseSchema),
removeSite: oc
.input(z.object({ slug: slugSchema, domain: z.string() }))
.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(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(successResponseSchema),
}),
}),
});
export type APIContract = typeof contract;