Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import { prisma } from './db'; import { cached, cacheSet, cacheInvalidate, cacheInvalidatePattern, invalidateJoinedDisplayCaches, } from './cache'; import { hashPassword } from './auth'; import type { Role, UserStatus } from '@prisma/client'; // Public view of a user (no passwordHash). export type SafeUser = { id: string; username: string; role: Role; status: UserStatus; lastLoginAt: Date | null; createdAt: Date; }; const SELECT_SAFE = { id: true, username: true, role: true, status: true, lastLoginAt: true, createdAt: true, } as const; const USER_DETAIL_TTL = 600; // 10 min const USER_LIST_TTL = 120; // 2 min — list changes more often // ---------- READ ---------- export function getUserById(id: string): Promise<SafeUser | null> { return cached( `user:byId:${id}`, () => prisma.user.findUnique({ where: { id }, select: SELECT_SAFE }), { ttlSeconds: USER_DETAIL_TTL }, ); } export function listUsers(): Promise<SafeUser[]> { return cached( 'user:list:all', () => prisma.user.findMany({ select: SELECT_SAFE, orderBy: [{ role: 'asc' }, { username: 'asc' }], }), { ttlSeconds: USER_LIST_TTL }, ); } // ---------- WRITE (each mutation MUST refresh/invalidate cache) ---------- export async function createUser(input: { username: string; password: string; role: Role; }): Promise<SafeUser> { const user = await prisma.user.create({ data: { username: input.username, passwordHash: await hashPassword(input.password), role: input.role, }, select: SELECT_SAFE, }); await cacheSet(`user:byId:${user.id}`, user, { ttlSeconds: USER_DETAIL_TTL }); await cacheInvalidatePattern('user:list:*'); return user; } export async function updateUser( id: string, patch: { username?: string; role?: Role; status?: UserStatus }, ): Promise<SafeUser> { const user = await prisma.user.update({ where: { id }, data: patch, select: SELECT_SAFE, }); await cacheSet(`user:byId:${id}`, user, { ttlSeconds: USER_DETAIL_TTL }); await cacheInvalidatePattern('user:list:*'); // Username appears as a denormalised display string in many cached lists // (activity log, transfer.initiatedByUsername, claim.filedByUsername). // If username actually changed, drop those too. if (patch.username !== undefined) { await invalidateJoinedDisplayCaches(); } return user; } export async function deleteUser(id: string): Promise<void> { await prisma.user.delete({ where: { id } }); await cacheInvalidate(`user:byId:${id}`); await cacheInvalidatePattern('user:list:*'); // Any list joining this user is now stale (would show old username text). await invalidateJoinedDisplayCaches(); } export async function setUserPassword( id: string, newPassword: string, ): Promise<SafeUser> { const user = await prisma.user.update({ where: { id }, data: { passwordHash: await hashPassword(newPassword) }, select: SELECT_SAFE, }); await cacheSet(`user:byId:${id}`, user, { ttlSeconds: USER_DETAIL_TTL }); await cacheInvalidatePattern('user:list:*'); return user; } // ---------- HELPERS used by server actions ---------- export async function findUserByUsername(username: string) { return prisma.user.findUnique({ where: { username }, select: { id: true } }); } export async function getUserWithHash(id: string) { return prisma.user.findUnique({ where: { id }, select: { id: true, role: true, passwordHash: true }, }); } |