feat: add organization settings and invitations
This commit is contained in:
@@ -7,7 +7,15 @@ import { jsonFailure } from "@/modules/core/server/api";
|
||||
import { recordAuditLog } from "@/modules/core/server/audit";
|
||||
import { getDatabase } from "@/modules/core/server/db";
|
||||
import { getPermissionsForRole, hasPermission, seedOrganizationRoles } from "@/modules/core/server/permissions";
|
||||
import { accounts, joinRequests, memberships, organizations, roles, sessions } from "@/modules/core/server/schema";
|
||||
import {
|
||||
accounts,
|
||||
joinRequests,
|
||||
memberships,
|
||||
organizationInvitations,
|
||||
organizations,
|
||||
roles,
|
||||
sessions,
|
||||
} from "@/modules/core/server/schema";
|
||||
import type { Account, AuthContext, Membership, Organization, Permission, PublicAccount, Session } from "@/modules/core/types";
|
||||
|
||||
const SESSION_COOKIE = "teatea_session";
|
||||
@@ -33,6 +41,14 @@ function toOrganization(row: OrganizationRow): Organization {
|
||||
name: row.name,
|
||||
slug: row.slug,
|
||||
status: row.status,
|
||||
registrationEnabled: row.registrationEnabled,
|
||||
oidcEnabled: row.oidcEnabled,
|
||||
oidcIssuerUrl: row.oidcIssuerUrl,
|
||||
oidcClientId: row.oidcClientId,
|
||||
oidcHasClientSecret: row.oidcClientSecret.length > 0,
|
||||
oidcScopes: row.oidcScopes,
|
||||
oidcRedirectUri: row.oidcRedirectUri,
|
||||
oidcAutoProvision: row.oidcAutoProvision,
|
||||
createdAt: toIsoString(row.createdAt),
|
||||
updatedAt: toIsoString(row.updatedAt),
|
||||
};
|
||||
@@ -251,6 +267,7 @@ export async function createRegistration(input: {
|
||||
email: string;
|
||||
password: string;
|
||||
organizationId?: string;
|
||||
invitationToken?: string;
|
||||
}): Promise<{ account: PublicAccount; session?: Session }> {
|
||||
const database = getDatabase();
|
||||
const password = hashPassword(input.password);
|
||||
@@ -263,6 +280,39 @@ export async function createRegistration(input: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invitationRows = input.invitationToken
|
||||
? await transaction
|
||||
.select({ invitation: organizationInvitations, organization: organizations, role: roles })
|
||||
.from(organizationInvitations)
|
||||
.innerJoin(organizations, eq(organizations.id, organizationInvitations.organizationId))
|
||||
.innerJoin(roles, eq(roles.id, organizationInvitations.roleId))
|
||||
.where(eq(organizationInvitations.token, input.invitationToken))
|
||||
.limit(1)
|
||||
: [];
|
||||
const invitation = invitationRows[0];
|
||||
if (input.invitationToken && !invitation) {
|
||||
throw new Error("邀请链接无效");
|
||||
}
|
||||
if (invitation && (invitation.invitation.status !== "active" || invitation.invitation.expiresAt < new Date())) {
|
||||
throw new Error("邀请链接已失效");
|
||||
}
|
||||
|
||||
const targetOrganizationId = invitation?.invitation.organizationId ?? input.organizationId;
|
||||
if (targetOrganizationId && !invitation) {
|
||||
const organizationRows = await transaction
|
||||
.select()
|
||||
.from(organizations)
|
||||
.where(eq(organizations.id, targetOrganizationId))
|
||||
.limit(1);
|
||||
const organization = organizationRows[0];
|
||||
if (!organization) {
|
||||
throw new Error("机构不存在");
|
||||
}
|
||||
if (!organization.registrationEnabled) {
|
||||
throw new Error("该机构暂未开放注册");
|
||||
}
|
||||
}
|
||||
|
||||
const accountRows = await transaction
|
||||
.insert(accounts)
|
||||
.values({
|
||||
@@ -270,7 +320,7 @@ export async function createRegistration(input: {
|
||||
email: normalizedEmail,
|
||||
passwordHash: password.hash,
|
||||
passwordSalt: password.salt,
|
||||
status: input.organizationId ? "pending" : "active",
|
||||
status: targetOrganizationId && !invitation ? "pending" : "active",
|
||||
})
|
||||
.returning();
|
||||
const account = accountRows[0];
|
||||
@@ -278,43 +328,63 @@ export async function createRegistration(input: {
|
||||
throw new Error("账号创建失败");
|
||||
}
|
||||
|
||||
const sessionRows = input.organizationId
|
||||
if (invitation) {
|
||||
await transaction.insert(memberships).values({
|
||||
accountId: account.id,
|
||||
organizationId: invitation.invitation.organizationId,
|
||||
roleId: invitation.invitation.roleId,
|
||||
status: "active",
|
||||
});
|
||||
await transaction
|
||||
.update(organizationInvitations)
|
||||
.set({
|
||||
status: "accepted",
|
||||
acceptedByAccountId: account.id,
|
||||
acceptedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(organizationInvitations.id, invitation.invitation.id));
|
||||
}
|
||||
|
||||
const sessionRows = targetOrganizationId && !invitation
|
||||
? []
|
||||
: await transaction
|
||||
.insert(sessions)
|
||||
.values({
|
||||
accountId: account.id,
|
||||
activeOrganizationId: invitation?.invitation.organizationId,
|
||||
expiresAt,
|
||||
})
|
||||
.returning();
|
||||
const session = sessionRows[0];
|
||||
|
||||
if (input.organizationId) {
|
||||
if (targetOrganizationId && !invitation) {
|
||||
await transaction.insert(joinRequests).values({
|
||||
accountId: account.id,
|
||||
organizationId: input.organizationId,
|
||||
organizationId: targetOrganizationId,
|
||||
status: "pending",
|
||||
reason: "公开注册申请加入机构",
|
||||
});
|
||||
}
|
||||
|
||||
return { account, session };
|
||||
return { account, organization: invitation?.organization, role: invitation?.role, session };
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
throw new Error("账号已存在");
|
||||
}
|
||||
|
||||
const publicAccount = toPublicAccount(created.account);
|
||||
const organization = created.organization ? toOrganization(created.organization) : undefined;
|
||||
const publicAccount = toPublicAccount(created.account, created.role?.key, organization);
|
||||
const session = created.session ? toSession(created.session) : undefined;
|
||||
await recordAuditLog({
|
||||
actor: publicAccount,
|
||||
organizationId: input.organizationId,
|
||||
organizationId: organization?.id ?? input.organizationId,
|
||||
action: "account.register",
|
||||
targetType: "account",
|
||||
targetId: publicAccount.id,
|
||||
result: "success",
|
||||
reason: input.organizationId ? "注册账号并申请加入机构" : "注册独立账号",
|
||||
reason: input.invitationToken ? "通过邀请注册并加入机构" : input.organizationId ? "注册账号并申请加入机构" : "注册独立账号",
|
||||
});
|
||||
|
||||
return { account: publicAccount, session };
|
||||
|
||||
Reference in New Issue
Block a user