expand user permissions

This commit is contained in:
Garret Patti
2026-04-18 11:48:01 -04:00
parent 8f84da7e2f
commit 2ea02b197b
27 changed files with 214 additions and 110 deletions

View File

@@ -67,7 +67,7 @@ export async function verifyPassword(password: string, hash: string): Promise<bo
}
// Auth guard result type
type AuthSuccess = { session: IronSession<SessionData> }
type AuthSuccess = { session: IronSession<SessionData>; accessLevel?: 'admin' | 'write' | 'read' }
type AuthResult = AuthSuccess | NextResponse
// Read-only session from an API route request (throwaway response)
@@ -100,13 +100,22 @@ export async function requireLibraryAccess(req: NextRequest, libraryId: string):
if (!session.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (session.role === 'admin') return { session }
if (session.role === 'admin') return { session, accessLevel: 'admin' }
// Lazy import to avoid pulling DB into edge contexts
const { getPermittedLibraryIds } = await import('./users')
const permitted = getPermittedLibraryIds(session.userId)
if (!permitted.includes(libraryId)) {
const { getLibraryAccessLevel } = await import('./users')
const accessLevel = getLibraryAccessLevel(session.userId, libraryId)
if (!accessLevel) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
return { session }
return { session, accessLevel }
}
export async function requireLibraryWriteAccess(req: NextRequest, libraryId: string): Promise<AuthResult> {
const result = await requireLibraryAccess(req, libraryId)
if (result instanceof NextResponse) return result
if (result.accessLevel === 'read') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
return result
}

View File

@@ -106,6 +106,7 @@ function initDb(db: Database.Database): void {
migrateMediaItemsAiFields(db)
migrateLibraryAiSettings(db)
migrateAiJobs(db)
migrateLibraryPermissionsAccessLevel(db)
seedAppSettings(db)
}
@@ -318,6 +319,15 @@ function migrateLibrariesType(db: Database.Database): void {
}
}
function migrateLibraryPermissionsAccessLevel(db: Database.Database): void {
const row = db
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='library_permissions'")
.get() as { sql: string } | undefined
if (row && !row.sql.includes('access_level')) {
db.exec(`ALTER TABLE library_permissions ADD COLUMN access_level TEXT NOT NULL DEFAULT 'write'`)
}
}
function migrateAiJobs(db: Database.Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS ai_jobs (

View File

@@ -77,43 +77,60 @@ export function listUsers(): User[] {
}))
}
export function getPermittedLibraryIds(userId: string): string[] {
const db = getDb()
const rows = db
.prepare('SELECT library_id FROM library_permissions WHERE user_id = ?')
.all(userId) as { library_id: string }[]
return rows.map((r) => r.library_id)
export interface LibraryPermission {
libraryId: string
accessLevel: 'read' | 'write'
}
export function setLibraryPermissions(userId: string, libraryIds: string[]): void {
export function getLibraryPermissions(userId: string): LibraryPermission[] {
const db = getDb()
const rows = db
.prepare('SELECT library_id, access_level FROM library_permissions WHERE user_id = ?')
.all(userId) as { library_id: string; access_level: string }[]
return rows.map((r) => ({ libraryId: r.library_id, accessLevel: r.access_level as 'read' | 'write' }))
}
export function getLibraryAccessLevel(userId: string, libraryId: string): 'read' | 'write' | null {
const db = getDb()
const row = db
.prepare('SELECT access_level FROM library_permissions WHERE user_id = ? AND library_id = ?')
.get(userId, libraryId) as { access_level: string } | undefined
if (!row) return null
return row.access_level as 'read' | 'write'
}
export function setLibraryPermissions(userId: string, permissions: LibraryPermission[]): void {
const db = getDb()
const tx = db.transaction(() => {
db.prepare('DELETE FROM library_permissions WHERE user_id = ?').run(userId)
const insert = db.prepare('INSERT INTO library_permissions (user_id, library_id) VALUES (?, ?)')
for (const libraryId of libraryIds) {
insert.run(userId, libraryId)
const insert = db.prepare(
'INSERT INTO library_permissions (user_id, library_id, access_level) VALUES (?, ?, ?)'
)
for (const { libraryId, accessLevel } of permissions) {
insert.run(userId, libraryId, accessLevel)
}
})
tx()
}
export function getLibrariesForUser(userId: string, role: 'admin' | 'user'): Library[] {
if (role === 'admin') return getLibraries()
if (role === 'admin') return getLibraries().map((l) => ({ ...l, accessLevel: 'admin' as const }))
const db = getDb()
const rows = db
.prepare(
`SELECT l.id, l.name, l.path, l.type, l.cover_ext
`SELECT l.id, l.name, l.path, l.type, l.cover_ext, lp.access_level
FROM libraries l
INNER JOIN library_permissions lp ON lp.library_id = l.id
WHERE lp.user_id = ?
ORDER BY l.name ASC`
)
.all(userId) as { id: string; name: string; path: string; type: string; cover_ext: string | null }[]
.all(userId) as { id: string; name: string; path: string; type: string; cover_ext: string | null; access_level: string }[]
return rows.map((r) => ({
id: r.id,
name: r.name,
path: r.path,
type: r.type as Library['type'],
coverExt: r.cover_ext,
accessLevel: r.access_level as 'read' | 'write',
}))
}