Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
# App Settings
app_name=
app_env=
app_version=

APP_NAME=musicstreamer
APP_ENV=development
APP_VERSION=1.0.0

# Database Settings
postgres_db=
postgres_user=
postgres_password=
postgres_server=
postgres_port=
POSTGRES_DB=music_stream_secure
POSTGRES_USER=music_admin
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432

# JWT Authentication
jwt_secret_key=
jwt_algorithm=
access_token_expire_minutes=
refresh_token_expire_minutes=
JWT_SECRET_KEY=your_jwt_secret_key_here
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_MINUTES=43200

# Password Security
password_pepper=
PASSWORD_PEPPER=password_pepper_here

# Test User Credentials
TEST_ADMIN_USERNAME=test_admin
[email protected]
TEST_ADMIN_PASSWORD=AdminPass123!
TEST_ADMIN_FIRST_NAME=Test
TEST_ADMIN_LAST_NAME=Admin

TEST_MUSICIAN_USERNAME=test_musician
[email protected]
TEST_MUSICIAN_PASSWORD=MusicianPass123!
TEST_MUSICIAN_FIRST_NAME=Test
TEST_MUSICIAN_LAST_NAME=Musician
TEST_MUSICIAN_STAGE_NAME=Test Musician
TEST_MUSICIAN_BIO=A test musician for development

