ghp / packages /server /src /repositories /userRepository.ts
QSLY's picture
deploy: build Hugging Face Space from source
00a912e
Raw
History Blame Contribute Delete
7.13 kB
import { db } from './database.js'
import { config } from '../config.js'
export type ServerUserRole = 'user' | 'admin'
export interface PersistedUser {
id: string
nickname: string
avatarUrl: string | null
passwordHash: string | null
role: ServerUserRole
createdAt: number
updatedAt: number
lastSeenAt: number
}
interface UserRow {
id: string
nickname: string
avatar_url: string | null
password_hash: string | null
role: ServerUserRole
created_at: number
updated_at: number
last_seen_at: number
}
interface PlatformAuthRenameRow {
platform: string
cookie_encrypted: string
nickname_snapshot: string | null
vip_type: number
created_at: number
updated_at: number
}
interface PermanentRoomMembershipRenameRow {
room_id: string
joined_at: number
last_seen_at: number
}
export type RenameUserResult =
| { success: true; user: PersistedUser }
| { success: false; reason: 'not_found' | 'conflict' }
function toUser(row: UserRow): PersistedUser {
return {
id: row.id,
nickname: row.nickname,
avatarUrl: row.avatar_url,
passwordHash: row.password_hash,
role: row.role,
createdAt: row.created_at,
updatedAt: row.updated_at,
lastSeenAt: row.last_seen_at,
}
}
const selectUser = db.prepare<string, UserRow>('SELECT * FROM users WHERE id = ?')
const selectUserCaseInsensitive = db.prepare<string, UserRow>('SELECT * FROM users WHERE id = ? COLLATE NOCASE LIMIT 1')
const insertUser = db.prepare(`
INSERT INTO users (id, nickname, avatar_url, password_hash, role, created_at, updated_at, last_seen_at)
VALUES (@id, @nickname, NULL, NULL, @role, @now, @now, @now)
`)
const touchUser = db.prepare('UPDATE users SET last_seen_at = ?, updated_at = ? WHERE id = ?')
const updateProfile = db.prepare(
'UPDATE users SET nickname = COALESCE(?, nickname), avatar_url = COALESCE(?, avatar_url), updated_at = ? WHERE id = ?',
)
const setPasswordHash = db.prepare('UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?')
const setRole = db.prepare('UPDATE users SET role = ?, updated_at = ? WHERE id = ?')
const listUsers = db.prepare<[], UserRow>('SELECT * FROM users ORDER BY created_at DESC')
const deleteUser = db.prepare('DELETE FROM users WHERE id = ?')
const insertRenamedUser = db.prepare(`
INSERT INTO users (id, nickname, avatar_url, password_hash, role, created_at, updated_at, last_seen_at)
VALUES (@id, @nickname, @avatarUrl, @passwordHash, @role, @createdAt, @now, @now)
`)
const selectPlatformAuthForRename = db.prepare<[string], PlatformAuthRenameRow>(`
SELECT platform, cookie_encrypted, nickname_snapshot, vip_type, created_at, updated_at
FROM platform_auth
WHERE user_id = ?
ORDER BY updated_at DESC
`)
const deletePlatformAuthForRename = db.prepare('DELETE FROM platform_auth WHERE user_id = ?')
const selectPermanentRoomMembershipsForRename = db.prepare<[string], PermanentRoomMembershipRenameRow>(`
SELECT room_id, joined_at, last_seen_at
FROM permanent_room_members
WHERE user_id = ?
`)
const insertRenamedPermanentRoomMembership = db.prepare(`
INSERT INTO permanent_room_members (room_id, user_id, joined_at, last_seen_at)
VALUES (@roomId, @userId, @joinedAt, @lastSeenAt)
ON CONFLICT(room_id, user_id) DO UPDATE SET
joined_at = MIN(permanent_room_members.joined_at, excluded.joined_at),
last_seen_at = MAX(permanent_room_members.last_seen_at, excluded.last_seen_at)
`)
const insertRenamedPlatformAuth = db.prepare(`
INSERT INTO platform_auth (
id, user_id, platform, cookie_encrypted, nickname_snapshot, vip_type, created_at, updated_at
)
VALUES (@id, @userId, @platform, @cookie, @nickname, @vipType, @createdAt, @updatedAt)
`)
const renameUser = db.transaction((oldUserId: string, newUserId: string): RenameUserResult => {
const current = selectUser.get(oldUserId)
if (!current) return { success: false, reason: 'not_found' }
if (oldUserId === newUserId) return { success: true, user: toUser(current) }
const conflict = selectUserCaseInsensitive.get(newUserId)
if (conflict && conflict.id !== oldUserId) return { success: false, reason: 'conflict' }
const authRows = selectPlatformAuthForRename.all(oldUserId)
const roomMemberships = selectPermanentRoomMembershipsForRename.all(oldUserId)
const now = Date.now()
insertRenamedUser.run({
id: newUserId,
nickname: current.nickname,
avatarUrl: current.avatar_url,
passwordHash: current.password_hash,
role: current.role,
createdAt: current.created_at,
now,
})
deletePlatformAuthForRename.run(oldUserId)
const seenPlatforms = new Set<string>()
for (const row of authRows) {
if (seenPlatforms.has(row.platform)) continue
seenPlatforms.add(row.platform)
insertRenamedPlatformAuth.run({
id: `account:${newUserId}:${row.platform}`,
userId: newUserId,
platform: row.platform,
cookie: row.cookie_encrypted,
nickname: row.nickname_snapshot,
vipType: row.vip_type,
createdAt: row.created_at,
updatedAt: Math.max(row.updated_at, now),
})
}
for (const membership of roomMemberships) {
insertRenamedPermanentRoomMembership.run({
roomId: membership.room_id,
userId: newUserId,
joinedAt: membership.joined_at,
lastSeenAt: membership.last_seen_at,
})
}
deleteUser.run(oldUserId)
return { success: true, user: toUser(selectUser.get(newUserId)!) }
})
export const userRepo = {
get(userId: string): PersistedUser | null {
const row = selectUser.get(userId)
return row ? toUser(row) : null
},
touch(userId: string): PersistedUser | null {
const existing = this.get(userId)
if (!existing) return null
const now = Date.now()
touchUser.run(now, now, userId)
if (config.serverAdminIds.has(userId) && existing.role !== 'admin') {
setRole.run('admin', now, userId)
}
return this.get(userId)
},
ensure(userId: string, defaults?: { nickname?: string }): PersistedUser {
const existing = this.touch(userId)
const now = Date.now()
if (existing) return existing
insertUser.run({
id: userId,
nickname: defaults?.nickname?.trim() ?? '',
role: config.serverAdminIds.has(userId) ? 'admin' : 'user',
now,
})
return this.get(userId)!
},
updateProfile(userId: string, data: { nickname?: string; avatarUrl?: string }): PersistedUser {
this.ensure(userId)
updateProfile.run(data.nickname?.trim() || null, data.avatarUrl ?? null, Date.now(), userId)
return this.get(userId)!
},
setPasswordHash(userId: string, passwordHash: string): PersistedUser {
this.ensure(userId)
setPasswordHash.run(passwordHash, Date.now(), userId)
return this.get(userId)!
},
list(): PersistedUser[] {
return listUsers.all().map(toUser)
},
delete(userId: string): boolean {
return deleteUser.run(userId).changes > 0
},
rename(oldUserId: string, newUserId: string): RenameUserResult {
return renameUser(oldUserId, newUserId)
},
isServerAdmin(userId: string): boolean {
return config.serverAdminIds.has(userId) || this.get(userId)?.role === 'admin'
},
}