date3k2 commited on
Commit
d2726bc
1 Parent(s): 0c8caac

Add new files and models

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +12 -0
  2. app/__init__.py +1 -0
  3. app/__pycache__/__init__.cpython-312.pyc +0 -0
  4. app/__pycache__/main.cpython-312.pyc +0 -0
  5. app/crud/__init__.py +0 -0
  6. app/crud/__pycache__/__init__.cpython-312.pyc +0 -0
  7. app/crud/__pycache__/song.cpython-312.pyc +0 -0
  8. app/crud/__pycache__/user.cpython-312.pyc +0 -0
  9. app/crud/user.py +25 -0
  10. app/db/__init__.py +1 -0
  11. app/db/__pycache__/__init__.cpython-312.pyc +0 -0
  12. app/db/__pycache__/supabase_service.cpython-312.pyc +0 -0
  13. app/db/supabase_service.py +13 -0
  14. app/main.py +26 -0
  15. app/models/__init__.py +1 -0
  16. app/models/__pycache__/__init__.cpython-312.pyc +0 -0
  17. app/models/__pycache__/concert.cpython-312.pyc +0 -0
  18. app/models/__pycache__/enums.cpython-312.pyc +0 -0
  19. app/models/__pycache__/group.cpython-312.pyc +0 -0
  20. app/models/__pycache__/music.cpython-312.pyc +0 -0
  21. app/models/__pycache__/song.cpython-312.pyc +0 -0
  22. app/models/__pycache__/user.cpython-312.pyc +0 -0
  23. app/models/concert.py +15 -0
  24. app/models/enums.py +13 -0
  25. app/models/group.py +16 -0
  26. app/models/music.py +30 -0
  27. app/models/user.py +28 -0
  28. app/requirements.txt +0 -0
  29. app/routers/__init__.py +1 -0
  30. app/routers/__pycache__/__init__.cpython-312.pyc +0 -0
  31. app/routers/__pycache__/album.cpython-312.pyc +0 -0
  32. app/routers/__pycache__/authentication.cpython-312.pyc +0 -0
  33. app/routers/__pycache__/concert.cpython-312.pyc +0 -0
  34. app/routers/__pycache__/group_admin.cpython-312.pyc +0 -0
  35. app/routers/__pycache__/playlist.cpython-312.pyc +0 -0
  36. app/routers/__pycache__/song.cpython-312.pyc +0 -0
  37. app/routers/__pycache__/users.cpython-312.pyc +0 -0
  38. app/routers/album.py +106 -0
  39. app/routers/authentication.py +114 -0
  40. app/routers/concert.py +69 -0
  41. app/routers/group_admin.py +107 -0
  42. app/routers/playlist.py +104 -0
  43. app/routers/song.py +129 -0
  44. app/routers/users.py +134 -0
  45. app/tests/__init__.py +1 -0
  46. app/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  47. app/tests/__pycache__/test_auth.cpython-312-pytest-7.4.3.pyc +0 -0
  48. app/tests/test_auth.py +2 -0
  49. app/utils/__init__.py +0 -0
  50. app/utils/__pycache__/__init__.cpython-312.pyc +0 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11.7-bookworm
2
+
3
+ WORKDIR /app
4
+
5
+ COPY ./app/ .
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
8
+
9
+ ENV SUPABASE_URL='https://lakewjtjnwmihctodrow.supabase.co'
10
+ ENV SUPABASE_SECRET_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imxha2V3anRqbndtaWhjdG9kcm93Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTg0MTgwMTMsImV4cCI6MjAxMzk5NDAxM30.6UI7vAzZXZFoSXPaJ2v8LQYpHmzAAuBVykWXV8lMLEA'
11
+ EXPOSE 5000
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is intentionally left blank
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (140 Bytes). View file
 
app/__pycache__/main.cpython-312.pyc ADDED
Binary file (1.41 kB). View file
 
app/crud/__init__.py ADDED
File without changes
app/crud/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (145 Bytes). View file
 
app/crud/__pycache__/song.cpython-312.pyc ADDED
Binary file (1.05 kB). View file
 
app/crud/__pycache__/user.cpython-312.pyc ADDED
Binary file (1.13 kB). View file
 
app/crud/user.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import Client
2
+ from utils.auth import get_user_response
3
+ from models.user import User
4
+
5
+
6
+ async def update_user(
7
+ supabase: Client, email: str = None, password: str = None, data: dict = None
8
+ ):
9
+ info = {}
10
+ if email:
11
+ info["email"] = email
12
+ if password:
13
+ info["password"] = password
14
+ if data:
15
+ info["data"] = data
16
+ supabase.auth.update_user(info)
17
+ return True
18
+
19
+
20
+ async def get_user(supabase: Client, token: str):
21
+ user_response = get_user_response(token, supabase)
22
+ if user_response:
23
+ return User(email=user_response.user.email, **user_response.user.user_metadata)
24
+ else:
25
+ return None
app/db/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is intentionally left blank
app/db/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (143 Bytes). View file
 
