diff --git a/backend/auth.py b/backend/auth.py index 8455bb3..2fb0725 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -51,3 +51,11 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session: Session if user is None: raise credentials_exception return user + +async def get_current_superuser(current_user: User = Depends(get_current_user)): + if not current_user.is_superuser: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="The user doesn't have enough privileges" + ) + return current_user diff --git a/backend/routers/admin.py b/backend/routers/admin.py index b651018..f9dd334 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, select, func from pydantic import BaseModel from database import get_session -from models import User, Profile, Show, Song, Venue, Tour, Rating, Comment, Review, Attendance +from models import User, Profile, Show, Song, Venue, Tour, Rating, Comment, Review, Attendance, Artist from dependencies import RoleChecker from auth import get_password_hash @@ -88,11 +88,18 @@ class TourCreate(BaseModel): start_date: Optional[datetime] = None end_date: Optional[datetime] = None + class TourUpdate(BaseModel): name: Optional[str] = None start_date: Optional[datetime] = None end_date: Optional[datetime] = None +class ArtistUpdate(BaseModel): + bio: Optional[str] = None + image_url: Optional[str] = None + notes: Optional[str] = None + + class PlatformStats(BaseModel): total_users: int verified_users: int @@ -434,6 +441,59 @@ def delete_tour( return {"message": "Tour deleted", "tour_id": tour_id} +# ============ ARTISTS ============ + +@router.get("/artists") +def list_artists( + skip: int = 0, + limit: int = 50, + search: Optional[str] = None, + session: Session = Depends(get_session), + _: User = Depends(allow_admin) +): + """List all artists with optional search""" + query = select(Artist) + + if search: + query = query.where(Artist.name.icontains(search)) + + query = query.order_by(Artist.name).offset(skip).limit(limit) + artists = session.exec(query).all() + + return [ + { + "id": a.id, + "name": a.name, + "slug": a.slug, + "bio": a.bio, + "image_url": a.image_url, + "notes": a.notes, + } + for a in artists + ] + + +@router.patch("/artists/{artist_id}") +def update_artist( + artist_id: int, + update: ArtistUpdate, + session: Session = Depends(get_session), + _: User = Depends(allow_admin) +): + """Update artist details (bio, image, notes)""" + artist = session.get(Artist, artist_id) + if not artist: + raise HTTPException(status_code=404, detail="Artist not found") + + for key, value in update.model_dump(exclude_unset=True).items(): + setattr(artist, key, value) + + session.add(artist) + session.commit() + session.refresh(artist) + return artist + + # ============ PERFORMANCES ============ from models import Performance diff --git a/frontend/app/admin/artists/page.tsx b/frontend/app/admin/artists/page.tsx new file mode 100644 index 0000000..caf3e6b --- /dev/null +++ b/frontend/app/admin/artists/page.tsx @@ -0,0 +1,236 @@ +"use client" + +import { useEffect, useState } from "react" +import { useAuth } from "@/contexts/auth-context" +import { useRouter } from "next/navigation" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Search, Edit, Save, X, Mic2 } from "lucide-react" +import { getApiUrl } from "@/lib/api-config" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" + +interface Artist { + id: number + name: string + slug: string + bio: string | null + image_url: string | null + notes: string | null +} + +export default function AdminArtistsPage() { + const { user, token } = useAuth() + const router = useRouter() + const [artists, setArtists] = useState([]) + const [loading, setLoading] = useState(true) + const [search, setSearch] = useState("") + const [editingArtist, setEditingArtist] = useState(null) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!user) { + router.push("/login") + return + } + if (user.role !== "admin") { + router.push("/") + return + } + fetchArtists() + }, [user, router]) + + const fetchArtists = async () => { + if (!token) return + + try { + const res = await fetch(`${getApiUrl()}/admin/artists?limit=100`, { + headers: { Authorization: `Bearer ${token}` } + }) + if (res.ok) setArtists(await res.json()) + } catch (e) { + console.error("Failed to fetch artists", e) + } finally { + setLoading(false) + } + } + + const updateArtist = async () => { + if (!token || !editingArtist) return + setSaving(true) + + try { + const res = await fetch(`${getApiUrl()}/admin/artists/${editingArtist.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + bio: editingArtist.bio, + image_url: editingArtist.image_url, + notes: editingArtist.notes + }) + }) + + if (res.ok) { + fetchArtists() + setEditingArtist(null) + } + } catch (e) { + console.error("Failed to update artist", e) + } finally { + setSaving(false) + } + } + + const filteredArtists = artists.filter(a => + a.name.toLowerCase().includes(search.toLowerCase()) + ) + + if (loading) { + return ( +
+
+
+ {[1, 2, 3].map(i =>
)} +
+
+ ) + } + + return ( +
+
+

+ + Artist Management +

+
+ +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + + + + + + + + + + + + {filteredArtists.map(artist => ( + + + + + + + ))} + +
ArtistBioImageActions
+
+

{artist.name}

+

{artist.slug}

+
+
+ {artist.bio ? ( + Has Bio + ) : ( + No Bio + )} + + {artist.image_url ? ( + Has Image + ) : ( + No Image + )} + + +
+ {filteredArtists.length === 0 && ( +
+ No artists found +
+ )} +
+
+ + setEditingArtist(null)}> + + + Edit Artist: {editingArtist?.name} + + {editingArtist && ( +
+
+ +