added functional groups api + started pictures
This commit is contained in:
@ -7,6 +7,7 @@ services:
|
|||||||
access_token_expiration_time: 10080
|
access_token_expiration_time: 10080
|
||||||
secret_key: "your-key"
|
secret_key: "your-key"
|
||||||
swagger_enabled: true
|
swagger_enabled: true
|
||||||
|
join_code_length: 8
|
||||||
# This part should not be touched. Probably
|
# This part should not be touched. Probably
|
||||||
algorithm: "HS256"
|
algorithm: "HS256"
|
||||||
db_host: "127.0.0.1" # review this later
|
db_host: "127.0.0.1" # review this later
|
||||||
|
|||||||
@ -22,6 +22,11 @@ async def add_admin(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
|
if db.check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="User already exists",
|
||||||
|
)
|
||||||
hashed_password = get_password_hash(password)
|
hashed_password = get_password_hash(password)
|
||||||
return db.create_user(conn, username, hashed_password, "admin")
|
return db.create_user(conn, username, hashed_password, "admin")
|
||||||
|
|
||||||
@ -36,5 +41,10 @@ async def add_user(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
|
if db.check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="User already exists",
|
||||||
|
)
|
||||||
hashed_password = get_password_hash(password)
|
hashed_password = get_password_hash(password)
|
||||||
return db.create_user(conn, username, hashed_password, "user")
|
return db.create_user(conn, username, hashed_password, "user")
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from psycopg2._psycopg import connection
|
|||||||
from api.models import Token
|
from api.models import Token
|
||||||
from api.utils import authenticate_user, create_access_token
|
from api.utils import authenticate_user, create_access_token
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
|
from db.users import check_user_disabled
|
||||||
from settings import startup_settings
|
from settings import startup_settings
|
||||||
|
|
||||||
auth_router = APIRouter(prefix="/api", tags=["auth"])
|
auth_router = APIRouter(prefix="/api", tags=["auth"])
|
||||||
@ -26,6 +27,14 @@ async def login(
|
|||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_disabled = check_user_disabled(conn, form_data.username)
|
||||||
|
if user_disabled:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User acoount is disabled",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
access_token_expire_time = timedelta(minutes=startup_settings.access_token_expiration_time)
|
access_token_expire_time = timedelta(minutes=startup_settings.access_token_expiration_time)
|
||||||
access_token = create_access_token(
|
access_token = create_access_token(
|
||||||
data={"sub": form_data.username}, expires_delta=access_token_expire_time
|
data={"sub": form_data.username}, expires_delta=access_token_expire_time
|
||||||
|
|||||||
0
src/api/feeds.py
Normal file
0
src/api/feeds.py
Normal file
@ -10,17 +10,22 @@ from settings.settings import load_settings, reset_settings, save_settings
|
|||||||
general_router = APIRouter(prefix="/api", tags=["general"])
|
general_router = APIRouter(prefix="/api", tags=["general"])
|
||||||
|
|
||||||
|
|
||||||
@general_router.get('/ping')
|
@general_router.get("/ping")
|
||||||
async def ping():
|
async def ping():
|
||||||
return {'ok'}
|
return {"ok"}
|
||||||
|
|
||||||
|
|
||||||
@general_router.get('/settings/get')
|
@general_router.get("/settings/get")
|
||||||
async def get_settings():
|
async def get_settings(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
return settings.settings
|
return settings.settings
|
||||||
|
|
||||||
|
|
||||||
@general_router.post('/settings/update')
|
@general_router.post("/settings/update")
|
||||||
async def update_settings(data: dict, current_user: Annotated[User, Depends(get_current_user)]):
|
async def update_settings(data: dict, current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -32,7 +37,7 @@ async def update_settings(data: dict, current_user: Annotated[User, Depends(get_
|
|||||||
return settings.settings
|
return settings.settings
|
||||||
|
|
||||||
|
|
||||||
@general_router.get('/settings/reset')
|
@general_router.get("/settings/reset")
|
||||||
async def reset_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
async def reset_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -43,7 +48,7 @@ async def reset_settings_api(current_user: Annotated[User, Depends(get_current_u
|
|||||||
return settings.settings
|
return settings.settings
|
||||||
|
|
||||||
|
|
||||||
@general_router.get('/settings/load_from_file')
|
@general_router.get("/settings/load_from_file")
|
||||||
async def load_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
async def load_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -53,7 +58,7 @@ async def load_settings_api(current_user: Annotated[User, Depends(get_current_us
|
|||||||
load_settings()
|
load_settings()
|
||||||
return settings.settings
|
return settings.settings
|
||||||
|
|
||||||
@general_router.get('/settings/save_to_file')
|
@general_router.get("/settings/save_to_file")
|
||||||
async def save_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
async def save_settings_api(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
|
import secrets
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from psycopg2._psycopg import connection
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
import db.users as db
|
import db.groups as db
|
||||||
import settings.settings as settings
|
import settings.settings as settings
|
||||||
from api.models import User
|
import settings.startup_settings as startup_settings
|
||||||
from api.utils import get_current_user
|
from api.models import Group, User
|
||||||
|
from api.utils import get_current_user, get_group_by_name
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
|
from db.memberships import check_membership_exists
|
||||||
|
from settings.consts import JOIN_CODE_SYMBOLS
|
||||||
|
|
||||||
groups_router = APIRouter(prefix="/api/groups", tags=["groups"])
|
groups_router = APIRouter(prefix="/api/groups", tags=["groups"])
|
||||||
|
|
||||||
|
|
||||||
@groups_router.get("/my")
|
@groups_router.post("/group")
|
||||||
async def read_users_groups(current_user: Annotated[User, Depends(get_current_user)]):
|
async def read_any_group(
|
||||||
return current_user
|
groupname: str,
|
||||||
|
|
||||||
@groups_router.post("/user")
|
|
||||||
async def read_users_any_groups(
|
|
||||||
username: str,
|
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
@ -27,46 +27,186 @@ async def read_users_any_groups(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
user = User()
|
group = Group()
|
||||||
user_data = db.get_user(conn, username)
|
group_data = db.get_group(conn, groupname)
|
||||||
if user_data is None:
|
if group_data is None:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No such user",
|
detail="No such group",
|
||||||
)
|
)
|
||||||
user.fill(user_data)
|
group.fill(group_data)
|
||||||
return user
|
return group
|
||||||
|
|
||||||
|
@groups_router.post("/invite_code")
|
||||||
|
async def read_group_invite_code(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
invite_code = db.get_group_invite_code(conn, groupname)
|
||||||
|
if invite_code is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such group",
|
||||||
|
)
|
||||||
|
return invite_code
|
||||||
|
|
||||||
|
|
||||||
@groups_router.post("/add")
|
@groups_router.post("/add")
|
||||||
async def add_group(
|
async def add_group(
|
||||||
groupname: str,
|
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)],
|
||||||
|
groupname: str,
|
||||||
|
allow_skips: bool = True,
|
||||||
|
feed_interval_minutes: int = 1440,
|
||||||
):
|
):
|
||||||
# TODO
|
if not settings.settings.allow_create_groups and current_user.role not in settings.settings.admin_roles:
|
||||||
pass
|
raise HTTPException(
|
||||||
# if not settings.settings.allow_create_admins_by_admins:
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
# if current_user.role not in settings.settings.admin_roles:
|
detail="Not allowed",
|
||||||
# raise HTTPException(
|
)
|
||||||
# status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
# detail="Not allowed",
|
|
||||||
# )
|
|
||||||
# return db.create_user(conn, username, hashed_password, "admin")
|
|
||||||
|
|
||||||
|
if db.check_group_existence(conn, groupname):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Group already exists",
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
|
while db.check_invite_code(conn, invite_code):
|
||||||
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
|
return {
|
||||||
|
"result": db.create_group(conn, groupname, invite_code, current_user.username, allow_skips, feed_interval_minutes),
|
||||||
|
"invite code": invite_code
|
||||||
|
}
|
||||||
|
|
||||||
@groups_router.post("/delete")
|
@groups_router.post("/delete")
|
||||||
async def delete_user(
|
async def delete_group(
|
||||||
groupname: str,
|
groupname: str,
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
# TODO
|
group = get_group_by_name(conn, groupname)
|
||||||
pass
|
if current_user.role in settings.settings.admin_roles:
|
||||||
# if current_user.username == username or current_user.role in settings.settings.admin_roles:
|
return db.delete_group(conn, groupname)
|
||||||
# return db.delete_user(conn, groupname)
|
if current_user.username == group.author:
|
||||||
# else:
|
return db.delete_group(conn, groupname)
|
||||||
# raise HTTPException(
|
else:
|
||||||
# status_code=status.HTTP_403_FORBIDDEN,
|
raise HTTPException(
|
||||||
# detail="Not allowed",
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
# )
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@groups_router.post("/update/groupname")
|
||||||
|
async def update_groupname(
|
||||||
|
groupname: str,
|
||||||
|
new_groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if db.check_group_existence(conn, new_groupname):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Groupname is already taken",
|
||||||
|
)
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.update_group_groupname(conn, groupname, new_groupname)
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return db.update_group_groupname(conn, groupname, new_groupname)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
@groups_router.post("/update/author")
|
||||||
|
async def update_author(
|
||||||
|
groupname: str,
|
||||||
|
new_author: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.update_group_author(conn, groupname, new_author)
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return db.update_group_author(conn, groupname, new_author)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
@groups_router.get("/update/invite_code")
|
||||||
|
async def update_invite_code(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
|
||||||
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
|
while db.check_invite_code(conn, invite_code):
|
||||||
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
|
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return {
|
||||||
|
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
||||||
|
"invite code": invite_code
|
||||||
|
}
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return {
|
||||||
|
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
||||||
|
"invite code": invite_code
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@groups_router.get("/update/allow_skips")
|
||||||
|
async def update_allow_skips(
|
||||||
|
groupname: str,
|
||||||
|
allow_skips: bool,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@groups_router.get("/update/feed_interval")
|
||||||
|
async def update_feed_interval(
|
||||||
|
groupname: str,
|
||||||
|
feed_interval: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|||||||
117
src/api/memberships.py
Normal file
117
src/api/memberships.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.memberships as db
|
||||||
|
import settings.settings as settings
|
||||||
|
from api.models import User
|
||||||
|
from api.utils import get_current_user, get_group_by_name
|
||||||
|
from db.groups import check_group_existence, get_groupname_by_invite_code
|
||||||
|
from db.internal import get_db_connection
|
||||||
|
from db.users import check_user_existence
|
||||||
|
|
||||||
|
memberships_router = APIRouter(prefix="/api/membership", tags=["memberships"])
|
||||||
|
|
||||||
|
|
||||||
|
@memberships_router.get("/me")
|
||||||
|
async def read_users_groups(
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
return db.get_memberships_by_username(conn, current_user.username)
|
||||||
|
|
||||||
|
@memberships_router.post("/user")
|
||||||
|
async def read_users_any_memberships(
|
||||||
|
username: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not username == current_user.username and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return db.get_memberships_by_username(conn, username)
|
||||||
|
|
||||||
|
@memberships_router.post("/group")
|
||||||
|
async def read_any_group_members(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
user_is_in_group = db.check_membership_exists(conn, current_user.username, groupname)
|
||||||
|
if not user_is_in_group and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_group_existence(conn, groupname):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such group",
|
||||||
|
)
|
||||||
|
|
||||||
|
return db.get_memberships_by_groupname(conn, groupname)
|
||||||
|
|
||||||
|
|
||||||
|
@memberships_router.post("/add")
|
||||||
|
async def add_membership(
|
||||||
|
username: str,
|
||||||
|
invite_code: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if username != current_user.username and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
groupname = get_groupname_by_invite_code(conn, invite_code)
|
||||||
|
if groupname is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Invite code is incorrect",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
if db.check_membership_exists(conn, username, groupname):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="User is already a member",
|
||||||
|
)
|
||||||
|
|
||||||
|
return db.create_membership(conn, username, groupname)
|
||||||
|
|
||||||
|
@memberships_router.post("/delete")
|
||||||
|
async def delete_membership(
|
||||||
|
username: str,
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.delete_membership(conn, username, groupname)
|
||||||
|
|
||||||
|
group = get_group_by_name(conn, groupname)
|
||||||
|
if current_user.username == group.author:
|
||||||
|
return db.delete_membership(conn, username, groupname)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
@ -14,17 +14,61 @@ class TokenData(BaseModel):
|
|||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
def fill(self, params):
|
def fill(self, params):
|
||||||
self.username = params['username']
|
self.username = params["username"]
|
||||||
self.password = params['password']
|
self.password = params["password"]
|
||||||
self.role = params['role']
|
self.role = params["role"]
|
||||||
self.disabled = params['disabled']
|
self.disabled = params["disabled"]
|
||||||
self.groups_ids = params['groups_ids']
|
self.groups_ids = params["groups_ids"]
|
||||||
self.last_seen_at = params['last_seen_at']
|
self.last_seen_at = params["last_seen_at"]
|
||||||
self.created_at = params['created_at']
|
self.created_at = params["created_at"]
|
||||||
username: str = ''
|
username: str = ""
|
||||||
password: str = ''
|
password: str = ""
|
||||||
role: str = 'user'
|
role: str = "user"
|
||||||
disabled: bool = False
|
disabled: bool = False
|
||||||
groups_ids: list[str] | None = None
|
groups_ids: list[str] | None = None
|
||||||
last_seen_at: datetime | None = None
|
last_seen_at: datetime | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Group(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.groupname = params["groupname"]
|
||||||
|
self.author = params["author"]
|
||||||
|
self.invite_code = params["invite_code"]
|
||||||
|
self.allow_skips = params["allow_skips"]
|
||||||
|
self.feed_interval_minutes = params["feed_interval_minutes"]
|
||||||
|
self.last_feed_id = params["last_feed_id"]
|
||||||
|
self.created_at = params["created_at"]
|
||||||
|
groupname: str = ""
|
||||||
|
author: str = ""
|
||||||
|
invite_code: str = ""
|
||||||
|
allow_skips: bool = True
|
||||||
|
feed_interval_minutes: int = 1440
|
||||||
|
last_feed_id: int | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.groupname = params["groupname"]
|
||||||
|
self.username = params["username"]
|
||||||
|
self.joined_at = params["joined_at"]
|
||||||
|
groupname: str = ""
|
||||||
|
username: str = ""
|
||||||
|
joined_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Picture(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.id = params["id"]
|
||||||
|
self.source = params["source"]
|
||||||
|
self.external_id = params["external_id"]
|
||||||
|
self.url = params["url"]
|
||||||
|
self.metadata = params["metadata"]
|
||||||
|
self.created_at = params["created_at"]
|
||||||
|
id: int = -1
|
||||||
|
source: str = ""
|
||||||
|
external_id: str = ""
|
||||||
|
url: str = ""
|
||||||
|
metadata: dict | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|||||||
103
src/api/pictures.py
Normal file
103
src/api/pictures.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.pictures as db
|
||||||
|
import settings.settings as settings
|
||||||
|
from api.models import Picture, User
|
||||||
|
from api.utils import get_current_user
|
||||||
|
from db.internal import get_db_connection
|
||||||
|
|
||||||
|
pictures_router = APIRouter(prefix="/api/pictures", tags=["pictures"])
|
||||||
|
|
||||||
|
|
||||||
|
# maybe to delete
|
||||||
|
@pictures_router.post("/picture/url")
|
||||||
|
async def read_picture_by_url(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
picture = Picture()
|
||||||
|
picture_data = db.get_picture_by_url(conn, groupname)
|
||||||
|
if picture_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such picture",
|
||||||
|
)
|
||||||
|
picture.fill(picture_data)
|
||||||
|
return picture
|
||||||
|
|
||||||
|
|
||||||
|
@pictures_router.post("/picture/id")
|
||||||
|
async def read_picture_by_id(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
picture = Picture()
|
||||||
|
picture_data = db.get_picture_by_id(conn, groupname)
|
||||||
|
if picture_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such picture",
|
||||||
|
)
|
||||||
|
picture.fill(picture_data)
|
||||||
|
return picture
|
||||||
|
|
||||||
|
|
||||||
|
@pictures_router.post("/add")
|
||||||
|
async def add_picture(
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)],
|
||||||
|
source: str,
|
||||||
|
external_id: str,
|
||||||
|
url: str,
|
||||||
|
metadata: dict
|
||||||
|
):
|
||||||
|
if not settings.settings.allow_create_pictures and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# if db.check_picture_existence(conn, groupname):
|
||||||
|
# raise HTTPException(
|
||||||
|
# status_code=status.HTTP_409_CONFLICT,
|
||||||
|
# detail="Picture already exists",
|
||||||
|
# )
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": db.create_picture(conn, source, external_id, url, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# maybe to delete
|
||||||
|
@pictures_router.post("/delete/url")
|
||||||
|
async def delete_picture_by_url(
|
||||||
|
picture_url: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.delete_picture_by_url(conn, picture_url)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
@pictures_router.post("/delete/id")
|
||||||
|
async def delete_picture_by_id(
|
||||||
|
picture_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.delete_picture_by_id(conn, picture_id)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
@ -46,11 +46,20 @@ async def add_admin(
|
|||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
if not settings.settings.allow_create_admins_by_admins:
|
if not settings.settings.allow_create_admins_by_admins:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
|
if db.check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="User already exists",
|
||||||
|
)
|
||||||
hashed_password = get_password_hash(password)
|
hashed_password = get_password_hash(password)
|
||||||
return db.create_user(conn, username, hashed_password, "admin")
|
return db.create_user(conn, username, hashed_password, "admin")
|
||||||
|
|
||||||
@ -61,11 +70,16 @@ async def add_user(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
if not settings.settings.allow_create_users or current_user.role not in settings.settings.admin_roles:
|
if not settings.settings.allow_create_users and current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
|
if db.check_user_existence(conn, username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="User already exists",
|
||||||
|
)
|
||||||
hashed_password = get_password_hash(password)
|
hashed_password = get_password_hash(password)
|
||||||
return db.create_user(conn, username, hashed_password, "user")
|
return db.create_user(conn, username, hashed_password, "user")
|
||||||
|
|
||||||
@ -119,12 +133,17 @@ async def update_role(
|
|||||||
@users_router.post("/update/username")
|
@users_router.post("/update/username")
|
||||||
async def update_username(
|
async def update_username(
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
new_username: str,
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
|
if db.check_user_existence(conn, new_username):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Username is already taken",
|
||||||
|
)
|
||||||
if current_user.username == username or current_user.role in settings.settings.admin_roles:
|
if current_user.username == username or current_user.role in settings.settings.admin_roles:
|
||||||
return db.update_user_username(conn, username, password)
|
return db.update_user_username(conn, username, new_username)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
|||||||
@ -10,9 +10,10 @@ from jwt.exceptions import InvalidTokenError
|
|||||||
# from passlib.context import CryptContext
|
# from passlib.context import CryptContext
|
||||||
from psycopg2._psycopg import connection
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.groups
|
||||||
import db.users
|
import db.users
|
||||||
import settings.startup_settings as startup_settings
|
import settings.startup_settings as startup_settings
|
||||||
from api.models import TokenData, User
|
from api.models import Group, TokenData, User
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
|
|
||||||
# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
@ -38,9 +39,13 @@ def authenticate_user(
|
|||||||
username: str,
|
username: str,
|
||||||
user_password: str
|
user_password: str
|
||||||
):
|
):
|
||||||
db_user_password = db.users.get_user_password(conn, username)
|
|
||||||
if not user_password:
|
if not user_password:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
db_user_password = db.users.get_user_password(conn, username)
|
||||||
|
if db_user_password is None:
|
||||||
|
return False
|
||||||
|
|
||||||
if not verify_password(user_password, db_user_password):
|
if not verify_password(user_password, db_user_password):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -64,7 +69,7 @@ async def get_current_user(
|
|||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Could not validate credentials",
|
detail="Could not validate credentials",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -90,3 +95,19 @@ async def get_current_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def get_group_by_name(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
group_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such group"
|
||||||
|
)
|
||||||
|
group = Group()
|
||||||
|
group_data = db.groups.get_group(conn, groupname)
|
||||||
|
if group_data is None:
|
||||||
|
raise group_exception
|
||||||
|
|
||||||
|
group.fill(group_data)
|
||||||
|
return group
|
||||||
|
|||||||
@ -7,6 +7,8 @@ from api.anon import anon_router
|
|||||||
from api.auth import auth_router
|
from api.auth import auth_router
|
||||||
from api.general import general_router
|
from api.general import general_router
|
||||||
from api.groups import groups_router
|
from api.groups import groups_router
|
||||||
|
from api.memberships import memberships_router
|
||||||
|
from api.pictures import pictures_router
|
||||||
from api.users import users_router
|
from api.users import users_router
|
||||||
from db.internal import connect_db, disconnect_db
|
from db.internal import connect_db, disconnect_db
|
||||||
from settings import startup_settings
|
from settings import startup_settings
|
||||||
@ -37,10 +39,12 @@ def create_app():
|
|||||||
app.add_event_handler("startup", settings_up)
|
app.add_event_handler("startup", settings_up)
|
||||||
|
|
||||||
app.include_router(general_router)
|
app.include_router(general_router)
|
||||||
app.include_router(auth_router)
|
|
||||||
app.include_router(anon_router)
|
app.include_router(anon_router)
|
||||||
|
app.include_router(auth_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(groups_router)
|
app.include_router(groups_router)
|
||||||
|
app.include_router(memberships_router)
|
||||||
|
app.include_router(pictures_router)
|
||||||
|
|
||||||
app.add_event_handler("shutdown", disconnect_db)
|
app.add_event_handler("shutdown", disconnect_db)
|
||||||
app.add_event_handler("shutdown", settings_down)
|
app.add_event_handler("shutdown", settings_down)
|
||||||
|
|||||||
262
src/db/groups.py
Normal file
262
src/db/groups.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
# group create and delete
|
||||||
|
|
||||||
|
def create_group(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
invite_code: str,
|
||||||
|
author: str,
|
||||||
|
allow_skips: bool = True,
|
||||||
|
feed_interval_minutes: int = 1440,
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.groups
|
||||||
|
(groupname, invite_code, author, allow_skips,
|
||||||
|
feed_interval_minutes, created_at)
|
||||||
|
values (%s, %s, %s, %s, %s, now())
|
||||||
|
""",
|
||||||
|
(groupname, invite_code, author, allow_skips, feed_interval_minutes),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def delete_group(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.groups
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# group checks
|
||||||
|
|
||||||
|
def check_group_existence(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select exists(
|
||||||
|
select 1
|
||||||
|
from picrinth.groups
|
||||||
|
where groupname = %s
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def check_invite_code(
|
||||||
|
conn: connection,
|
||||||
|
invite_code: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select exists(
|
||||||
|
select 1
|
||||||
|
from picrinth.groups
|
||||||
|
where invite_code = %s
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
(invite_code,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# group updates
|
||||||
|
|
||||||
|
def update_group_groupname(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
new_groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set groupname = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(new_groupname, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
def update_group_author(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
author: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set author = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(author, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
def update_group_invite_code(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
invite_code: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set invite_code = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(invite_code, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def update_group_allow_skips(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
allow_skips: bool
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set allow_skips = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(allow_skips, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
def update_group_feed_interval(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
feed_interval: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set feed_interval_minutes = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(feed_interval, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
def update_group_last_feed(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
last_feed_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.groups
|
||||||
|
set last_feed_id = %s
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(last_feed_id, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# group receiving
|
||||||
|
|
||||||
|
def get_group(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname, author,
|
||||||
|
invite_code, allow_skips,
|
||||||
|
feed_interval_minutes,
|
||||||
|
last_feed_id, created_at
|
||||||
|
from picrinth.groups
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def get_group_by_invite_code(
|
||||||
|
conn: connection,
|
||||||
|
invite_code: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname, author,
|
||||||
|
invite_code, allow_skips,
|
||||||
|
feed_interval_minutes,
|
||||||
|
last_feed_id, created_at
|
||||||
|
from picrinth.groups
|
||||||
|
where invite_code = %s
|
||||||
|
""",
|
||||||
|
(invite_code,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def get_groupname_by_invite_code(
|
||||||
|
conn: connection,
|
||||||
|
invite_code: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname
|
||||||
|
from picrinth.groups
|
||||||
|
where invite_code = %s
|
||||||
|
""",
|
||||||
|
(invite_code,),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0] # type: ignore
|
||||||
|
|
||||||
|
def get_group_invite_code(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select invite_code
|
||||||
|
from picrinth.groups
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0] # type: ignore
|
||||||
93
src/db/memberships.py
Normal file
93
src/db/memberships.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
# membership create and delete
|
||||||
|
|
||||||
|
def create_membership(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.memberships
|
||||||
|
(username, groupname, joined_at)
|
||||||
|
values (%s, %s, now())
|
||||||
|
""",
|
||||||
|
(username, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def delete_membership(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.memberships
|
||||||
|
where username = %s and groupname = %s
|
||||||
|
""",
|
||||||
|
(username, groupname),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# membership checks
|
||||||
|
|
||||||
|
def check_membership_exists(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select exists(
|
||||||
|
select 1
|
||||||
|
from picrinth.memberships
|
||||||
|
where username = %s and groupname = %s
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
(username, groupname),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# membership receiving
|
||||||
|
|
||||||
|
def get_memberships_by_username(
|
||||||
|
conn: connection,
|
||||||
|
username: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from picrinth.memberships
|
||||||
|
where username = %s
|
||||||
|
""",
|
||||||
|
(username,),
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
def get_memberships_by_groupname(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from picrinth.memberships
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
97
src/db/pictures.py
Normal file
97
src/db/pictures.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
# picture create and delete
|
||||||
|
|
||||||
|
def create_picture(
|
||||||
|
conn: connection,
|
||||||
|
source: str,
|
||||||
|
external_id: str,
|
||||||
|
url: str,
|
||||||
|
metadata: dict
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.pictures
|
||||||
|
(source, external_id, url, metadata, created_at)
|
||||||
|
values (%s, %s, %s, %s, now())
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(source, external_id, url, json.dumps(metadata)),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_picture_by_url(
|
||||||
|
conn: connection,
|
||||||
|
url: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.pictures
|
||||||
|
where url = %s
|
||||||
|
""",
|
||||||
|
(url,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
def delete_picture_by_id(
|
||||||
|
conn: connection,
|
||||||
|
id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.pictures
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# picture receiving
|
||||||
|
|
||||||
|
def get_picture_by_url(
|
||||||
|
conn: connection,
|
||||||
|
url: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select id, source,
|
||||||
|
external_id, url,
|
||||||
|
metadata, created_at
|
||||||
|
from picrinth.pictures
|
||||||
|
where url = %s
|
||||||
|
""",
|
||||||
|
(url,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
def get_picture_by_id(
|
||||||
|
conn: connection,
|
||||||
|
id: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select id, source,
|
||||||
|
external_id, url,
|
||||||
|
metadata, created_at
|
||||||
|
from picrinth.pictures
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
@ -55,7 +55,7 @@ def check_user_existence(
|
|||||||
""",
|
""",
|
||||||
(username,),
|
(username,),
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
def check_user_disabled(
|
def check_user_disabled(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
@ -70,7 +70,10 @@ def check_user_disabled(
|
|||||||
""",
|
""",
|
||||||
(username,),
|
(username,),
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# user updates
|
# user updates
|
||||||
@ -91,6 +94,7 @@ def update_user_disabled(
|
|||||||
(disabled, username),
|
(disabled, username),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
def update_user_role(
|
def update_user_role(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
@ -107,6 +111,7 @@ def update_user_role(
|
|||||||
(role, username),
|
(role, username),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def update_user_username(
|
def update_user_username(
|
||||||
@ -124,6 +129,7 @@ def update_user_username(
|
|||||||
(newUsername, username),
|
(newUsername, username),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
def update_user_password(
|
def update_user_password(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
@ -140,6 +146,7 @@ def update_user_password(
|
|||||||
(password, username),
|
(password, username),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def update_user_last_seen(
|
def update_user_last_seen(
|
||||||
@ -156,6 +163,7 @@ def update_user_last_seen(
|
|||||||
(username,),
|
(username,),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
# user receiving
|
# user receiving
|
||||||
@ -190,4 +198,7 @@ def get_user_password(
|
|||||||
""",
|
""",
|
||||||
(username,),
|
(username,),
|
||||||
)
|
)
|
||||||
return cur.fetchone()[0] # type: ignore
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0] # type: ignore
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
JOIN_CODE_SYMBOLS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No O + 0, I + 1
|
||||||
|
|||||||
@ -7,74 +7,78 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
class Settings(BaseModel):
|
class Settings(BaseModel):
|
||||||
def update(self, params):
|
def update(self, params):
|
||||||
self.admin_roles = params['admin_roles']
|
self.admin_roles = params["admin_roles"]
|
||||||
self.allow_create_admins_by_admins = params['allow_create_admins_by_admins']
|
self.allow_create_admins_by_admins = params["allow_create_admins_by_admins"]
|
||||||
self.allow_create_admins = params['allow_create_admins']
|
self.allow_create_admins = params["allow_create_admins"]
|
||||||
self.allow_create_users = params['allow_create_users']
|
self.allow_create_users = params["allow_create_users"]
|
||||||
admin_roles: list[str] = ['admin']
|
self.allow_create_groups = params["allow_create_groups"]
|
||||||
|
self.allow_create_pictures = params["allow_create_groups"]
|
||||||
|
admin_roles: list[str] = ["admin"]
|
||||||
allow_create_admins_by_admins: bool = True
|
allow_create_admins_by_admins: bool = True
|
||||||
allow_create_admins: bool = True
|
allow_create_admins: bool = True
|
||||||
allow_create_users: bool = True
|
allow_create_users: bool = True
|
||||||
|
allow_create_groups: bool = True
|
||||||
|
allow_create_pictures: bool = False
|
||||||
|
|
||||||
|
|
||||||
json_path = 'data/'
|
json_path = "data/"
|
||||||
json_settings_name = 'settings.json'
|
json_settings_name = "settings.json"
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
def settings_up():
|
def settings_up():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
logger.info('Configuring settings for startup')
|
logger.info("Configuring settings for startup")
|
||||||
try:
|
try:
|
||||||
if not(os.path.exists(json_path)):
|
if not(os.path.exists(json_path)):
|
||||||
os.mkdir(json_path)
|
os.mkdir(json_path)
|
||||||
logger.debug(f'Created "{json_path}" directory')
|
logger.debug(f"Created '{json_path}' directory")
|
||||||
|
|
||||||
if os.path.exists(json_path + json_settings_name):
|
if os.path.exists(json_path + json_settings_name):
|
||||||
load_settings()
|
load_settings()
|
||||||
else:
|
else:
|
||||||
with open(json_path + json_settings_name, 'w') as f:
|
with open(json_path + json_settings_name, "w") as f:
|
||||||
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
||||||
logger.info('Wrote settings to the JSON')
|
logger.info("Wrote settings to the JSON")
|
||||||
logger.info('Successfully configured settings')
|
logger.info("Successfully configured settings")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Failed to configure settings during startup: {e}')
|
logger.error(f"Failed to configure settings during startup: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def settings_down():
|
def settings_down():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
logger.info('Saving settings for shutdown')
|
logger.info("Saving settings for shutdown")
|
||||||
try:
|
try:
|
||||||
with open(json_path + json_settings_name, 'w') as f:
|
with open(json_path + json_settings_name, "w") as f:
|
||||||
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
||||||
logger.info('Wrote settings to the JSON')
|
logger.info("Wrote settings to the JSON")
|
||||||
logger.success('Successfully saved settings')
|
logger.success("Successfully saved settings")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Failed to save settings during shutdown: {e}')
|
logger.error(f"Failed to save settings during shutdown: {e}")
|
||||||
|
|
||||||
|
|
||||||
def reset_settings():
|
def reset_settings():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
logger.info('Resetting settings')
|
logger.info("Resetting settings")
|
||||||
print(settings)
|
print(settings)
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
print(settings)
|
print(settings)
|
||||||
with open(json_path + json_settings_name, 'w') as f:
|
with open(json_path + json_settings_name, "w") as f:
|
||||||
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
||||||
logger.info('Wrote settings to the JSON')
|
logger.info("Wrote settings to the JSON")
|
||||||
|
|
||||||
|
|
||||||
def load_settings():
|
def load_settings():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
logger.info('Loading settings')
|
logger.info("Loading settings")
|
||||||
with open(json_path + json_settings_name, 'r') as f:
|
with open(json_path + json_settings_name, "r") as f:
|
||||||
json_settings = json.load(f)
|
json_settings = json.load(f)
|
||||||
settings = Settings.model_validate_json(json_settings)
|
settings = Settings.model_validate_json(json_settings)
|
||||||
logger.info('Loaded settings from the JSON')
|
logger.info("Loaded settings from the JSON")
|
||||||
|
|
||||||
def save_settings():
|
def save_settings():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
with open(json_path + json_settings_name, 'w') as f:
|
with open(json_path + json_settings_name, "w") as f:
|
||||||
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
||||||
logger.info('Wrote settings to the JSON')
|
logger.info("Wrote settings to the JSON")
|
||||||
|
|||||||
@ -2,22 +2,23 @@ from decouple import config
|
|||||||
|
|
||||||
|
|
||||||
def str_to_bool(string: str) -> bool:
|
def str_to_bool(string: str) -> bool:
|
||||||
if string.lower() == 'true':
|
if string.lower() == "true":
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# database
|
# database
|
||||||
db_host = str(config('db_host', default='127.0.0.1'))
|
db_host = str(config("db_host", default="127.0.0.1"))
|
||||||
db_port = int(config('db_port', default=5432))
|
db_port = int(config("db_port", default=5432))
|
||||||
db_name = str(config('db_name', default='postgres'))
|
db_name = str(config("db_name", default="postgres"))
|
||||||
db_user = str(config('db_user', default='postgres'))
|
db_user = str(config("db_user", default="postgres"))
|
||||||
db_password = str(config('db_password', default='postgres'))
|
db_password = str(config("db_password", default="postgres"))
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
secret_key = str(config('secret_key'))
|
secret_key = str(config("secret_key"))
|
||||||
algorithm = str(config('algorithm', 'HS256'))
|
algorithm = str(config("algorithm", "HS256"))
|
||||||
access_token_expiration_time = int(config('access_token_expiration_time', default=10080))
|
access_token_expiration_time = int(config("access_token_expiration_time", default=10080))
|
||||||
|
|
||||||
# other settings
|
# other settings
|
||||||
swagger_enabled = str_to_bool(str(config('swagger_enabled', 'false')))
|
swagger_enabled = str_to_bool(str(config("swagger_enabled", "false")))
|
||||||
log_level = str(config('log_level', default='INFO'))
|
log_level = str(config("log_level", default="INFO"))
|
||||||
|
join_code_length = int(config("join_code_length", default=8))
|
||||||
|
|||||||
67
tables.sql
67
tables.sql
@ -1,28 +1,69 @@
|
|||||||
CREATE TABLE picrinth.users (
|
create schema picrinth;
|
||||||
|
|
||||||
|
create table picrinth.users (
|
||||||
id serial not null,
|
id serial not null,
|
||||||
username text not null,
|
username text not null,
|
||||||
"password" text not null,
|
"password" text not null,
|
||||||
"role" text not null default "user",
|
"role" text not null default 'user',
|
||||||
"disabled" bool not null,
|
"disabled" bool not null default false,
|
||||||
groups_ids integer[] NULL,
|
|
||||||
last_seen_at timestamp with time zone null,
|
last_seen_at timestamp with time zone null,
|
||||||
created_at timestamp with time zone null,
|
created_at timestamp with time zone default now(),
|
||||||
CONSTRAINT username_unique UNIQUE (username)
|
constraint username_unique unique (username)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE picrinth.groups (
|
create table picrinth.groups (
|
||||||
id serial not null,
|
id serial not null,
|
||||||
groupname text not null,
|
groupname text not null,
|
||||||
invite_code text not null,
|
invite_code text not null,
|
||||||
created_at timestamp with time zone null,
|
author text null,
|
||||||
CONSTRAINT groupname_unique UNIQUE (groupname)
|
allow_skips bool not null default true,
|
||||||
|
feed_interval_minutes integer null default 1440,
|
||||||
|
last_feed_id int null,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
constraint groupname_unique unique (groupname),
|
||||||
|
constraint invite_code_unique unique (invite_code)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table picrinth.group_members (
|
create table picrinth.memberships (
|
||||||
username text,
|
username text,
|
||||||
groupname text,
|
groupname text,
|
||||||
joined_at timestamp with time zone null,
|
joined_at timestamp with time zone null,
|
||||||
PRIMARY KEY (username, groupname)
|
primary key (username, groupname),
|
||||||
FOREIGN KEY (username) REFERENCES users (username) on delete cascade on update cascade
|
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
|
||||||
FOREIGN KEY (groupname) REFERENCES groups (groupname) on delete cascade on update cascade
|
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table picrinth.pictures (
|
||||||
|
id serial not null,
|
||||||
|
source text not null,
|
||||||
|
external_id text not null,
|
||||||
|
url text not null,
|
||||||
|
metadata jsonb null,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
constraint pictures_pkey primary key (id),
|
||||||
|
constraint url_unique unique (url)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table picrinth.feeds (
|
||||||
|
id serial not null,
|
||||||
|
groupname text not null,
|
||||||
|
image_ids integer[] not null,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
constraint feeds_pkey primary key (id),
|
||||||
|
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade,
|
||||||
|
constraint unique_feed_per_timestamp_per_group unique (groupname, created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table picrinth.swipes (
|
||||||
|
id serial not null,
|
||||||
|
username text not null,
|
||||||
|
feed_id integer not null,
|
||||||
|
picture_id integer not null,
|
||||||
|
swipe_value smallint not null,
|
||||||
|
swiped_at timestamp with time zone default now(),
|
||||||
|
primary key (id),
|
||||||
|
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
|
||||||
|
foreign key (feed_id) references picrinth.feeds (id) on delete cascade on update cascade,
|
||||||
|
foreign key (picture_id) references picrinth.pictures (id) on delete cascade on update cascade,
|
||||||
|
constraint swipes_unique unique (username, feed_id, picture_id)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user