TEST_LISTENER_USERNAME=test_listener
[email protected]
TEST_LISTENER_PASSWORD=ListenerPass123!
TEST_LISTENER_FIRST_NAME=Test
TEST_LISTENER_LAST_NAME=Listener
2 changes: 1 addition & 1 deletion backend/app/api/v1/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
disable_artist, enable_artist, delete_artist, artist_exists,
get_artist_with_related_entities, get_artists_followed_by_user
)
from app.api.v1.deps import (
from app.core.deps import (
get_current_active_user, get_current_admin, get_current_musician
)

Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/artist_band_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.api.v1.deps import get_db, get_current_musician, get_current_admin
from app.core.deps import get_db, get_current_musician, get_current_admin
from app.crud.artist_band_member import (
create_artist_band_member,
get_artist_band_member_by_id,
Expand Down
6 changes: 5 additions & 1 deletion backend/app/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from app.schemas.user import UserLogin, UserOut
from app.schemas.token import TokenResponse, TokenRefresh
from app.services.auth import AuthService
from app.api.v1.deps import get_current_active_user, get_current_admin, get_auth_service
from app.core.deps import get_current_active_user, get_current_admin, get_auth_service

router = APIRouter()

Expand Down Expand Up @@ -119,6 +119,9 @@ async def get_current_user_info(
"""
return current_user

'''

TODO: use cron job -- refer to issues for assistance

@router.post("/cleanup-expired")
async def cleanup_expired_tokens(
Expand All @@ -136,3 +139,4 @@ async def cleanup_expired_tokens(
"message": f"Cleaned up {cleaned_count} expired tokens",
"tokens_removed": cleaned_count
}
'''
38 changes: 10 additions & 28 deletions backend/app/api/v1/band.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
BandCreate, BandOut, BandUpdate, BandStats, BandWithRelations
)
from app.crud.band import (
create_band, get_band_by_id, get_band_by_name, get_all_bands,
create_band, get_band_by_id, get_all_bands,
get_active_bands, search_bands_by_name, update_band,
disable_band, enable_band, delete_band_permanently, get_band_statistics
)
from app.api.v1.deps import (
from app.core.deps import (
get_current_active_user, get_current_admin, get_current_musician
)

router = APIRouter()
# TODO: add slug later on;
# resource: https://stackoverflow.com/questions/10018100/identify-item-by-either-an-id-or-a-slug-in-a-restful-api
"""
AUTHENTICATION LEVELS:
- None: Public endpoint, no authentication required
Expand Down Expand Up @@ -61,7 +63,7 @@ async def get_bands_public(
db: Session = Depends(get_db),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(20, ge=1, le=100, description="Maximum number of records to return"),
search: Optional[str] = Query(None, min_length=1, description="Search bands by name"),
name: Optional[str] = Query(None, min_length=1, description="Filter bands by name (case-insensitive, partial)"),
active_only: bool = Query(True, description="Return only active bands")
):
"""
Expand All @@ -70,13 +72,13 @@ async def get_bands_public(
Query Parameters:
- skip: Number of records to skip (pagination)
- limit: Maximum number of records to return (pagination)
- search: Search bands by name
- name: Filter bands by name (case-insensitive, partial)
- active_only: Return only active bands (default: True)

Returns: 200 OK - List of bands
"""
if search:
bands = search_bands_by_name(db, search, skip=skip, limit=limit)
if name:
bands = search_bands_by_name(db, name, skip=skip, limit=limit)
elif active_only:
bands = get_active_bands(db, skip=skip, limit=limit)
else:
Expand Down Expand Up @@ -105,28 +107,8 @@ async def get_band_public(
)

return band


@router.get("/name/{name}", response_model=BandOut)
async def get_band_by_name_public(
name: str,
db: Session = Depends(get_db)
):
"""
Get public band profile by name.
Returns basic band information for public viewing.
Only active bands are returned.
Returns: 200 OK - Band profile found
Returns: 404 Not Found - Band not found or inactive
"""
band = get_band_by_name(db, name)
if not band or band.is_disabled:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Band not found"
)

return band
# removed {name} function as it can be fetched alrdy via query param in get_bands_public



@router.get("/me/bands", response_model=List[BandOut])
Expand Down
158 changes: 158 additions & 0 deletions backend/app/api/v1/genre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.deps import get_current_admin, get_current_user_optional
from app.schemas.genre import GenreCreate, GenreUpdate, GenreOut, GenreStats
from app.crud.genre import (
create_genre, genre_exists, get_genre_by_id, get_genre_by_name, get_all_genres,
get_all_active_genres, get_genres_by_fuzzy_name, get_genres_by_partial_name, update_genre, disable_genre, enable_genre,
genre_name_taken, get_genre_statistics,
get_genre_by_name_any, get_genres_by_partial_name_any, get_genres_by_fuzzy_name_any
)

router = APIRouter()
# TODO: add slug later on;
# https://stackoverflow.com/questions/10018100/identify-item-by-either-an-id-or-a-slug-in-a-restful-api

@router.get("/", response_model=List[GenreOut])
async def list_genres(
name: Optional[str] = Query(None, description="Exact genre name to filter"),
q: Optional[str] = Query(None, description="Partial/fuzzy name search"),
db: Session = Depends(get_db),
current_user = Depends(get_current_user_optional)
):
"""List genres with rbac: admins see all; others see active only."""
is_admin = bool(current_user and getattr(current_user, "role", None) == "admin")

if name:
genre = get_genre_by_name_any(db, name) if is_admin else get_genre_by_name(db, name)
if not genre:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found")
return [genre]
if q:
if is_admin:
partial = get_genres_by_partial_name_any(db, q)
if partial:
return partial
return get_genres_by_fuzzy_name_any(db, q)
else:
partial = get_genres_by_partial_name(db, q)
if partial:
return partial
return get_genres_by_fuzzy_name(db, q)
return get_all_genres(db) if is_admin else get_all_active_genres(db)


@router.get("/{genre_id}", response_model=GenreOut)
async def get_genre(genre_id: int, db: Session = Depends(get_db)):
"""Get a specific genre by ID - public access"""
genre = get_genre_by_id(db, genre_id)
if not genre:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found"
)
return genre


@router.post("/", response_model=GenreOut, status_code=status.HTTP_201_CREATED)
async def create_new_genre(
genre_data: GenreCreate,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Create a new genre - admin only"""
created = create_genre(db, genre_data)
if created is None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Genre name already exists")
return created


@router.put("/{genre_id}", response_model=GenreOut)
async def update_genre_endpoint(
genre_id: int,
genre_data: GenreUpdate,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Update a genre - admin only"""
if genre_data.name and genre_name_taken(db, genre_data.name, exclude_genre_id=genre_id):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Genre name already exists"
)

updated_genre = update_genre(db, genre_id, genre_data)
if not updated_genre:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found"
)

return updated_genre


@router.patch("/{genre_id}", response_model=GenreOut)
async def partial_update_genre(
genre_id: int,
genre_data: GenreUpdate,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Partially update a genre - admin only"""
if genre_data.name and genre_name_taken(db, genre_data.name, exclude_genre_id=genre_id):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Genre name already exists"
)

updated_genre = update_genre(db, genre_id, genre_data)
if not updated_genre:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found"
)

return updated_genre


@router.post("/{genre_id}/disable")
async def disable_genre_endpoint(
genre_id: int,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Disable a genre - admin only"""
success = disable_genre(db, genre_id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found")
return {"message": "Genre disabled successfully"}


@router.post("/{genre_id}/enable")
async def enable_genre_endpoint(
genre_id: int,
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Enable a genre - admin only"""
success = enable_genre(db, genre_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Genre not found"
)

return {"message": "Genre enabled successfully"}


@router.get("/statistics", response_model=GenreStats)
async def get_genre_statistics_endpoint(
db: Session = Depends(get_db),
current_admin: dict = Depends(get_current_admin)
):
"""Get genre statistics"""
stats = get_genre_statistics(db)
return GenreStats(**stats)

Loading