mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-19 01:51:11 +08:00
Centralize request audit recording, preserve immutable download-task history, and derive hourly statistics and backfills from the same authoritative sources. Add durable user registration facts so admin deletion no longer destroys signup history.
265 lines
9.3 KiB
TypeScript
265 lines
9.3 KiB
TypeScript
import { relations, sql } from 'drizzle-orm'
|
|
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
|
|
export const user = sqliteTable(
|
|
'user',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
email: text('email').notNull().unique(),
|
|
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
|
|
image: text('image'),
|
|
role: text('role'),
|
|
banned: integer('banned', { mode: 'boolean' }).default(false),
|
|
banReason: text('ban_reason'),
|
|
banExpires: integer('ban_expires', { mode: 'timestamp_ms' }),
|
|
username: text('username').unique(),
|
|
displayUsername: text('display_username'),
|
|
lastActiveAt: integer('last_active_at', { mode: 'timestamp_ms' }),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index('user_created_idx').on(table.createdAt), index('user_lastActiveAt_idx').on(table.lastActiveAt)],
|
|
)
|
|
|
|
export const session = sqliteTable(
|
|
'session',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
|
token: text('token').notNull().unique(),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
ipAddress: text('ip_address'),
|
|
userAgent: text('user_agent'),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
impersonatedBy: text('impersonated_by'),
|
|
activeOrganizationId: text('active_organization_id'),
|
|
},
|
|
(table) => [index('session_userId_idx').on(table.userId), index('session_created_idx').on(table.createdAt)],
|
|
)
|
|
|
|
export const account = sqliteTable(
|
|
'account',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
accountId: text('account_id').notNull(),
|
|
providerId: text('provider_id').notNull(),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
accessToken: text('access_token'),
|
|
refreshToken: text('refresh_token'),
|
|
idToken: text('id_token'),
|
|
accessTokenExpiresAt: integer('access_token_expires_at', {
|
|
mode: 'timestamp_ms',
|
|
}),
|
|
refreshTokenExpiresAt: integer('refresh_token_expires_at', {
|
|
mode: 'timestamp_ms',
|
|
}),
|
|
scope: text('scope'),
|
|
password: text('password'),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index('account_userId_idx').on(table.userId)],
|
|
)
|
|
|
|
export const verification = sqliteTable(
|
|
'verification',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
identifier: text('identifier').notNull(),
|
|
value: text('value').notNull(),
|
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index('verification_identifier_idx').on(table.identifier)],
|
|
)
|
|
|
|
export const organization = sqliteTable('organization', {
|
|
id: text('id').primaryKey(),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull().unique(),
|
|
logo: text('logo'),
|
|
metadata: text('metadata'),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date()),
|
|
})
|
|
|
|
export const member = sqliteTable(
|
|
'member',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id')
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
role: text('role').notNull().default('member'),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
},
|
|
(table) => [index('member_organizationId_idx').on(table.organizationId), index('member_userId_idx').on(table.userId)],
|
|
)
|
|
|
|
export const invitation = sqliteTable(
|
|
'invitation',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
organizationId: text('organization_id')
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: 'cascade' }),
|
|
email: text('email').notNull(),
|
|
role: text('role').notNull(),
|
|
status: text('status').notNull().default('pending'),
|
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
inviterId: text('inviter_id')
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: 'cascade' }),
|
|
},
|
|
(table) => [
|
|
index('invitation_organizationId_idx').on(table.organizationId),
|
|
index('invitation_email_idx').on(table.email),
|
|
],
|
|
)
|
|
|
|
// apikey table — managed by @better-auth/api-key plugin
|
|
export const apikey = sqliteTable(
|
|
'apikey',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
configId: text('config_id').notNull().default('default'),
|
|
name: text('name'),
|
|
start: text('start'),
|
|
referenceId: text('reference_id').notNull(), // organizationId when references='organization'
|
|
prefix: text('prefix'),
|
|
key: text('key').notNull(),
|
|
refillInterval: integer('refill_interval'),
|
|
refillAmount: integer('refill_amount'),
|
|
lastRefillAt: integer('last_refill_at', { mode: 'timestamp_ms' }),
|
|
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
|
rateLimitEnabled: integer('rate_limit_enabled', { mode: 'boolean' }).notNull().default(true),
|
|
rateLimitTimeWindow: integer('rate_limit_time_window'),
|
|
rateLimitMax: integer('rate_limit_max'),
|
|
requestCount: integer('request_count').notNull().default(0),
|
|
remaining: integer('remaining'),
|
|
lastRequest: integer('last_request', { mode: 'timestamp_ms' }),
|
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
|
permissions: text('permissions'), // JSON-serialized Statements
|
|
metadata: text('metadata'), // JSON string
|
|
},
|
|
(table) => [
|
|
index('apikey_config_id_idx').on(table.configId),
|
|
index('apikey_reference_id_idx').on(table.referenceId),
|
|
index('apikey_key_idx').on(table.key),
|
|
],
|
|
)
|
|
|
|
export const deviceCode = sqliteTable(
|
|
'deviceCode',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
deviceCode: text('device_code').notNull(),
|
|
userCode: text('user_code').notNull(),
|
|
userId: text('user_id'),
|
|
clientId: text('client_id'),
|
|
scope: text('scope'),
|
|
status: text('status').notNull(),
|
|
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
|
lastPolledAt: integer('last_polled_at', { mode: 'timestamp_ms' }),
|
|
pollingInterval: integer('polling_interval'),
|
|
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
index('deviceCode_device_code_idx').on(table.deviceCode),
|
|
index('deviceCode_user_code_idx').on(table.userCode),
|
|
index('deviceCode_status_idx').on(table.status),
|
|
],
|
|
)
|
|
|
|
export const userRelations = relations(user, ({ many }) => ({
|
|
sessions: many(session),
|
|
accounts: many(account),
|
|
members: many(member),
|
|
}))
|
|
|
|
export const sessionRelations = relations(session, ({ one }) => ({
|
|
user: one(user, {
|
|
fields: [session.userId],
|
|
references: [user.id],
|
|
}),
|
|
}))
|
|
|
|
export const accountRelations = relations(account, ({ one }) => ({
|
|
user: one(user, {
|
|
fields: [account.userId],
|
|
references: [user.id],
|
|
}),
|
|
}))
|
|
|
|
export const organizationRelations = relations(organization, ({ many }) => ({
|
|
members: many(member),
|
|
invitations: many(invitation),
|
|
}))
|
|
|
|
export const memberRelations = relations(member, ({ one }) => ({
|
|
organization: one(organization, {
|
|
fields: [member.organizationId],
|
|
references: [organization.id],
|
|
}),
|
|
user: one(user, {
|
|
fields: [member.userId],
|
|
references: [user.id],
|
|
}),
|
|
}))
|
|
|
|
export const invitationRelations = relations(invitation, ({ one }) => ({
|
|
organization: one(organization, {
|
|
fields: [invitation.organizationId],
|
|
references: [organization.id],
|
|
}),
|
|
inviter: one(user, {
|
|
fields: [invitation.inviterId],
|
|
references: [user.id],
|
|
}),
|
|
}))
|