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>
This commit is contained in:
RevIQ
2026-01-10 17:11:22 +08:00
parent 9f4c6ac0b9
commit 39863bd947
11 changed files with 779 additions and 181 deletions

View File

@@ -15,26 +15,7 @@ import {
loginRequestMiddleware,
os,
} from "./procedures/base.js";
import { meDelete } from "./procedures/me/delete.js";
import {
getDeviceInfo,
listTrustedDevices,
revokeAllTrustedDevices,
trustDevice,
untrustDevice,
} from "./procedures/me/devices.js";
import {
deletePasskey,
listPasskeys,
renamePasskey,
} from "./procedures/me/passkeys.js";
import {
listSessions,
revokeAllSessions,
revokeSession,
} from "./procedures/me/sessions.js";
import { setPassword } from "./procedures/me/set-password.js";
import { updateProfile } from "./procedures/me/update-profile.js";
import { meRoutes } from "./procedures/me/_routes.js";
import {
invitesAccept,
invitesCancel,
@@ -164,105 +145,6 @@ const verifyAuthentication = os.auth.webauthn.verifyAuthentication
return { success: true };
});
// Me procedures
const meGet = os.me.get.use(authMiddleware).handler(async ({ context }) => {
const user = await context.db
.selectFrom("users")
.select([
"id",
"email",
"display_name",
"full_name",
"phone_number",
"avatar_url",
"email_verified_at",
"is_superuser",
"password_hash",
])
.where("id", "=", context.user.id)
.executeTakeFirstOrThrow();
return {
id: user.id,
email: user.email,
displayName: user.display_name,
fullName: user.full_name,
phoneNumber: user.phone_number,
avatarUrl: user.avatar_url,
emailVerified: user.email_verified_at !== null,
needsSetup: user.display_name === null,
isSuperuser: user.is_superuser,
hasPassword: user.password_hash !== null,
};
});
const meAuthStatus = os.me.authStatus
.use(authMiddleware)
.handler(async ({ context }) => {
const user = await context.db
.selectFrom("users")
.select([
"id",
"email",
"display_name",
"full_name",
"phone_number",
"avatar_url",
"email_verified_at",
"is_superuser",
"password_hash",
])
.where("id", "=", context.user.id)
.executeTakeFirstOrThrow();
return {
user: {
id: user.id,
email: user.email,
displayName: user.display_name,
fullName: user.full_name,
phoneNumber: user.phone_number,
avatarUrl: user.avatar_url,
emailVerified: user.email_verified_at !== null,
needsSetup: user.display_name === null,
isSuperuser: user.is_superuser,
hasPassword: user.password_hash !== null,
},
auth: context.auth,
};
});
const setupProfile = os.me.setupProfile
.use(authMiddleware)
.handler(async ({ input, context }) => {
const { displayName, fullName, phoneNumber } = input;
await context.db
.updateTable("users")
.set({
display_name: displayName,
full_name: fullName ?? null,
phone_number: phoneNumber ?? null,
updated_at: new Date(),
})
.where("id", "=", context.user.id)
.execute();
return { success: true };
});
// Me procedures imported from ./procedures/me/*
// - updateProfile, setPassword, meDelete
// - listPasskeys, renamePasskey, deletePasskey
// - listSessions, revokeSession, revokeAllSessions
// - getDeviceInfo, trustDevice, listTrustedDevices, untrustDevice, revokeAllTrustedDevices
// Orgs procedures - imported from ./procedures/orgs/index.js
// - orgsList, orgsCreate, orgsGet, orgsUpdate, orgsDelete, orgsLeave
// - membersList, membersUpdateRole, membersRemove
// - invitesList, invitesCreate, invitesCancel, invitesAccept
// - sitesList
// Build the router
export const router = os.router({
auth: {
@@ -283,27 +165,7 @@ export const router = os.router({
verifyAuthentication,
},
},
me: {
get: meGet,
authStatus: meAuthStatus,
setupProfile,
updateProfile,
delete: meDelete,
setPassword,
passkeys: {
list: listPasskeys,
rename: renamePasskey,
delete: deletePasskey,
},
listSessions,
revokeSession,
revokeAllSessions,
getDeviceInfo,
trustDevice,
listTrustedDevices,
untrustDevice,
revokeAllTrustedDevices,
},
me: meRoutes,
orgs: {
list: orgsList,
create: orgsCreate,