feat: add real account workspace controls

This commit is contained in:
2026-07-02 19:52:29 -07:00
parent 1b8fae0116
commit 1cdf89c608
12 changed files with 759 additions and 203 deletions

View File

@@ -17,7 +17,16 @@ import {
sessions,
} from "@/modules/core/server/schema";
import { getSystemSettings } from "@/modules/core/server/system-settings";
import type { Account, AuthContext, Membership, Organization, Permission, PublicAccount, Session } from "@/modules/core/types";
import type {
Account,
AccountOrganizationOption,
AuthContext,
Membership,
Organization,
Permission,
PublicAccount,
Session,
} from "@/modules/core/types";
const SESSION_COOKIE = "teatea_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
@@ -80,6 +89,23 @@ function toMembership(row: MembershipRow, role: RoleRow): Membership {
};
}
function toOrganizationOption(
organization: OrganizationRow,
roleLabel: string,
activeOrganizationId: string | null,
): AccountOrganizationOption {
return {
id: organization.id,
name: organization.name,
slug: organization.slug,
status: organization.status,
registrationEnabled: organization.registrationEnabled,
oidcEnabled: organization.oidcEnabled,
roleLabel,
isActive: organization.id === activeOrganizationId,
};
}
export function toPublicAccount(account: AccountRow | Account, roleKey?: string, organization?: Organization): PublicAccount {
const fallbackRole = "role" in account ? account.role : "viewer";
const fallbackOrganization = "organization" in account ? account.organization : undefined;
@@ -505,16 +531,73 @@ export async function getCurrentAuthContext(): Promise<AuthContext | null> {
const permissions = [...new Set([...platformPermissions, ...organizationPermissions])];
const organization = organizationRow ? toOrganization(organizationRow) : undefined;
const membership = membershipRow ? toMembership(membershipRow.membership, membershipRow.role) : undefined;
const canAccessAllOrganizations =
platformPermissions.includes("platform:manage") || platformPermissions.includes("organization:read");
const organizationOptions = canAccessAllOrganizations
? (
await database
.select()
.from(organizations)
.where(eq(organizations.status, "active"))
).map((item) => toOrganizationOption(item, platformRole?.label ?? "平台账号", row.session.activeOrganizationId))
: (
await database
.select({ membership: memberships, organization: organizations, role: roles })
.from(memberships)
.innerJoin(organizations, eq(organizations.id, memberships.organizationId))
.innerJoin(roles, eq(roles.id, memberships.roleId))
.where(
and(
eq(memberships.accountId, row.account.id),
eq(memberships.status, "active"),
eq(organizations.status, "active"),
eq(roles.isEnabled, true),
),
)
).map((item) => toOrganizationOption(item.organization, item.role.label, row.session.activeOrganizationId));
return {
account: toPublicAccount(row.account, platformRole?.key ?? membership?.roleKey, organization),
organization,
organizations: organizationOptions,
membership,
permissions,
session,
};
}
export async function switchActiveOrganization(organizationId: string): Promise<AuthContext> {
const context = await getCurrentAuthContext();
if (!context) {
throw new Error("未登录或会话已过期");
}
const target = context.organizations.find((organization) => organization.id === organizationId);
if (!target) {
throw new Error("无权切换到该机构");
}
const database = getDatabase();
await database.update(sessions).set({ activeOrganizationId: organizationId }).where(eq(sessions.id, context.session.id));
await recordAuditLog({
actor: context.account,
organizationId,
action: "session.organization.switch",
targetType: "organization",
targetId: organizationId,
result: "success",
reason: `切换机构:${target.name}`,
});
const nextContext = await getCurrentAuthContext();
if (!nextContext) {
throw new Error("机构已切换,请重新登录");
}
return nextContext;
}
export async function removeCurrentSession(): Promise<PublicAccount | null> {
const context = await getCurrentAuthContext();
const cookieStore = await cookies();

View File

@@ -162,6 +162,14 @@ export type Account = {
export type PublicAccount = Omit<Account, "passwordHash" | "passwordSalt">;
export type AccountOrganizationOption = Pick<
Organization,
"id" | "name" | "slug" | "status" | "registrationEnabled" | "oidcEnabled"
> & {
roleLabel: string;
isActive: boolean;
};
export type Session = {
id: string;
accountId: string;
@@ -263,6 +271,7 @@ export type AppData = {
export type AuthContext = {
account: PublicAccount;
organization?: Organization;
organizations: AccountOrganizationOption[];
membership?: Membership;
permissions: Permission[];
session: Session;