Added media endpoints, various style modifications and minor fixes, added API.md

This commit is contained in:
Nocturn9x 2022-10-06 12:27:53 +02:00
parent efa1ebf669
commit 1da075f48f
7 changed files with 419 additions and 237 deletions

6
API.md Normal file
View File

@ -0,0 +1,6 @@
# PySimpleSocial - API Documentation
## Disclaimer
This is a technical page that only describes the API's functionality. For information about licensing,
setup and credits, please check out the README.md file included with the project.

54
endpoints/media.py Normal file
View File

@ -0,0 +1,54 @@
from fastapi import Request, APIRouter as FastAPI, Depends
from fastapi.exceptions import HTTPException
from responses.media import MediaResponse
from responses import UnprocessableEntity, Response as APIResponse, NotFound
from config import MANAGER, LIMITER
from orm.media import Media, PublicMediaModel
from orm.users import UserModel
router = FastAPI()
@router.get("/media/{media_id}",
tags=["Media"],
status_code=200,
responses={
200: {"model": MediaResponse},
422: {"model": UnprocessableEntity},
404: {"model": NotFound}
},
)
@LIMITER.limit("2/second")
async def get_media(request: Request, media_id: str, _user: UserModel = Depends(MANAGER)):
"""
Gets a media object by its ID
"""
if (m := await Media.select(Media.media_id).where(Media.media_id == media_id).first()) is None:
raise HTTPException(status_code=404, detail="Media not found")
m = Media(**m)
return MediaResponse(data=PublicMediaModel(media_id=m.media_id, content=m.content, content_type=m.content_type,
creation_date=m.creation_date))
@router.get("/media/{media_id}/report",
tags=["Media"],
status_code=200,
responses={
200: {"model": APIResponse},
422: {"model": UnprocessableEntity},
404: {"model": NotFound}
},
)
@LIMITER.limit("2/second")
async def report_media(request: Request, media_id: str, _user: UserModel = Depends(MANAGER)):
"""
Reports a piece of media by its ID. This creates
a report that can be seen by admins, which can
then decide what to do
"""
if (m := await Media.select(Media.media_id).where(Media.media_id == media_id).first()) is None:
raise HTTPException(status_code=404, detail="Media not found")
# TODO: Create report
return APIResponse(msg="Success")

View File