app/db/__pycache__/supabase_service.cpython-312.pyc ADDED
Binary file (598 Bytes). View file
 
app/db/supabase_service.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client
2
+ from dotenv import load_dotenv
3
+ import os
4
+
5
+ load_dotenv()
6
+
7
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
8
+ SUPABASE_SECRET_KEY = os.getenv("SUPABASE_SECRET_KEY")
9
+ SUPABASE = create_client(SUPABASE_URL, SUPABASE_SECRET_KEY)
10
+
11
+
12
+ def get_supabase():
13
+ return SUPABASE
app/main.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.responses import RedirectResponse
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from routers import authentication, users, group_admin, song, album, concert, playlist
5
+
6
+ app = FastAPI()
7
+
8
+ app.include_router(authentication.router)
9
+ app.include_router(users.router)
10
+ app.include_router(group_admin.router)
11
+ app.include_router(song.router)
12
+ app.include_router(album.router)
13
+ app.include_router(concert.router)
14
+ app.include_router(playlist.router)
15
+ app.add_middleware(
16
+ CORSMiddleware,
17
+ allow_origins=["*"],
18
+ allow_credentials=True,
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+
24
+ @app.get("/")
25
+ def read_root():
26
+ return RedirectResponse(url="/docs")
app/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is intentionally left blank
app/models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (147 Bytes). View file
 
app/models/__pycache__/concert.cpython-312.pyc ADDED
Binary file (814 Bytes). View file
 
app/models/__pycache__/enums.cpython-312.pyc ADDED
Binary file (641 Bytes). View file
 
app/models/__pycache__/group.cpython-312.pyc ADDED
Binary file (937 Bytes). View file
 
app/models/__pycache__/music.cpython-312.pyc ADDED
Binary file (1.35 kB). View file
 
app/models/__pycache__/song.cpython-312.pyc ADDED
Binary file (583 Bytes). View file
 
app/models/__pycache__/user.cpython-312.pyc ADDED
Binary file (1.62 kB). View file
 
app/models/concert.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ConcertInfo(BaseModel):
5
+ name: str
6
+ location: str
7
+ description: str | None = None
8
+ start_at: str
9
+ number_of_tickets: int = 0
10
+
11
+
12
+ class Concert(ConcertInfo):
13
+ id: int
14
+ created_at: str
15
+ group_id: int
app/models/enums.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Role(str, Enum):
5
+ user = "user"
6
+ admin = "admin"
7
+ group_admin = "group_admin"
8
+
9
+
10
+ class Gender(Enum):
11
+ male = 0
12
+ female = 1
13
+ other = 2
app/models/group.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class GroupBase(BaseModel):
5
+ id: int
6
+ created_at: str
7
+
8
+
9
+ class GroupInfo(BaseModel):
10
+ name: str
11
+ description: str | None = None
12
+
13
+
14
+ class Group(GroupBase, GroupInfo):
15
+ admin_id: str
16
+ members_id: list[str] | None = None
app/models/music.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Song(BaseModel):
5
+ id: int
6
+ name: str
7
+ created_at: str
8
+ user_id: str
9
+ name_in_storage: str
10
+ description: str | None
11
+ lyric: str | None
12
+
13
+
14
+ class BaseAlbum(BaseModel):
15
+ name: str
16
+ description: str | None
17
+
18
+
19
+ class Album(BaseAlbum):
20
+ id: int
21
+ created_at: str
22
+ user_id: str
23
+
24
+ class BasePlaylist(BaseModel):
25
+ name: str
26
+
27
+ class Playlist(BasePlaylist):
28
+ id: int
29
+ created_at: str
30
+ user_id: str
app/models/user.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+ from datetime import date
4
+ from .enums import Gender, Role
5
+
6
+
7
+ class UserBase(BaseModel):
8
+ email: str = Field(description="Email of user", example="datvip@gmail.com")
9
+
10
+
11
+ class UserLogin(UserBase):
12
+ password: str
13
+
14
+
15
+ class UserInfo(BaseModel):
16
+ name: str
17
+ birthdate: date | None = Field(example="2000-01-01")
18
+ phone_number: str | None = Field(example="0382929292")
19
+ gender: Gender
20
+ role: Role = Role.user
21
+
22
+
23
+ class UserSignup(UserLogin, UserInfo):
24
+ pass
25
+
26
+
27
+ class User(UserBase, UserInfo):
28
+ pass
app/requirements.txt ADDED
Binary file (1.5 kB). View file
 
app/routers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is intentionally left blank
app/routers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (148 Bytes). View file
 
app/routers/__pycache__/album.cpython-312.pyc ADDED
Binary file (4.96 kB). View file
 
app/routers/__pycache__/authentication.cpython-312.pyc ADDED
Binary file (4.85 kB). View file
 
app/routers/__pycache__/concert.cpython-312.pyc ADDED
Binary file (3.35 kB). View file
 
app/routers/__pycache__/group_admin.cpython-312.pyc ADDED
Binary file (4.87 kB). View file
 
app/routers/__pycache__/playlist.cpython-312.pyc ADDED
Binary file (4.81 kB). View file
 
app/routers/__pycache__/song.cpython-312.pyc ADDED
Binary file (5.72 kB). View file
 
app/routers/__pycache__/users.cpython-312.pyc ADDED
Binary file (6.08 kB). View file
 
app/routers/album.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ HTTPException,
5
+ status,
6
+ Security,
7
+ )
8
+ from models.music import Album, BaseAlbum
9
+ from db.supabase_service import get_supabase
10
+ from typing import Annotated, List, Tuple
11
+ from supabase import Client
12
+ from utils.auth import get_id
13
+ from fastapi.encoders import jsonable_encoder
14
+ from utils.exceptions import BAD_REQUEST
15
+
16
+ router = APIRouter(tags=["Album"], prefix="/album")
17
+
18
+
19
+ @router.post("", description="Create album")
20
+ async def create_album(
21
+ supabase: Annotated[Client, Depends(get_supabase)],
22
+ user_id: Annotated[str, Security(get_id)],
23
+ album: BaseAlbum,
24
+ ):
25
+ try:
26
+ supabase.table("albums").insert(
27
+ {"name": album.name, "description": album.description, "user_id": user_id}
28
+ ).execute()
29
+ return {"detail": "Album created"}
30
+ except:
31
+ raise BAD_REQUEST
32
+
33
+
34
+ # delete album
35
+ @router.delete("", description="Delete album")
36
+ async def delete_album(
37
+ supabase: Annotated[Client, Depends(get_supabase)],
38
+ user_id: Annotated[str, Security(get_id)],
39
+ album_id: int,
40
+ ):
41
+ try:
42
+ supabase.table("albums").delete().eq("id", album_id).execute()
43
+ return {"detail": "Album deleted"}
44
+ except:
45
+ raise BAD_REQUEST
46
+
47
+
48
+ # get list of all album
49
+ @router.get("/all", description="Get list of all album", response_model=List[Album])
50
+ async def get_all_album(
51
+ supabase: Annotated[Client, Depends(get_supabase)],
52
+ user_id: Annotated[str, Security(get_id)],
53
+ ) -> List[Album]:
54
+ try:
55
+ res = supabase.table("albums").select("*").execute().dict()["data"]
56
+ return res
57
+ except:
58
+ raise BAD_REQUEST
59
+
60
+
61
+ # modify album
62
+ @router.patch("", description="Modify album")
63
+ async def modify_album(
64
+ supabase: Annotated[Client, Depends(get_supabase)],
65
+ user_id: Annotated[str, Security(get_id)],
66
+ album: Album,
67
+ songs: List[int],
68
+ ):
69
+ try:
70
+ if album.user_id != user_id:
71
+ raise HTTPException(
72
+ status_code=status.HTTP_400_BAD_REQUEST,
73
+ detail="You are not the owner of this album",
74
+ )
75
+ supabase.table("albums").update(
76
+ jsonable_encoder(album, exclude={"id", "user_id", "created_at"})
77
+ ).eq("id", album.id).execute()
78
+ supabase.table("songs").update({"album_id": None}).eq(
79
+ "album_id", album.id
80
+ ).execute()
81
+ supabase.table("songs").update({"album_id": album.id}).in_(
82
+ "id", songs
83
+ ).execute()
84
+ return {"detail": "Album modified"}
85
+ except:
86
+ raise BAD_REQUEST
87
+
88
+
89
+ # get album and its songs
90
+ @router.get("", description="Get album and its songs")
91
+ async def get_album(
92
+ supabase: Annotated[Client, Depends(get_supabase)],
93
+ user_id: Annotated[str, Security(get_id)],
94
+ album_id: int,
95
+ ):
96
+ try:
97
+ res = (
98
+ supabase.table("albums")
99
+ .select("*, songs(*)")
100
+ .eq("id", album_id)
101
+ .execute()
102
+ .dict()["data"][0]
103
+ )
104
+ return res
105
+ except:
106
+ raise BAD_REQUEST
app/routers/authentication.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Security, Body, Query
2
+ from fastapi.security import OAuth2PasswordRequestForm
3
+ from models.user import UserSignup
4
+ from db.supabase_service import get_supabase
5
+ from typing import Annotated
6
+ from supabase import Client
7
+ from utils.auth import get_id
8
+ from fastapi.encoders import jsonable_encoder
9
+ from models.enums import Role
10
+ from gotrue.errors import AuthApiError
11
+ from crud.user import update_user
12
+ from utils.exceptions import BAD_REQUEST, CONFLICT, NOT_FOUND
13
+
14
+ router = APIRouter(tags=["Authentication"])
15
+
16
+
17
+ @router.post("/login", description="Login user using email and password")
18
+ async def login(
19
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
20
+ supabase: Annotated[Client, Depends(get_supabase)],
21
+ ):
22
+ email = form_data.username
23
+ password = form_data.password
24
+ try:
25
+ user = supabase.auth.sign_in_with_password(
26
+ {"email": email, "password": password}
27
+ )
28
+ return {
29
+ "access_token": user.session.access_token,
30
+ "token_type": "bearer",
31
+ }
32
+ except:
33
+ raise HTTPException(
34
+ status_code=status.HTTP_400_BAD_REQUEST,
35
+ detail="Incorrect email or password",
36
+ )
37
+
38
+
39
+ @router.post(
40
+ "/signup", description="Sign up new user", status_code=status.HTTP_201_CREATED
41
+ )
42
+ async def signup(
43
+ user: Annotated[UserSignup, Body()],
44
+ supabase: Annotated[Client, Depends(get_supabase)],
45
+ ):
46
+ email = user.email
47
+ password = user.password
48
+ if len(password) < 6:
49
+ raise HTTPException(
50
+ status_code=status.HTTP_400_BAD_REQUEST,
51
+ detail="Password should be at least 6 characters",
52
+ )
53
+ try:
54
+ user.role = Role.user
55
+ res = supabase.auth.sign_up(
56
+ {
57
+ "email": email,
58
+ "password": password,
59
+ "options": {
60
+ "data": jsonable_encoder(user, exclude=("password", "email"))
61
+ },
62
+ }
63
+ )
64
+ return {"detail": "User created"}
65
+ except AuthApiError:
66
+ raise CONFLICT
67
+ except:
68
+ raise BAD_REQUEST
69
+
70
+
71
+ @router.post("/reset_password", description="Send reset password email")
72
+ async def reset_password(
73
+ supabase: Annotated[Client, Depends(get_supabase)],
74
+ email: str,
75
+ ):
76
+ try:
77
+ if (
78
+ len(supabase.rpc("get_user_id_by_email", {"email": email}).execute().data)
79
+ > 0
80
+ ):
81
+ supabase.auth.reset_password_email(email)
82
+ return {"detail": "Reset password email sent"}
83
+ else:
84
+ raise NOT_FOUND
85
+ except:
86
+ raise NOT_FOUND
87
+
88
+
89
+ @router.post("/reset_password_confirm", description="Reset password")
90
+ async def reset_password_confirm(
91
+ supabase: Annotated[Client, Depends(get_supabase)],
92
+ email: Annotated[str, Query(description="Email", title="Email")],
93
+ new_password: Annotated[
94
+ str, Query(min_length=6, description="New password", title="New password")
95
+ ],
96
+ token: Annotated[
97
+ str, Query(description="Reset password token", title="Reset password token")
98
+ ],
99
+ ):
100
+ try:
101
+ supabase.auth.verify_otp(
102
+ {
103
+ "email": email,
104
+ "token": token,
105
+ "type": "email",
106
+ }
107
+ )
108
+ await update_user(supabase, password=new_password)
109
+ return {"detail": "Your password has been reset"}
110
+ except:
111
+ raise HTTPException(
112
+ status_code=status.HTTP_400_BAD_REQUEST,
113
+ detail="Incorrect token",
114
+ )
app/routers/concert.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Security
2
+ from typing import Annotated
3
+ from fastapi.encoders import jsonable_encoder
4
+ from supabase import Client
5
+ from db.supabase_service import get_supabase
6
+ from typing import Annotated
7
+ from supabase import Client
8
+ from utils.auth import get_id
9
+ from utils.exceptions import BAD_REQUEST
10
+ from models.concert import ConcertInfo
11
+
12
+ router = APIRouter(tags=["Concert"], prefix="/concert")
13
+
14
+
15
+ # create concert
16
+ @router.post("", description="Create concert")
17
+ async def create_concert(
18
+ supabase: Annotated[Client, Depends(get_supabase)],
19
+ user_id: Annotated[str, Security(get_id)],
20
+ concert_info: ConcertInfo,
21
+ ):
22
+ try:
23
+ supabase.table("concerts").insert(jsonable_encoder(concert_info)).execute()
24
+ return {"detail": "Concert created"}
25
+ except:
26
+ raise BAD_REQUEST
27
+
28
+
29
+ # get list of all concert
30
+ @router.get("/all", description="Get list of all concert")
31
+ async def get_all_concert(
32
+ supabase: Annotated[Client, Depends(get_supabase)],
33
+ user_id: Annotated[str, Security(get_id)],
34
+ ):
35
+ try:
36
+ res = supabase.table("concerts").select("*").execute().dict()["data"]
37
+ return res
38
+ except:
39
+ raise BAD_REQUEST
40
+
41
+
42
+ # modify concert
43
+ @router.put("", description="Modify concert")
44
+ async def modify_concert(
45
+ supabase: Annotated[Client, Depends(get_supabase)],
46
+ user_id: Annotated[str, Security(get_id)],
47
+ concert_info: ConcertInfo,
48
+ ):
49
+ try:
50
+ supabase.table("concerts").update(jsonable_encoder(concert_info)).eq(
51
+ "id", concert_info.id
52
+ ).execute()
53
+ return {"detail": "Concert modified"}
54
+ except:
55
+ raise BAD_REQUEST
56
+
57
+
58
+ # delete concert
59
+ @router.delete("", description="Delete concert")
60
+ async def delete_concert(
61
+ supabase: Annotated[Client, Depends(get_supabase)],
62
+ user_id: Annotated[str, Security(get_id)],
63
+ concert_id: int,
64
+ ):
65
+ try:
66
+ supabase.table("concerts").delete().eq("id", concert_id).execute()
67
+ return {"detail": "Concert deleted"}
68
+ except:
69
+ raise BAD_REQUEST
app/routers/group_admin.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Security
2
+ from db.supabase_service import get_supabase
3
+ from typing import Annotated
4
+ from supabase import Client
5
+ from utils.auth import get_role, get_id, oauth2_scheme
6
+ from models.enums import Role
7
+ from models.group import GroupInfo
8
+ from utils.exceptions import UNAUTHORIZED
9
+ from typing import List
10
+
11
+ router = APIRouter(tags=["Group"], prefix="/group")
12
+
13
+
14
+ @router.post("", description="Create group")
15
+ async def create_group(
16
+ supabase: Annotated[Client, Depends(get_supabase)],
17
+ role: Annotated[str, Security(get_role)],
18
+ id: Annotated[str, Security(get_id)],
19
+ group_info: GroupInfo,
20
+ ):
21
+ if role != Role.group_admin:
22
+ raise UNAUTHORIZED
23
+ supabase.table("groups").insert(
24
+ {
25
+ "name": group_info.name,
26
+ "description": group_info.description,
27
+ "admin_id": id,
28
+ }
29
+ ).execute()
30
+ return {"detail": "Group created"}
31
+
32
+
33
+ @router.post("/user", description="Modify group")
34
+ async def add_user(
35
+ token: Annotated[str, Depends(oauth2_scheme)],
36
+ supabase: Annotated[Client, Depends(get_supabase)],
37
+ role: Annotated[str, Security(get_role)],
38
+ id: Annotated[str, Security(get_id)],
39
+ email: str,
40
+ group_id: int,
41
+ ):
42
+ if role != Role.group_admin:
43
+ raise UNAUTHORIZED
44
+ user_id = supabase.rpc("get_user_id_by_email", {"email": email}).execute().data
45
+ if len(user_id) == 0:
46
+ raise HTTPException(
47
+ status_code=status.HTTP_400_BAD_REQUEST,
48
+ detail="User does not exist",
49
+ )
50
+ user_id = user_id[0]["id"]
51
+ members_id: List = (
52
+ supabase.table("groups")
53
+ .select("members_id")
54
+ .match({"admin_id": id, "id": group_id})
55
+ .execute()
56
+ .data[0]["members_id"]
57
+ )
58
+ if not members_id:
59
+ members_id = []
60
+ if user_id in members_id:
61
+ raise HTTPException(
62
+ status_code=status.HTTP_400_BAD_REQUEST,
63
+ detail="User already in group",
64
+ )
65
+ members_id.append(user_id)
66
+ supabase.table("groups").update({"members_id": members_id}).match(
67
+ {"admin_id": id, "id": group_id}
68
+ ).execute()
69
+ return {"detail": "User added to group"}
70
+
71
+
72
+ @router.delete("/user", description="Delete user from group")
73
+ async def delete_user(
74
+ supabase: Annotated[Client, Depends(get_supabase)],
75
+ role: Annotated[str, Security(get_role)],
76
+ id: Annotated[str, Security(get_id)],
77
+ group_id: int,
78
+ email: str,
79
+ ):
80
+ if role != Role.group_admin:
81
+ raise UNAUTHORIZED
82
+ user_id = supabase.rpc("get_user_id_by_email", {"email": email}).execute().data
83
+ if len(user_id) == 0:
84
+ raise HTTPException(
85
+ status_code=status.HTTP_400_BAD_REQUEST,
86
+ detail="User does not exist",
87
+ )
88
+ user_id = user_id[0]["id"]
89
+ members_id: List = (
90
+ supabase.table("groups")
91
+ .select("members_id")
92
+ .match({"admin_id": id, "id": group_id})
93
+ .execute()
94
+ .data[0]["members_id"]
95
+ )
96
+ if not members_id:
97
+ members_id = []
98
+ if user_id not in members_id:
99
+ raise HTTPException(
100
+ status_code=status.HTTP_400_BAD_REQUEST,
101
+ detail="User not in group",
102
+ )
103
+ members_id.remove(user_id)
104
+ supabase.table("groups").update({"members_id": members_id}).match(
105
+ {"admin_id": id, "id": group_id}
106
+ ).execute()
107
+ return {"detail": "User deleted from group"}
app/routers/playlist.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ HTTPException,
5
+ status,
6
+ Security,
7
+ )
8
+ from models.music import Playlist, BasePlaylist
9
+ from db.supabase_service import get_supabase
10
+ from typing import Annotated, List
11
+ from supabase import Client
12
+ from utils.auth import get_id
13
+ from fastapi.encoders import jsonable_encoder
14
+ from utils.exceptions import BAD_REQUEST
15
+
16
+ router = APIRouter(tags=["Playlist"], prefix="/playlist")
17
+
18
+
19
+ @router.post("", description="Create playlist")
20
+ async def create_playlist(
21
+ supabase: Annotated[Client, Depends(get_supabase)],
22
+ user_id: Annotated[str, Security(get_id)],
23
+ playlist: BasePlaylist,
24
+ ):
25
+ try:
26
+ supabase.table("playlist").insert(
27
+ {"name": playlist.name, "song_id" : [], "user_id": user_id}
28
+ ).execute()
29
+ return {"detail": "Playlist created"}
30
+ except:
31
+ raise BAD_REQUEST
32
+
33
+
34
+ @router.delete("", description="Delete playlist")
35
+ async def delete_playlist(
36
+ supabase: Annotated[Client, Depends(get_supabase)],
37
+ user_id: Annotated[str, Security(get_id)],
38
+ playlist_id: int,
39
+ ):
40
+ try:
41
+ supabase.table("playlist").delete().eq("id", playlist_id).execute()
42
+ return {"detail": "Album deleted"}
43
+ except:
44
+ raise BAD_REQUEST
45
+
46
+ # get list of all your playlist
47
+ @router.get("/all", description="Get list of your playlist", response_model=List[Playlist])
48
+ async def get_all_album(
49
+ supabase: Annotated[Client, Depends(get_supabase)],
50
+ user_id: Annotated[str, Security(get_id)],
51
+ ) -> List[Playlist]:
52
+ try:
53
+
54
+ res = supabase.table("playlist").select("*").match({"user_id" : user_id}).execute().data
55
+ return res
56
+ except:
57
+ raise BAD_REQUEST
58
+
59
+ # modify playlist
60
+ @router.patch("", description="Modify playlist")
61
+ async def modify_playlist(
62
+ supabase: Annotated[Client, Depends(get_supabase)],
63
+ user_id: Annotated[str, Security(get_id)],
64
+ playlist: Playlist,
65
+ songs: List[int],
66
+ ):
67
+
68
+ if playlist.user_id != user_id:
69
+ raise HTTPException(
70
+ status_code=status.HTTP_400_BAD_REQUEST,
71
+ detail="You are not the owner of this playlist",
72
+ )
73
+ try:
74
+ supabase.table("playlist").update(
75
+ jsonable_encoder(playlist, exclude={"id", "user_id", "created_at"})
76
+ ).eq("id", playlist.id).execute()
77
+
78
+ supabase.table("playlist").update({ "song_id": songs }).eq("id", playlist.id).execute()
79
+
80
+ return {"detail": "Playlist modified"}
81
+ except:
82
+ raise BAD_REQUEST
83
+
84
+
85
+
86
+
87
+ # get playlist and its songs
88
+ @router.get("/{playlist_id}", description="Get playlist and its songs")
89
+ async def get_playlist(
90
+ supabase: Annotated[Client, Depends(get_supabase)],
91
+ user_id: Annotated[str, Security(get_id)],
92
+ playlist_id: int,
93
+ ):
94
+ try:
95
+ res = (
96
+ supabase.table("playlist")
97
+ .select("*")
98
+ .eq("id", playlist_id)
99
+ .execute()
100
+ .dict()["data"]
101
+ )
102
+ return res
103
+ except:
104
+ raise BAD_REQUEST
app/routers/song.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ APIRouter,
3
+ Depends,
4
+ HTTPException,
5
+ UploadFile,
6
+ status,
7
+ Security,
8
+ )
9
+ from models.music import Song
10
+ from db.supabase_service import get_supabase
11
+ from typing import Annotated, List, Tuple
12
+ from supabase import Client
13
+ from utils.auth import get_id
14
+ from fastapi.encoders import jsonable_encoder
15
+ from utils.exceptions import BAD_REQUEST
16
+
17
+ router = APIRouter(tags=["Song"], prefix="/song")
18
+
19
+
20
+ # upload mp3 file to supabase storage
21
+ @router.post("", description="Upload music file")
22
+ async def upload_music(
23
+ supabase: Annotated[Client, Depends(get_supabase)],
24
+ id: Annotated[str, Security(get_id)],
25
+ music: UploadFile,
26
+ name: str | None = None,
27
+ ):
28
+ if music.content_type != "audio/mpeg":
29
+ raise HTTPException(
30
+ status_code=status.HTTP_400_BAD_REQUEST,
31
+ detail="Only mp3 file is accepted",
32
+ )
33
+ try:
34
+ if not name:
35
+ name = music.filename
36
+ f = await music.read()
37
+ supabase.storage.from_("songs").upload(
38
+ path=music.filename, file=f, file_options={"content-type": "audio/mpeg"}
39
+ )
40
+ supabase.table("songs").insert(
41
+ {"name": name, "name_in_storage": music.filename, "user_id": id}
42
+ ).execute()
43
+ return {"detail": "Music uploaded"}
44
+ except:
45
+ raise BAD_REQUEST
46
+
47
+
48
+ # get url of music file from supabase storage
49
+ @router.get("/link", description="Get url of music file")
50
+ async def get_link_to_music(
51
+ supabase: Annotated[Client, Depends(get_supabase)],
52
+ id: Annotated[str, Security(get_id)],
53
+ name_in_storage: str,
54
+ ):
55
+ try:
56
+ res = supabase.storage.from_("songs").create_signed_url(
57
+ path=name_in_storage, expires_in=3600
58
+ )
59
+ return res
60
+ except:
61
+ raise BAD_REQUEST
62
+
63
+
64
+ # get list of all song from search
65
+ @router.get(
66
+ "/search", description="Get list of all music", response_model=List[Song]
67
+ )
68
+ async def get_music(
69
+ supabase: Annotated[Client, Depends(get_supabase)],
70
+ id: Annotated[str, Security(get_id)],
71
+ query: str | None = None,
72
+ ) -> List[Song]:
73
+ try:
74
+ if query:
75
+ res = (
76
+ supabase.table("songs")
77
+ .select("*")
78
+ .ilike("name", f"%{query}%")
79
+ .execute()
80
+ .dict()["data"]
81
+ )
82
+ else:
83
+ res = supabase.table("songs").select("*").execute().dict()["data"]
84
+ return res
85
+ except:
86
+ raise BAD_REQUEST
87
+
88
+
89
+ # delete song
90
+ @router.delete("", description="Delete music")
91
+ async def delete_music(
92
+ supabase: Annotated[Client, Depends(get_supabase)],
93
+ user_id: Annotated[str, Security(get_id)],
94
+ song: Song,
95
+ ):
96
+ try:
97
+ if song.user_id != user_id:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_400_BAD_REQUEST,
100
+ detail="You are not the owner of this music",
101
+ )
102
+ supabase.storage.from_("songs").remove([song.name_in_storage])
103
+ supabase.table("songs").delete().eq("id", song.id).execute()
104
+ return {"detail": "Music deleted"}
105
+ except:
106
+ raise
107
+
108
+
109
+ # change music info
110
+ @router.patch("/info", description="Change music info")
111
+ async def change_music_info(
112
+ supabase: Annotated[Client, Depends(get_supabase)],
113
+ user_id: Annotated[str, Security(get_id)],
114
+ song: Song,
115
+ ):
116
+ try:
117
+ if song.user_id != user_id:
118
+ raise HTTPException(
119
+ status_code=status.HTTP_400_BAD_REQUEST,
120
+ detail="You are not the owner of this music",
121
+ )
122
+ supabase.table("songs").update(
123
+ jsonable_encoder(
124
+ song, exclude={"id", "user_id", "created_at", "name_in_storage"}
125
+ )
126
+ ).eq("id", song.id).execute()
127
+ return {"detail": "Music info updated"}
128
+ except:
129
+ raise BAD_REQUEST
app/routers/users.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Security, Body, Query
2
+ from db.supabase_service import get_supabase
3
+ from typing import Annotated
4
+ from supabase import Client
5
+ from utils.auth import get_role, get_id, oauth2_scheme
6
+ from models.enums import Role
7
+ from models.user import UserInfo
8
+ from models.group import GroupInfo
9
+ from fastapi.encoders import jsonable_encoder
10
+ from crud.user import get_user, update_user
11
+ from utils.exceptions import BAD_REQUEST
12
+
13
+ router = APIRouter(tags=["User"], prefix="/user")
14
+
15
+
16
+ @router.get("/info", description="Get information of logged in user")
17
+ async def get_user_info(
18
+ token: Annotated[str, Depends(oauth2_scheme)],
19
+ supabase: Annotated[Client, Depends(get_supabase)],
20
+ ):
21
+ return await get_user(supabase, token)
22
+
23
+
24
+ @router.post(
25
+ "/signup_group_admin",
26
+ description="Sign up to be a group admin",
27
+ status_code=status.HTTP_201_CREATED,
28
+ )
29
+ async def signup_as_group_admin(
30
+ supabase: Annotated[Client, Depends(get_supabase)],
31
+ role: Annotated[str, Security(get_role)],
32
+ id: Annotated[str, Security(get_id)],
33
+ group_info: GroupInfo,
34
+ ):
35
+ if role != Role.user:
36
+ raise HTTPException(
37
+ status_code=status.HTTP_400_BAD_REQUEST,
38
+ detail="You are already a group admin or an admin",
39
+ )
40
+ try:
41
+ supabase.auth.update_user({"data": {"role": Role.group_admin}})
42
+ supabase.table("groups").insert(
43
+ {
44
+ "name": group_info.name,
45
+ "description": group_info.description,
46
+ "admin_id": id,
47
+ }
48
+ ).execute()
49
+ return {"detail": "You are now a group admin"}
50
+ except:
51
+ raise BAD_REQUEST
52
+
53
+
54
+ @router.put("/password", description="Change password of logged in user")
55
+ async def change_password(
56
+ supabase: Annotated[Client, Depends(get_supabase)],
57
+ id: Annotated[str, Security(get_id)],
58
+ new_password: Annotated[
59
+ str, Query(min_length=6, description="New password", title="New password")
60
+ ],
61
+ ):
62
+ try:
63
+ await update_user(supabase, password=new_password)
64
+ return {"detail": "Password updated"}
65
+ except:
66
+ raise
67
+
68
+
69
+ @router.patch("/info", description="Update user info")
70
+ async def update_user_info(
71
+ supabase: Annotated[Client, Depends(get_supabase)],
72
+ id: Annotated[str, Security(get_id)],
73
+ new_user_info: Annotated[UserInfo, Body()],
74
+ ):
75
+ try:
76
+ if new_user_info.role:
77
+ del new_user_info.role
78
+ await update_user(supabase, data=jsonable_encoder(new_user_info))
79
+ return {"detail": "User info updated"}
80
+ except:
81
+ raise BAD_REQUEST
82
+
83
+
84
+ # buy ticket
85
+ @router.post("/buy_ticket", description="Buy ticket")
86
+ async def buy_ticket(
87
+ supabase: Annotated[Client, Depends(get_supabase)],
88
+ user_id: Annotated[str, Security(get_id)],
89
+ concert_id: int,
90
+ ):
91
+ try:
92
+ number_of_tickets = (
93
+ supabase.table("concerts")
94
+ .select("number_of_tickets")
95
+ .eq("id", concert_id)
96
+ .execute()
97
+ .dict()["data"][0]["number_of_tickets"]
98
+ )
99
+ if number_of_tickets == 0:
100
+ raise HTTPException(
101
+ status_code=status.HTTP_400_BAD_REQUEST,
102
+ detail="There are no tickets left",
103
+ )
104
+ return {"detail": "Please navigate to the payment page"}
105
+ except:
106
+ raise BAD_REQUEST
107
+
108
+
109
+ # Confirm payment
110
+ @router.post("/confirm_payment", description="Confirm payment")
111
+ async def confirm_payment(
112
+ supabase: Annotated[Client, Depends(get_supabase)],
113
+ user_id: Annotated[str, Security(get_id)],
114
+ concert_id: int,
115
+ ):
116
+ try:
117
+ number_of_tickets = (
118
+ supabase.table("concerts")
119
+ .select("number_of_tickets")
120
+ .eq("id", concert_id)
121
+ .execute()
122
+ .dict()["data"][0]["number_of_tickets"]
123
+ )
124
+ if number_of_tickets == 0:
125
+ raise HTTPException(
126
+ status_code=status.HTTP_400_BAD_REQUEST,
127
+ detail="There are no tickets left",
128
+ )
129
+ supabase.table("concerts").update(
130
+ {"number_of_tickets": number_of_tickets - 1}
131
+ ).eq("id", concert_id).execute()
132
+ return {"detail": "Payment successful"}
133
+ except:
134
+ raise BAD_REQUEST
app/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file is intentionally left blank.
app/tests/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (146 Bytes). View file
 
app/tests/__pycache__/test_auth.cpython-312-pytest-7.4.3.pyc ADDED
Binary file (335 Bytes). View file
 
app/tests/test_auth.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
app/utils/__init__.py ADDED
File without changes
app/utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (146 Bytes). View file