add auth
This commit is contained in:
36
src/app/api/auth/login/route.ts
Normal file
36
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getWritableSession, verifyPassword, type SessionData } from '@/lib/auth'
|
||||
import { getUserByUsername } from '@/lib/users'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { username?: string; password?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { username, password } = body
|
||||
if (!username || !password) {
|
||||
return NextResponse.json({ error: 'username and password are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const user = getUserByUsername(username)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
|
||||
}
|
||||
|
||||
const valid = await verifyPassword(password, user.passwordHash)
|
||||
if (!valid) {
|
||||
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ role: user.role })
|
||||
const session = await getWritableSession(request, response)
|
||||
session.userId = user.id
|
||||
session.username = user.username
|
||||
session.role = user.role
|
||||
await session.save()
|
||||
|
||||
return response
|
||||
}
|
||||
9
src/app/api/auth/logout/route.ts
Normal file
9
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getWritableSession } from '@/lib/auth'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const response = new NextResponse(null, { status: 204 })
|
||||
const session = await getWritableSession(request, response)
|
||||
session.destroy()
|
||||
return response
|
||||
}
|
||||
59
src/app/api/auth/register/route.ts
Normal file
59
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSessionOptions, hashPassword, type SessionData } from '@/lib/auth'
|
||||
import { getIronSession } from 'iron-session'
|
||||
import { getUserCount, createUser } from '@/lib/users'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { username?: string; password?: string; role?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { username, password } = body
|
||||
let { role } = body
|
||||
|
||||
if (!username || !password) {
|
||||
return NextResponse.json({ error: 'username and password are required' }, { status: 400 })
|
||||
}
|
||||
if (username.trim().length < 2) {
|
||||
return NextResponse.json({ error: 'Username must be at least 2 characters' }, { status: 400 })
|
||||
}
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json({ error: 'Password must be at least 8 characters' }, { status: 400 })
|
||||
}
|
||||
|
||||
const userCount = getUserCount()
|
||||
|
||||
if (userCount === 0) {
|
||||
// First user always becomes admin
|
||||
role = 'admin'
|
||||
} else {
|
||||
// Subsequent users require an admin session
|
||||
const res = new NextResponse()
|
||||
const session = await getIronSession<SessionData>(request, res, getSessionOptions())
|
||||
if (!session.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
if (session.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
if (role !== 'admin' && role !== 'user') {
|
||||
role = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password)
|
||||
|
||||
try {
|
||||
const user = createUser(username.trim(), passwordHash, role as 'admin' | 'user')
|
||||
return NextResponse.json({ id: user.id, username: user.username, role: user.role }, { status: 201 })
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to create user'
|
||||
if (message.includes('UNIQUE constraint failed')) {
|
||||
return NextResponse.json({ error: 'Username already taken' }, { status: 409 })
|
||||
}
|
||||
return NextResponse.json({ error: message }, { status: 400 })
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, resolveLibraryRoot } from '@/lib/libraries'
|
||||
import { scanDirectory } from '@/lib/files'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
const subpath = searchParams.get('path') ?? ''
|
||||
@@ -11,6 +12,9 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.mp4': 'video/mp4',
|
||||
@@ -35,6 +36,9 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId or path' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'fs'
|
||||
import sharp from 'sharp'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
|
||||
const MAX_COVER_BYTES = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
@@ -13,6 +14,9 @@ function isCoverType(s: string | null): s is CoverType {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
const itemId = searchParams.get('itemId')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, resolveLibraryRoot } from '@/lib/libraries'
|
||||
import { scanGamesLibrary } from '@/lib/games'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
|
||||
@@ -10,6 +11,9 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, removeLibrary } from '@/lib/libraries'
|
||||
import { removeAllAssignmentsForLibrary } from '@/lib/tags'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const library = getLibrary(id)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibraries, addLibrary } from '@/lib/libraries'
|
||||
import { getLibrariesForUser } from '@/lib/users'
|
||||
import { requireAuth, requireAdmin } from '@/lib/auth'
|
||||
import type { LibraryType } from '@/types'
|
||||
|
||||
export function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAuth(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
const { session } = auth
|
||||
|
||||
try {
|
||||
const libraries = getLibraries()
|
||||
const libraries =
|
||||
session.role === 'admin'
|
||||
? getLibraries()
|
||||
: getLibrariesForUser(session.userId, session.role)
|
||||
return NextResponse.json(libraries)
|
||||
} catch (err) {
|
||||
console.error('Failed to read libraries', err)
|
||||
@@ -13,6 +22,9 @@ export function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
let body: { name?: string; path?: string; type?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'fs'
|
||||
import sharp from 'sharp'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, updateLibraryCover, clearLibraryCover } from '@/lib/libraries'
|
||||
import { requireAuth, requireAdmin } from '@/lib/auth'
|
||||
|
||||
const COVERS_DIR = path.resolve(process.cwd(), '.covers')
|
||||
const MAX_COVER_BYTES = 10 * 1024 * 1024 // 10 MB
|
||||
@@ -12,9 +13,12 @@ function coverPath(id: string, ext: string) {
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAuth(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
const library = getLibrary(id)
|
||||
if (!library?.coverExt) {
|
||||
@@ -39,6 +43,9 @@ export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
const library = getLibrary(id)
|
||||
if (!library) {
|
||||
@@ -85,9 +92,12 @@ export async function POST(
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
const library = getLibrary(id)
|
||||
if (!library) {
|
||||
|
||||
@@ -4,8 +4,9 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
||||
import { scanMoviesLibrary } from '@/lib/movies'
|
||||
import { removeAllAssignmentsForItem } from '@/lib/tags'
|
||||
import { requireLibraryAccess, requireAdmin } from '@/lib/auth'
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
|
||||
@@ -13,6 +14,9 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
@@ -26,7 +30,10 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json(movies)
|
||||
}
|
||||
|
||||
export function DELETE(request: NextRequest) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
const movieId = searchParams.get('movieId')
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getResolvedTagsForItem, addTagToItem, removeTagFromItem } from '@/lib/tags'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
function extractLibraryId(mediaKey: string): string | null {
|
||||
const colonIdx = mediaKey.indexOf(':')
|
||||
if (colonIdx === -1) return null
|
||||
return mediaKey.slice(0, colonIdx)
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -8,6 +15,13 @@ export async function GET(request: NextRequest) {
|
||||
if (!mediaKey) {
|
||||
return NextResponse.json({ error: 'mediaKey is required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
return NextResponse.json(getResolvedTagsForItem(mediaKey))
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: (err as Error).message }, { status: 500 })
|
||||
@@ -20,6 +34,13 @@ export async function POST(request: NextRequest) {
|
||||
if (!mediaKey || !tagId) {
|
||||
return NextResponse.json({ error: 'mediaKey and tagId are required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
addTagToItem(mediaKey, tagId)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (err) {
|
||||
@@ -35,6 +56,13 @@ export async function DELETE(request: NextRequest) {
|
||||
if (!mediaKey || !tagId) {
|
||||
return NextResponse.json({ error: 'mediaKey and tagId are required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
removeTagFromItem(mediaKey, tagId)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { updateCategory, deleteCategory, deleteCategoryForce, getTags } from '@/lib/tags'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const { name } = await request.json()
|
||||
@@ -19,6 +23,9 @@ export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getCategories, addCategory } from '@/lib/tags'
|
||||
import { requireAuth, requireAdmin } from '@/lib/auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAuth(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return NextResponse.json(getCategories())
|
||||
} catch (err) {
|
||||
@@ -10,6 +14,9 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { name } = await request.json()
|
||||
const category = addCategory(name)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { updateTag, deleteTag } from '@/lib/tags'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const { name } = await request.json()
|
||||
@@ -16,9 +20,12 @@ export async function PATCH(
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
deleteTag(id)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getTags, getTagsSortedByUsage, addTag } from '@/lib/tags'
|
||||
import { requireAuth, requireAdmin } from '@/lib/auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAuth(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const categoryId = searchParams.get('categoryId') ?? undefined
|
||||
@@ -15,6 +19,9 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const { name, categoryId } = await request.json()
|
||||
const tag = addTag(name, categoryId)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getTagAssignmentsForLibrary } from '@/lib/tags'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
if (!libraryId) return Response.json({ error: 'libraryId required' }, { status: 400 })
|
||||
return Response.json(getTagAssignmentsForLibrary(libraryId))
|
||||
if (!libraryId) return NextResponse.json({ error: 'libraryId required' }, { status: 400 })
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
return NextResponse.json(getTagAssignmentsForLibrary(libraryId))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
||||
import { getThumbnailPath } from '@/lib/thumbnails'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.mkv', '.avi', '.webm', '.m4v'])
|
||||
const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'])
|
||||
@@ -23,6 +24,9 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId or path' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
|
||||
@@ -4,8 +4,9 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getLibrary, resolveLibraryRoot, resolveAndJail } from '@/lib/libraries'
|
||||
import { scanTvLibrary, scanTvSeasons, scanTvEpisodes } from '@/lib/tv'
|
||||
import { removeAllAssignmentsForItem } from '@/lib/tags'
|
||||
import { requireLibraryAccess, requireAdmin } from '@/lib/auth'
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
const seriesId = searchParams.get('seriesId')
|
||||
@@ -15,6 +16,9 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing libraryId' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
return NextResponse.json({ error: 'Library not found' }, { status: 404 })
|
||||
@@ -39,7 +43,10 @@ export function GET(request: NextRequest) {
|
||||
return NextResponse.json(series)
|
||||
}
|
||||
|
||||
export function DELETE(request: NextRequest) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { searchParams } = request.nextUrl
|
||||
const libraryId = searchParams.get('libraryId')
|
||||
const seriesId = searchParams.get('seriesId')
|
||||
|
||||
58
src/app/api/users/[id]/permissions/route.ts
Normal file
58
src/app/api/users/[id]/permissions/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
import { getUserById, getPermittedLibraryIds, setLibraryPermissions } from '@/lib/users'
|
||||
import { getLibraries } from '@/lib/libraries'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const user = getUserById(id)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const libraryIds = getPermittedLibraryIds(id)
|
||||
return NextResponse.json({ libraryIds })
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const { id } = await params
|
||||
|
||||
const user = getUserById(id)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
let body: { libraryIds?: unknown }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!Array.isArray(body.libraryIds) || !body.libraryIds.every((id) => typeof id === 'string')) {
|
||||
return NextResponse.json({ error: 'libraryIds must be an array of strings' }, { status: 400 })
|
||||
}
|
||||
|
||||
const allLibraries = getLibraries()
|
||||
const validIds = new Set(allLibraries.map((l) => l.id))
|
||||
const invalid = body.libraryIds.filter((id) => !validIds.has(id))
|
||||
if (invalid.length > 0) {
|
||||
return NextResponse.json({ error: `Unknown library IDs: ${invalid.join(', ')}` }, { status: 400 })
|
||||
}
|
||||
|
||||
setLibraryPermissions(id, body.libraryIds)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
}
|
||||
33
src/app/api/users/[id]/route.ts
Normal file
33
src/app/api/users/[id]/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
import { getUserById, deleteUser, listUsers } from '@/lib/users'
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
const { session } = auth
|
||||
|
||||
const { id } = await params
|
||||
|
||||
if (id === session.userId) {
|
||||
return NextResponse.json({ error: 'Cannot delete your own account' }, { status: 409 })
|
||||
}
|
||||
|
||||
const target = getUserById(id)
|
||||
if (!target) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (target.role === 'admin') {
|
||||
const admins = listUsers().filter((u) => u.role === 'admin')
|
||||
if (admins.length <= 1) {
|
||||
return NextResponse.json({ error: 'Cannot delete the last admin account' }, { status: 409 })
|
||||
}
|
||||
}
|
||||
|
||||
deleteUser(id)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
}
|
||||
10
src/app/api/users/route.ts
Normal file
10
src/app/api/users/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
import { listUsers } from '@/lib/users'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
return NextResponse.json(listUsers())
|
||||
}
|
||||
Reference in New Issue
Block a user