@ -50,8 +50,15 @@ from config import (
FORCE_EMAIL_VERIFICATION,
UNVERIFIED_MANAGER,
)
from responses import Response as APIResponse, UnprocessableEntity, BadRequest, NotFound, \
MediaTypeNotAcceptable, PayloadTooLarge, InternalServerError
from responses import (
Response as APIResponse,
UnprocessableEntity,
BadRequest,
NotFound,
MediaTypeNotAcceptable,
PayloadTooLarge,
InternalServerError,
)
from responses.users import (
PrivateUserResponse,
PublicUserResponse,
@ -88,12 +95,21 @@ async def get_self_by_id_unverified(public_id: UUID) -> UserModel:
# Here follow our *beautifully* documented path operations
@router.post("/user", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity}})
@router.post(
"/user",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("5/minute")
async def login(request: Request, response: Response, data: OAuth2PasswordRequestForm = Depends()):
async def login(
request: Request, response: Response, data: OAuth2PasswordRequestForm = Depends()
):
"""
Performs user authentication. Endpoint is limited to 5 hits per minute
"""
@ -120,9 +136,9 @@ async def login(request: Request, response: Response, data: OAuth2PasswordReques
detail="Authentication failed: invalid characters in password",
)
if not (
user := await get_user_by_username(
username, include_secrets=True, restricted_ok=True
)
user := await get_user_by_username(
username, include_secrets=True, restricted_ok=True
)
):
raise HTTPException(
status_code=413,
@ -134,7 +150,8 @@ async def login(request: Request, response: Response, data: OAuth2PasswordReques
detail="Authentication failed: password mismatch",
)
token = MANAGER.create_access_token(
expires=timedelta(seconds=SESSION_EXPIRE_LIMIT), data={"sub": str(user.public_id)}
expires=timedelta(seconds=SESSION_EXPIRE_LIMIT),
data={"sub": str(user.public_id)},
)
response.set_cookie(
secure=SECURE_COOKIE,
@ -149,11 +166,16 @@ async def login(request: Request, response: Response, data: OAuth2PasswordReques
return APIResponse(status_code=200, msg="Authentication successful")
@router.get("/user/logout", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
422: {"model": UnprocessableEntity}})
@router.get(
"/user/logout",
tags=["Users"],
status_code=200,
responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}},
)
@LIMITER.limit("5/minute")
async def logout(request: Request, response: Response, _user: UserModel = Depends(UNVERIFIED_MANAGER)):
async def logout(
request: Request, response: Response, _user: UserModel = Depends(UNVERIFIED_MANAGER)
):
"""
Deletes a user's session cookie, logging them
out. Endpoint is limited to 5 hits per minute
@ -174,8 +196,13 @@ async def logout(request: Request, response: Response, _user: UserModel = Depend
"/user/me",
tags=["Users"],
status_code=200,
responses={200: {"model": PrivateUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
422: {"model": UnprocessableEntity}},
responses={
200: {
"model": PrivateUserResponse,
"exclude": {"password_hash", "internal_id", "deleted"},
},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("2/second")
async def get_self(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGER)):
@ -194,14 +221,18 @@ async def get_self(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGE
"/user/username/{username}",
tags=["Users"],
status_code=200,
responses={200: {"model": PublicUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
404: {"model": NotFound},
422: {"model": UnprocessableEntity}
},
responses={
200: {
"model": PublicUserResponse,
"exclude": {"password_hash", "internal_id", "deleted"},
},
404: {"model": NotFound},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("30/second")
async def get_user_by_name(
request: Request, username: str, _auth: UserModel = Depends(MANAGER)
request: Request, username: str, _auth: UserModel = Depends(MANAGER)
):
"""
Fetches a single user by its public username
@ -216,31 +247,37 @@ async def get_user_by_name(
"/user/id/{public_id}",
tags=["Users"],
status_code=200,
responses={200: {"model": PublicUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
404: {"model": NotFound},
422: {"model": UnprocessableEntity}
},
responses={
200: {
"model": PublicUserResponse,
"exclude": {"password_hash", "internal_id", "deleted"},
},
404: {"model": NotFound},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("30/second")
async def get_user_by_public_id(
request: Request, public_id: str, _auth: UserModel = Depends(MANAGER)
request: Request, public_id: str, _auth: UserModel = Depends(MANAGER)
):
"""
Fetches a single user by its public ID
"""
if not (user := await get_user_by_id(UUID(public_id))):
raise HTTPException(status_code=404, detail="Lookup failed: the user does not exist")
raise HTTPException(
status_code=404, detail="Lookup failed: the user does not exist"
)
return PublicUserResponse(data=user)
async def validate_user(
first_name: str | None,
last_name: str | None,
username: str | None,
email: str | None,
password: str | None,
bio: str | None,
first_name: str | None,
last_name: str | None,
username: str | None,
email: str | None,
password: str | None,
bio: str | None,
):
"""
Performs some validation upon user creation. Returns
@ -261,9 +298,9 @@ async def validate_user(
if username and len(username) > 32:
return False, "username is too long"
if (
username
and VALIDATE_USERNAME_REGEX
and not re.match(VALIDATE_USERNAME_REGEX, username)
username
and VALIDATE_USERNAME_REGEX
and not re.match(VALIDATE_USERNAME_REGEX, username)
):
return False, "username is invalid"
if email and not validators.email(email):
@ -271,13 +308,13 @@ async def validate_user(
if password and len(password) > 72:
return False, "password is too long"
if (
password
and VALIDATE_PASSWORD_REGEX
and not re.match(VALIDATE_PASSWORD_REGEX, password)
password
and VALIDATE_PASSWORD_REGEX
and not re.match(VALIDATE_PASSWORD_REGEX, password)
):
return False, "password is too weak"
if username and await get_user_by_username(
username, deleted_ok=True, restricted_ok=True
username, deleted_ok=True, restricted_ok=True
):
return False, "username is already taken"
if email and await get_user_by_email(email, deleted_ok=True, restricted_ok=True):
@ -292,12 +329,15 @@ async def validate_user(
return True, ""
@router.delete("/user", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
422: {"model": UnprocessableEntity}})
@router.delete(
"/user",
tags=["Users"],
status_code=200,
responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}},
)
@LIMITER.limit("1/minute")
async def delete(
request: Request, response: Response, user: UserModel = Depends(UNVERIFIED_MANAGER)
request: Request, response: Response, user: UserModel = Depends(UNVERIFIED_MANAGER)
):
"""
Sets the user's deleted flag in the database,
@ -317,25 +357,33 @@ async def delete(
return APIResponse(status_code=200, msg="Success")
@router.get("/user/verifyEmail/{verification_id}", tags=["Users"], status_code=200,
responses={200: {"model": PrivateUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
404: {"model": NotFound},
422: {"model": UnprocessableEntity}
})
@router.get(
"/user/verifyEmail/{verification_id}",
tags=["Users"],
status_code=200,
responses={
200: {
"model": PrivateUserResponse,
"exclude": {"password_hash", "internal_id", "deleted"},
},
404: {"model": NotFound},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("3/second")
async def verify_email(
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
):
"""
Verifies a user's email address
"""
if not (
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
):
raise HTTPException(status_code=404, detail="Verification ID is invalid")
elif not verification["pending"]:
@ -351,29 +399,37 @@ async def verify_email(
await EmailVerification.update({EmailVerification.pending: False}).where(
EmailVerification.user == user.public_id
)
await User.update({User.email_verified: True}).where(User.public_id == user.public_id)
await User.update({User.email_verified: True}).where(
User.public_id == user.public_id
)
return APIResponse(status_code=200, msg="Verification successful")
@router.get("/user/resetPassword/{verification_id}", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
404: {"model": NotFound}})
@router.get(
"/user/resetPassword/{verification_id}",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
404: {"model": NotFound},
},
)
@LIMITER.limit("3/second")
async def reset_password(
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
):
"""
Modifies a user's password
"""
if not (
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
):
raise HTTPException(status_code=404, detail="Request ID is invalid")
elif not verification["pending"]:
@ -395,25 +451,31 @@ async def reset_password(
return APIResponse(status_code=200, msg="Password updated")
@router.get("/user/changeEmail/{verification_id}", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
404: {"model": NotFound}})
@router.get(
"/user/changeEmail/{verification_id}",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
404: {"model": NotFound},
},
)
@LIMITER.limit("3/second")
async def change_email(
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
request: Request,
verification_id: str,
user: UserModel = Depends(UNVERIFIED_MANAGER),
):
"""
Modifies a user's email
"""
if not (
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
verification := await EmailVerification.select(*EmailVerification.all_columns())
.where(EmailVerification.id == verification_id)
.first()
):
raise HTTPException(status_code=404, detail="Request ID is invalid")
elif not verification["pending"]:
@ -438,11 +500,17 @@ async def change_email(
return APIResponse(status_code=200, msg="Email updated")
@router.put("user/resendMail", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError}})
@router.put(
"user/resendMail",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError},
},
)
@LIMITER.limit("6/minute")
async def resend_email(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGER)):
"""
@ -460,30 +528,30 @@ async def resend_email(request: Request, user: UserModel = Depends(UNVERIFIED_MA
email_message = json.load(f)["signup"]
verification_id = uuid.uuid4()
if await send_email(
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user["first_name"],
last_name=user["last_name"],
username=user["username"],
email=user["email_address"],
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user["email_address"],
email_message["subject"].format(
first_name=user["first_name"],
last_name=user["last_name"],
username=user["username"],
email=user["email_address"],
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user["first_name"],
last_name=user["last_name"],
username=user["username"],
email=user["email_address"],
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user["email_address"],
email_message["subject"].format(
first_name=user["first_name"],
last_name=user["last_name"],
username=user["username"],
email=user["email_address"],
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
):
await EmailVerification.update(
{
@ -493,26 +561,35 @@ async def resend_email(request: Request, user: UserModel = Depends(UNVERIFIED_MA
)
return APIResponse(status_code=200, msg="Success")
else:
raise HTTPException(status_code=500, detail="An error occurred while trying to resend the email,"
" please try again later")
raise HTTPException(
status_code=500,
detail="An error occurred while trying to resend the email,"
" please try again later",
)
@router.put("/user", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError},
413: {"model": PayloadTooLarge}})
@router.put(
"/user",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError},
413: {"model": PayloadTooLarge},
},
)
@LIMITER.limit("2/minute")
async def signup(
request: Request,
first_name: str,
last_name: str,
username: str,
email: str,
password: str,
bio: str | None = None,
locale: str = "en_US",
request: Request,
first_name: str,
last_name: str,
username: str,
email: str,
password: str,
bio: str | None = None,
locale: str = "en_US",
):
"""
Endpoint used to create new users
@ -546,30 +623,30 @@ async def signup(
email_message = json.load(f)["signup"]
verification_id = uuid.uuid4()
if await send_email(
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=first_name,
last_name=last_name,
username=username,
email=email,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
email,
email_message["subject"].format(
first_name=first_name,
last_name=last_name,
username=username,
email=email,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=first_name,
last_name=last_name,
username=username,
email=email,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
email,
email_message["subject"].format(
first_name=first_name,
last_name=last_name,
username=username,
email=email,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
):
await User.insert(user)
await EmailVerification.insert(
@ -585,12 +662,12 @@ async def signup(
raise HTTPException(
status_code=500,
detail="An error occurred while sending verification email, please"
" try again later",
" try again later",
)
async def validate_profile_picture(
file: UploadFile,
file: UploadFile,
) -> tuple[bool | None, str, bytes, str]:
"""
Validates a profile picture's size and content to see if it fits
@ -619,23 +696,30 @@ async def validate_profile_picture(
return None, "", b"", ""
@router.patch("/user", tags=["Users"], status_code=200,
responses={200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError},
413: {"model": PayloadTooLarge}})
@router.patch(
"/user",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
500: {"model": InternalServerError},
413: {"model": PayloadTooLarge},
415: {"model": MediaTypeNotAcceptable}
},
)
async def update_user(
request: Request,
user: UserModel = Depends(UNVERIFIED_MANAGER),
first_name: str | None = None,
last_name: str | None = None,
username: str | None = None,
profile_picture: UploadFile | None = None,
email_address: str | None = None,
password: str | None = None,
bio: str | None = None,
delete: bool = False,
request: Request,
user: UserModel = Depends(UNVERIFIED_MANAGER),
first_name: str | None = None,
last_name: str | None = None,
username: str | None = None,
profile_picture: UploadFile | None = None,
email_address: str | None = None,
password: str | None = None,
bio: str | None = None,
delete: bool = False,
):
"""
Updates a user's profile information. Parameters that are not specified are left unchanged unless
@ -650,7 +734,7 @@ async def update_user(
"""
if not delete and not any(
(first_name, last_name, username, profile_picture, email_address, bio, password)
(first_name, last_name, username, profile_picture, email_address, bio, password)
):
raise HTTPException(
status_code=400, detail="At least one value has to be specified"
@ -678,14 +762,7 @@ async def update_user(
)
elif result is None:
raise HTTPException(status_code=413, detail="The file is too large")
elif (
await (
old_media := Media.select(Media.media_id)
.where(Media.media_id == digest)
.first()
)
is None
):
elif (m := await Media.select(Media.media_id).where(Media.media_id == digest).first()) is None:
# This media hasn't been already uploaded (either by this user or by someone
# else), so we save it now. If it has been already uploaded, there's no need
# to do it again (that's what the hash is for)
@ -726,30 +803,30 @@ async def update_user(
email_message = json.load(f)["password_change"]
verification_id = uuid.uuid4()
if not await send_email(
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/resetPassword/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user.email_address,
email_message["subject"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/resetPassword/{verification_id}",
platformName=PLATFORM_NAME,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user.email_address,
email_message["subject"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
):
raise HTTPException(
500,
@ -782,31 +859,31 @@ async def update_user(
email_message = json.load(f)["email_change"]
verification_id = uuid.uuid4()
if not await send_email(
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/changeEmail/{verification_id}",
newMail=email_address,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user.email_address,
email_message["subject"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
SMTP_HOST,
SMTP_PORT,
email_message["content"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/changeEmail/{verification_id}",
newMail=email_address,
),
SMTP_TIMEOUT,
SMTP_FROM_USER,
user.email_address,
email_message["subject"].format(
first_name=user.first_name,
last_name=user.last_name,
username=user.username,
email=user.email_address,
platformName=PLATFORM_NAME,
),
SMTP_USER,
SMTP_PASSWORD,
use_tls=SMTP_USE_TLS,
):
raise HTTPException(
500,
@ -830,8 +907,8 @@ async def update_user(
fields = []
for field in user:
if (
field not in ["email_address", "password"]
and orig_user[field] != user[field]
field not in ["email_address", "password"]
and orig_user[field] != user[field]
):
fields.append((field, user[field]))
if fields:

View File

@ -12,7 +12,7 @@ from slowapi.errors import RateLimitExceeded
from pathlib import Path
from endpoints import users
from endpoints import users, media
from config import (
LOGGER,
LIMITER,
@ -39,7 +39,7 @@ from util.exception_handlers import (
from util.email import test_smtp
with (Path(__file__).parent / "README.md").resolve(strict=True).open() as f:
with (Path(__file__).parent / "API.md").resolve(strict=True).open() as f:
description = f.read()
@ -67,6 +67,10 @@ app = FastAPI(
"name": "Posts",
"description": "Endpoints that handle post creation, modification and deletion",
},
{
"name": "Media",
"description": "Endpoints that allow fetching individual media objects by their ID"
}
],
)
@ -140,6 +144,7 @@ if __name__ == "__main__":
LOGGER.info("Backend starting up!")
LOGGER.debug("Including modules")
app.include_router(users.router)
app.include_router(media.router)
app.state.limiter = LIMITER
LOGGER.debug("Setting exception handlers")
app.add_exception_handler(RateLimitExceeded, rate_limited)

View File

@ -4,7 +4,17 @@ Media relation
from piccolo.table import Table
from piccolo.utils.pydantic import create_pydantic_model
from piccolo.columns import Text, Boolean, Date, SmallInt, Varchar, Column, ForeignKey, OnUpdate, OnDelete
from piccolo.columns import (
Text,
Boolean,
Date,
SmallInt,
Varchar,
Column,
ForeignKey,
OnUpdate,
OnDelete,
)
from piccolo.columns.defaults.date import DateNow
from enum import Enum, auto
from typing import Any
@ -35,7 +45,9 @@ class Media(Table):
MediaModel = create_pydantic_model(Media)
PublicMediaModel = create_pydantic_model(Media, exclude_columns=(Media.flagged, Media.deleted, Media.media_type))
PublicMediaModel = create_pydantic_model(
Media, exclude_columns=(Media.flagged, Media.deleted, Media.media_type)
)
async def get_media_by_column(

12
responses/media.py Normal file
View File

@ -0,0 +1,12 @@
from responses import Response
from orm.media import PublicMediaModel
class MediaResponse(Response):
"""
A media response object
"""
status_code: int = 200
msg: str = "Lookup successful"
data: PublicMediaModel

View File

@ -21,8 +21,13 @@ async def rate_limited(request: Request, error: RateLimitExceeded) -> JSONRespon
f"{request.client.host} got rate-limited at {str(request.url)} "
f"(exceeded {error.detail})"
)
return JSONResponse(status_code=200, content=dict(status_code=429,
msg=f"Too many requests, retry after {error.detail[error.detail.find('per') + 4:]}"))
return JSONResponse(
status_code=200,
content=dict(
status_code=429,
msg=f"Too many requests, retry after {error.detail[error.detail.find('per') + 4:]}",
),
)
def not_authenticated(request: Request, _: NotAuthenticated) -> JSONResponse:
@ -31,7 +36,9 @@ def not_authenticated(request: Request, _: NotAuthenticated) -> JSONResponse:
"""
LOGGER.info(f"{request.client.host} failed to authenticate at {str(request.url)}")
return JSONResponse(status_code=200, content=dict(status_code=401, msg="Authentication is required"))
return JSONResponse(
status_code=200, content=dict(status_code=401, msg="Authentication is required")
)
def request_invalid(request: Request, exc: RequestValidationError) -> JSONResponse:
@ -42,7 +49,10 @@ def request_invalid(request: Request, exc: RequestValidationError) -> JSONRespon
LOGGER.info(
f"{request.client.host} sent an invalid request at {request.url!r}: {type(exc).__name__}: {exc}"
)
return JSONResponse(status_code=200, content=dict(status_code=400, msg=f"Bad request: {type(exc).__name__}: {exc}"))
return JSONResponse(
status_code=200,
content=dict(status_code=400, msg=f"Bad request: {type(exc).__name__}: {exc}"),
)
def http_exception(
@ -58,12 +68,16 @@ def http_exception(
f"{request.client.host} raised a {exc.status_code} error at {request.url!r}:"
f"{type(exc).__name__}: {exc}"
)
return JSONResponse(status_code=200, content=dict(status_code=500, msg="Internal Server Error"))
return JSONResponse(
status_code=200, content=dict(status_code=500, msg="Internal Server Error")
)
else:
LOGGER.info(
f"{request.client.host} raised an HTTP error ({exc.status_code}) at {str(request.url)}"
)
return JSONResponse(status_code=200, content=dict(status_code=exc.status_code, msg=exc.detail))
return JSONResponse(
status_code=200, content=dict(status_code=exc.status_code, msg=exc.detail)
)
async def generic_error(request: Request, exc: Exception) -> JSONResponse:
@ -75,4 +89,6 @@ async def generic_error(request: Request, exc: Exception) -> JSONResponse:
f"{request.client.host} raised an unexpected error ({type(exc).__name__}: {exc}) at {str(request.url)}"
)
# We can't leak anything about the error, it would be too risky
return JSONResponse(status_code=200, content=dict(status_code=500, msg="Internal Server Error"))
return JSONResponse(
status_code=200, content=dict(status_code=500, msg="Internal Server Error")
)