mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
269 lines
8.3 KiB
TypeScript
269 lines
8.3 KiB
TypeScript
import { and, count, desc, eq, inArray, or, sql } from 'drizzle-orm'
|
|
import { nanoid } from 'nanoid'
|
|
import { member, organization, user } from '../db/auth-schema'
|
|
import { orgQuotaEntitlements, orgQuotas } from '../db/schema'
|
|
import type { Database } from '../platform/interface'
|
|
|
|
export interface UserWithOrg {
|
|
id: string
|
|
name: string
|
|
username: string
|
|
email: string
|
|
image: string | null
|
|
role: string | null
|
|
banned: boolean | null
|
|
createdAt: Date
|
|
orgId: string | null
|
|
orgName: string | null
|
|
quotaUsed: number
|
|
quotaDefault: number
|
|
quotaTotal: number
|
|
}
|
|
|
|
export interface QuotaEntitlementItem {
|
|
id: string
|
|
orgId: string
|
|
resourceType: string
|
|
entitlementType: string
|
|
source: string
|
|
sourceId: string
|
|
bytes: number
|
|
startsAt: Date
|
|
expiresAt: Date | null
|
|
status: string
|
|
metadata: string | null
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
export interface UserOperationFailure {
|
|
error: string
|
|
status: 404
|
|
}
|
|
|
|
export async function listUsers(
|
|
db: Database,
|
|
page: number,
|
|
pageSize: number,
|
|
search?: string,
|
|
): Promise<{ items: UserWithOrg[]; total: number }> {
|
|
const offset = (page - 1) * pageSize
|
|
const term = search?.trim().toLowerCase()
|
|
const now = new Date()
|
|
const filter = term
|
|
? or(
|
|
sql`lower(${user.name}) like ${`%${term}%`}`,
|
|
sql`lower(${user.username}) like ${`%${term}%`}`,
|
|
sql`lower(${user.email}) like ${`%${term}%`}`,
|
|
)
|
|
: undefined
|
|
|
|
const countRows = filter
|
|
? await db.select({ total: count() }).from(user).where(filter)
|
|
: await db.select({ total: count() }).from(user)
|
|
const total = countRows[0]?.total ?? 0
|
|
|
|
const query = db
|
|
.select({
|
|
id: user.id,
|
|
name: user.name,
|
|
username: user.username,
|
|
email: user.email,
|
|
image: user.image,
|
|
role: user.role,
|
|
banned: user.banned,
|
|
createdAt: user.createdAt,
|
|
orgId: organization.id,
|
|
orgName: organization.name,
|
|
quotaUsed: orgQuotas.used,
|
|
quotaDefault: sql<number>`0`,
|
|
quotaTotal: activeStorageEntitlementBytesSql(now),
|
|
})
|
|
.from(user)
|
|
.leftJoin(organization, eq(organization.slug, sql`'personal-' || ${user.id}`))
|
|
.leftJoin(orgQuotas, eq(orgQuotas.orgId, organization.id))
|
|
|
|
const rows = await (filter ? query.where(filter) : query).orderBy(desc(user.createdAt)).limit(pageSize).offset(offset)
|
|
|
|
const items: UserWithOrg[] = rows.map((row) => ({
|
|
...row,
|
|
username: row.username ?? '',
|
|
quotaUsed: row.quotaUsed ?? 0,
|
|
quotaDefault: row.quotaDefault ?? 0,
|
|
quotaTotal: row.quotaTotal ?? 0,
|
|
}))
|
|
|
|
return { items, total }
|
|
}
|
|
|
|
export async function getUser(db: Database, userId: string): Promise<UserWithOrg | UserOperationFailure> {
|
|
const now = new Date()
|
|
const rows = await db
|
|
.select({
|
|
id: user.id,
|
|
name: user.name,
|
|
username: user.username,
|
|
email: user.email,
|
|
image: user.image,
|
|
role: user.role,
|
|
banned: user.banned,
|
|
createdAt: user.createdAt,
|
|
orgId: organization.id,
|
|
orgName: organization.name,
|
|
quotaUsed: orgQuotas.used,
|
|
quotaDefault: sql<number>`0`,
|
|
quotaTotal: activeStorageEntitlementBytesSql(now),
|
|
})
|
|
.from(user)
|
|
.leftJoin(organization, eq(organization.slug, sql`'personal-' || ${user.id}`))
|
|
.leftJoin(orgQuotas, eq(orgQuotas.orgId, organization.id))
|
|
.where(eq(user.id, userId))
|
|
|
|
const row = rows[0]
|
|
if (!row) return { error: `User not found: ${userId}`, status: 404 }
|
|
return {
|
|
...row,
|
|
username: row.username ?? '',
|
|
quotaUsed: row.quotaUsed ?? 0,
|
|
quotaDefault: row.quotaDefault ?? 0,
|
|
quotaTotal: row.quotaTotal ?? 0,
|
|
}
|
|
}
|
|
|
|
function activeStorageEntitlementBytesSql(now: Date) {
|
|
const timestamp = now.getTime()
|
|
return sql<number>`(
|
|
SELECT COALESCE(SUM(${orgQuotaEntitlements.bytes}), 0)
|
|
FROM ${orgQuotaEntitlements}
|
|
WHERE ${orgQuotaEntitlements.orgId} = ${organization.id}
|
|
AND ${orgQuotaEntitlements.resourceType} = 'storage'
|
|
AND ${orgQuotaEntitlements.status} = 'active'
|
|
AND ${orgQuotaEntitlements.startsAt} <= ${timestamp}
|
|
AND (${orgQuotaEntitlements.expiresAt} IS NULL OR ${orgQuotaEntitlements.expiresAt} > ${timestamp})
|
|
)`
|
|
}
|
|
|
|
export async function setUserStatus(db: Database, userId: string, status: 'active' | 'disabled'): Promise<boolean> {
|
|
const existing = await db.select({ id: user.id }).from(user).where(eq(user.id, userId))
|
|
if (existing.length === 0) return false
|
|
|
|
await db
|
|
.update(user)
|
|
.set({ banned: status === 'disabled' })
|
|
.where(eq(user.id, userId))
|
|
return true
|
|
}
|
|
|
|
export async function deleteUser(db: Database, userId: string): Promise<boolean> {
|
|
const existing = await db.select({ id: user.id }).from(user).where(eq(user.id, userId))
|
|
if (existing.length === 0) return false
|
|
|
|
await db.delete(user).where(eq(user.id, userId))
|
|
return true
|
|
}
|
|
|
|
export async function setUsersStatus(
|
|
db: Database,
|
|
userIds: string[],
|
|
status: 'active' | 'disabled',
|
|
): Promise<{ updated: number; ids: string[] } | UserOperationFailure> {
|
|
const existingIds = await requireUsers(db, userIds)
|
|
if ('error' in existingIds) return existingIds
|
|
|
|
await db
|
|
.update(user)
|
|
.set({ banned: status === 'disabled' })
|
|
.where(inArray(user.id, existingIds))
|
|
return { updated: existingIds.length, ids: existingIds }
|
|
}
|
|
|
|
export async function deleteUsers(
|
|
db: Database,
|
|
userIds: string[],
|
|
): Promise<{ deleted: number; ids: string[] } | UserOperationFailure> {
|
|
const existingIds = await requireUsers(db, userIds)
|
|
if ('error' in existingIds) return existingIds
|
|
|
|
await db.delete(user).where(inArray(user.id, existingIds))
|
|
return { deleted: existingIds.length, ids: existingIds }
|
|
}
|
|
|
|
export async function listUserPersonalEntitlements(
|
|
db: Database,
|
|
userId: string,
|
|
): Promise<{ orgId: string; items: QuotaEntitlementItem[] } | UserOperationFailure> {
|
|
const org = await findUserPersonalOrg(db, userId)
|
|
if ('error' in org) return org
|
|
const items = await db
|
|
.select()
|
|
.from(orgQuotaEntitlements)
|
|
.where(eq(orgQuotaEntitlements.orgId, org.orgId))
|
|
.orderBy(desc(orgQuotaEntitlements.createdAt))
|
|
return { orgId: org.orgId, items }
|
|
}
|
|
|
|
export async function grantUserPersonalEntitlement(
|
|
db: Database,
|
|
input: {
|
|
adminUserId: string
|
|
targetUserId: string
|
|
resourceType: 'storage'
|
|
bytes: number
|
|
expiresAt?: Date | null
|
|
note?: string | null
|
|
},
|
|
): Promise<{ orgId: string; entitlement: QuotaEntitlementItem } | UserOperationFailure> {
|
|
const org = await findUserPersonalOrg(db, input.targetUserId)
|
|
if ('error' in org) return org
|
|
const now = new Date()
|
|
const entitlement = {
|
|
id: nanoid(),
|
|
orgId: org.orgId,
|
|
resourceType: input.resourceType,
|
|
entitlementType: 'grant',
|
|
source: 'admin_grant',
|
|
sourceId: `admin_grant:${nanoid()}`,
|
|
bytes: input.bytes,
|
|
startsAt: now,
|
|
expiresAt: input.expiresAt ?? null,
|
|
status: 'active',
|
|
metadata: JSON.stringify({
|
|
note: input.note ?? null,
|
|
grantedBy: input.adminUserId,
|
|
targetUserId: input.targetUserId,
|
|
}),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
} satisfies typeof orgQuotaEntitlements.$inferInsert
|
|
const rows = await db.insert(orgQuotaEntitlements).values(entitlement).returning()
|
|
return { orgId: org.orgId, entitlement: rows[0] }
|
|
}
|
|
|
|
async function findUserPersonalOrg(db: Database, userId: string): Promise<{ orgId: string } | UserOperationFailure> {
|
|
const existingIds = await requireUsers(db, [userId])
|
|
if ('error' in existingIds) return existingIds
|
|
const rows = await db
|
|
.select({ orgId: organization.id })
|
|
.from(user)
|
|
.innerJoin(member, eq(member.userId, user.id))
|
|
.innerJoin(
|
|
organization,
|
|
and(eq(organization.id, member.organizationId), eq(organization.slug, sql`'personal-' || ${user.id}`)),
|
|
)
|
|
.where(eq(user.id, userId))
|
|
const orgId = rows[0]?.orgId
|
|
if (!orgId) return { error: `Personal organization not found for user: ${userId}`, status: 404 }
|
|
return { orgId }
|
|
}
|
|
|
|
async function requireUsers(db: Database, userIds: string[]): Promise<string[] | UserOperationFailure> {
|
|
const uniqueIds = [...new Set(userIds)]
|
|
const rows = await db.select({ id: user.id }).from(user).where(inArray(user.id, uniqueIds))
|
|
if (rows.length !== uniqueIds.length) {
|
|
const found = new Set(rows.map((row) => row.id))
|
|
const missing = uniqueIds.filter((id) => !found.has(id))
|
|
return { error: `User not found: ${missing.join(', ')}`, status: 404 }
|
|
}
|
|
return uniqueIds
|
|
}
|