Added media endpoints, various style modifications and minor fixes, added API.md
This commit is contained in:
parent
efa1ebf669
commit
1da075f48f
|
@ -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.
|
|
@ -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")
|
|
@ -50,8 +50,15 @@ from config import (
|
||||||
FORCE_EMAIL_VERIFICATION,
|
FORCE_EMAIL_VERIFICATION,
|
||||||
UNVERIFIED_MANAGER,
|
UNVERIFIED_MANAGER,
|
||||||
)
|
)
|
||||||
from responses import Response as APIResponse, UnprocessableEntity, BadRequest, NotFound, \
|
from responses import (
|
||||||
MediaTypeNotAcceptable, PayloadTooLarge, InternalServerError
|
Response as APIResponse,
|
||||||
|
UnprocessableEntity,
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
MediaTypeNotAcceptable,
|
||||||
|
PayloadTooLarge,
|
||||||
|
InternalServerError,
|
||||||
|
)
|
||||||
from responses.users import (
|
from responses.users import (
|
||||||
PrivateUserResponse,
|
PrivateUserResponse,
|
||||||
PublicUserResponse,
|
PublicUserResponse,
|
||||||
|
@ -88,12 +95,21 @@ async def get_self_by_id_unverified(public_id: UUID) -> UserModel:
|
||||||
|
|
||||||
# Here follow our *beautifully* documented path operations
|
# Here follow our *beautifully* documented path operations
|
||||||
|
|
||||||
@router.post("/user", tags=["Users"], status_code=200,
|
|
||||||
responses={200: {"model": APIResponse},
|
@router.post(
|
||||||
400: {"model": BadRequest},
|
"/user",
|
||||||
422: {"model": UnprocessableEntity}})
|
tags=["Users"],
|
||||||
|
status_code=200,
|
||||||
|
responses={
|
||||||
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("5/minute")
|
@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
|
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",
|
detail="Authentication failed: invalid characters in password",
|
||||||
)
|
)
|
||||||
if not (
|
if not (
|
||||||
user := await get_user_by_username(
|
user := await get_user_by_username(
|
||||||
username, include_secrets=True, restricted_ok=True
|
username, include_secrets=True, restricted_ok=True
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=413,
|
status_code=413,
|
||||||
|
@ -134,7 +150,8 @@ async def login(request: Request, response: Response, data: OAuth2PasswordReques
|
||||||
detail="Authentication failed: password mismatch",
|
detail="Authentication failed: password mismatch",
|
||||||
)
|
)
|
||||||
token = MANAGER.create_access_token(
|
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(
|
response.set_cookie(
|
||||||
secure=SECURE_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")
|
return APIResponse(status_code=200, msg="Authentication successful")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/logout", tags=["Users"], status_code=200,
|
@router.get(
|
||||||
responses={200: {"model": APIResponse},
|
"/user/logout",
|
||||||
422: {"model": UnprocessableEntity}})
|
tags=["Users"],
|
||||||
|
status_code=200,
|
||||||
|
responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}},
|
||||||
|
)
|
||||||
@LIMITER.limit("5/minute")
|
@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
|
Deletes a user's session cookie, logging them
|
||||||
out. Endpoint is limited to 5 hits per minute
|
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",
|
"/user/me",
|
||||||
tags=["Users"],
|
tags=["Users"],
|
||||||
status_code=200,
|
status_code=200,
|
||||||
responses={200: {"model": PrivateUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
|
responses={
|
||||||
422: {"model": UnprocessableEntity}},
|
200: {
|
||||||
|
"model": PrivateUserResponse,
|
||||||
|
"exclude": {"password_hash", "internal_id", "deleted"},
|
||||||
|
},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@LIMITER.limit("2/second")
|
@LIMITER.limit("2/second")
|
||||||
async def get_self(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGER)):
|
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}",
|
"/user/username/{username}",
|
||||||
tags=["Users"],
|
tags=["Users"],
|
||||||
status_code=200,
|
status_code=200,
|
||||||
responses={200: {"model": PublicUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
|
responses={
|
||||||
404: {"model": NotFound},
|
200: {
|
||||||
422: {"model": UnprocessableEntity}
|
"model": PublicUserResponse,
|
||||||
},
|
"exclude": {"password_hash", "internal_id", "deleted"},
|
||||||
|
},
|
||||||
|
404: {"model": NotFound},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@LIMITER.limit("30/second")
|
@LIMITER.limit("30/second")
|
||||||
async def get_user_by_name(
|
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
|
Fetches a single user by its public username
|
||||||
|
@ -216,31 +247,37 @@ async def get_user_by_name(
|
||||||
"/user/id/{public_id}",
|
"/user/id/{public_id}",
|
||||||
tags=["Users"],
|
tags=["Users"],
|
||||||
status_code=200,
|
status_code=200,
|
||||||
responses={200: {"model": PublicUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
|
responses={
|
||||||
404: {"model": NotFound},
|
200: {
|
||||||
422: {"model": UnprocessableEntity}
|
"model": PublicUserResponse,
|
||||||
},
|
"exclude": {"password_hash", "internal_id", "deleted"},
|
||||||
|
},
|
||||||
|
404: {"model": NotFound},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
@LIMITER.limit("30/second")
|
@LIMITER.limit("30/second")
|
||||||
async def get_user_by_public_id(
|
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
|
Fetches a single user by its public ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not (user := await get_user_by_id(UUID(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)
|
return PublicUserResponse(data=user)
|
||||||
|
|
||||||
|
|
||||||
async def validate_user(
|
async def validate_user(
|
||||||
first_name: str | None,
|
first_name: str | None,
|
||||||
last_name: str | None,
|
last_name: str | None,
|
||||||
username: str | None,
|
username: str | None,
|
||||||
email: str | None,
|
email: str | None,
|
||||||
password: str | None,
|
password: str | None,
|
||||||
bio: str | None,
|
bio: str | None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Performs some validation upon user creation. Returns
|
Performs some validation upon user creation. Returns
|
||||||
|
@ -261,9 +298,9 @@ async def validate_user(
|
||||||
if username and len(username) > 32:
|
if username and len(username) > 32:
|
||||||
return False, "username is too long"
|
return False, "username is too long"
|
||||||
if (
|
if (
|
||||||
username
|
username
|
||||||
and VALIDATE_USERNAME_REGEX
|
and VALIDATE_USERNAME_REGEX
|
||||||
and not re.match(VALIDATE_USERNAME_REGEX, username)
|
and not re.match(VALIDATE_USERNAME_REGEX, username)
|
||||||
):
|
):
|
||||||
return False, "username is invalid"
|
return False, "username is invalid"
|
||||||
if email and not validators.email(email):
|
if email and not validators.email(email):
|
||||||
|
@ -271,13 +308,13 @@ async def validate_user(
|
||||||
if password and len(password) > 72:
|
if password and len(password) > 72:
|
||||||
return False, "password is too long"
|
return False, "password is too long"
|
||||||
if (
|
if (
|
||||||
password
|
password
|
||||||
and VALIDATE_PASSWORD_REGEX
|
and VALIDATE_PASSWORD_REGEX
|
||||||
and not re.match(VALIDATE_PASSWORD_REGEX, password)
|
and not re.match(VALIDATE_PASSWORD_REGEX, password)
|
||||||
):
|
):
|
||||||
return False, "password is too weak"
|
return False, "password is too weak"
|
||||||
if username and await get_user_by_username(
|
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"
|
return False, "username is already taken"
|
||||||
if email and await get_user_by_email(email, deleted_ok=True, restricted_ok=True):
|
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, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/user", tags=["Users"], status_code=200,
|
@router.delete(
|
||||||
responses={200: {"model": APIResponse},
|
"/user",
|
||||||
422: {"model": UnprocessableEntity}})
|
tags=["Users"],
|
||||||
|
status_code=200,
|
||||||
|
responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}},
|
||||||
|
)
|
||||||
@LIMITER.limit("1/minute")
|
@LIMITER.limit("1/minute")
|
||||||
async def delete(
|
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,
|
Sets the user's deleted flag in the database,
|
||||||
|
@ -317,25 +357,33 @@ async def delete(
|
||||||
return APIResponse(status_code=200, msg="Success")
|
return APIResponse(status_code=200, msg="Success")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/verifyEmail/{verification_id}", tags=["Users"], status_code=200,
|
@router.get(
|
||||||
responses={200: {"model": PrivateUserResponse, "exclude": {"password_hash", "internal_id", "deleted"}},
|
"/user/verifyEmail/{verification_id}",
|
||||||
404: {"model": NotFound},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity}
|
status_code=200,
|
||||||
})
|
responses={
|
||||||
|
200: {
|
||||||
|
"model": PrivateUserResponse,
|
||||||
|
"exclude": {"password_hash", "internal_id", "deleted"},
|
||||||
|
},
|
||||||
|
404: {"model": NotFound},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("3/second")
|
@LIMITER.limit("3/second")
|
||||||
async def verify_email(
|
async def verify_email(
|
||||||
request: Request,
|
request: Request,
|
||||||
verification_id: str,
|
verification_id: str,
|
||||||
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Verifies a user's email address
|
Verifies a user's email address
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
||||||
.where(EmailVerification.id == verification_id)
|
.where(EmailVerification.id == verification_id)
|
||||||
.first()
|
.first()
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="Verification ID is invalid")
|
raise HTTPException(status_code=404, detail="Verification ID is invalid")
|
||||||
elif not verification["pending"]:
|
elif not verification["pending"]:
|
||||||
|
@ -351,29 +399,37 @@ async def verify_email(
|
||||||
await EmailVerification.update({EmailVerification.pending: False}).where(
|
await EmailVerification.update({EmailVerification.pending: False}).where(
|
||||||
EmailVerification.user == user.public_id
|
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")
|
return APIResponse(status_code=200, msg="Verification successful")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/resetPassword/{verification_id}", tags=["Users"], status_code=200,
|
@router.get(
|
||||||
responses={200: {"model": APIResponse},
|
"/user/resetPassword/{verification_id}",
|
||||||
400: {"model": BadRequest},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity},
|
status_code=200,
|
||||||
404: {"model": NotFound}})
|
responses={
|
||||||
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
404: {"model": NotFound},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("3/second")
|
@LIMITER.limit("3/second")
|
||||||
async def reset_password(
|
async def reset_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
verification_id: str,
|
verification_id: str,
|
||||||
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Modifies a user's password
|
Modifies a user's password
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
||||||
.where(EmailVerification.id == verification_id)
|
.where(EmailVerification.id == verification_id)
|
||||||
.first()
|
.first()
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="Request ID is invalid")
|
raise HTTPException(status_code=404, detail="Request ID is invalid")
|
||||||
elif not verification["pending"]:
|
elif not verification["pending"]:
|
||||||
|
@ -395,25 +451,31 @@ async def reset_password(
|
||||||
return APIResponse(status_code=200, msg="Password updated")
|
return APIResponse(status_code=200, msg="Password updated")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/changeEmail/{verification_id}", tags=["Users"], status_code=200,
|
@router.get(
|
||||||
responses={200: {"model": APIResponse},
|
"/user/changeEmail/{verification_id}",
|
||||||
400: {"model": BadRequest},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity},
|
status_code=200,
|
||||||
404: {"model": NotFound}})
|
responses={
|
||||||
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
404: {"model": NotFound},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("3/second")
|
@LIMITER.limit("3/second")
|
||||||
async def change_email(
|
async def change_email(
|
||||||
request: Request,
|
request: Request,
|
||||||
verification_id: str,
|
verification_id: str,
|
||||||
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Modifies a user's email
|
Modifies a user's email
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
verification := await EmailVerification.select(*EmailVerification.all_columns())
|
||||||
.where(EmailVerification.id == verification_id)
|
.where(EmailVerification.id == verification_id)
|
||||||
.first()
|
.first()
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="Request ID is invalid")
|
raise HTTPException(status_code=404, detail="Request ID is invalid")
|
||||||
elif not verification["pending"]:
|
elif not verification["pending"]:
|
||||||
|
@ -438,11 +500,17 @@ async def change_email(
|
||||||
return APIResponse(status_code=200, msg="Email updated")
|
return APIResponse(status_code=200, msg="Email updated")
|
||||||
|
|
||||||
|
|
||||||
@router.put("user/resendMail", tags=["Users"], status_code=200,
|
@router.put(
|
||||||
responses={200: {"model": APIResponse},
|
"user/resendMail",
|
||||||
400: {"model": BadRequest},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity},
|
status_code=200,
|
||||||
500: {"model": InternalServerError}})
|
responses={
|
||||||
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
500: {"model": InternalServerError},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("6/minute")
|
@LIMITER.limit("6/minute")
|
||||||
async def resend_email(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGER)):
|
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"]
|
email_message = json.load(f)["signup"]
|
||||||
verification_id = uuid.uuid4()
|
verification_id = uuid.uuid4()
|
||||||
if await send_email(
|
if await send_email(
|
||||||
SMTP_HOST,
|
SMTP_HOST,
|
||||||
SMTP_PORT,
|
SMTP_PORT,
|
||||||
email_message["content"].format(
|
email_message["content"].format(
|
||||||
first_name=user["first_name"],
|
first_name=user["first_name"],
|
||||||
last_name=user["last_name"],
|
last_name=user["last_name"],
|
||||||
username=user["username"],
|
username=user["username"],
|
||||||
email=user["email_address"],
|
email=user["email_address"],
|
||||||
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
|
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}",
|
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_TIMEOUT,
|
SMTP_TIMEOUT,
|
||||||
SMTP_FROM_USER,
|
SMTP_FROM_USER,
|
||||||
user["email_address"],
|
user["email_address"],
|
||||||
email_message["subject"].format(
|
email_message["subject"].format(
|
||||||
first_name=user["first_name"],
|
first_name=user["first_name"],
|
||||||
last_name=user["last_name"],
|
last_name=user["last_name"],
|
||||||
username=user["username"],
|
username=user["username"],
|
||||||
email=user["email_address"],
|
email=user["email_address"],
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_USER,
|
SMTP_USER,
|
||||||
SMTP_PASSWORD,
|
SMTP_PASSWORD,
|
||||||
use_tls=SMTP_USE_TLS,
|
use_tls=SMTP_USE_TLS,
|
||||||
):
|
):
|
||||||
await EmailVerification.update(
|
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")
|
return APIResponse(status_code=200, msg="Success")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="An error occurred while trying to resend the email,"
|
raise HTTPException(
|
||||||
" please try again later")
|
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,
|
@router.put(
|
||||||
responses={200: {"model": APIResponse},
|
"/user",
|
||||||
400: {"model": BadRequest},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity},
|
status_code=200,
|
||||||
500: {"model": InternalServerError},
|
responses={
|
||||||
413: {"model": PayloadTooLarge}})
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
500: {"model": InternalServerError},
|
||||||
|
413: {"model": PayloadTooLarge},
|
||||||
|
},
|
||||||
|
)
|
||||||
@LIMITER.limit("2/minute")
|
@LIMITER.limit("2/minute")
|
||||||
async def signup(
|
async def signup(
|
||||||
request: Request,
|
request: Request,
|
||||||
first_name: str,
|
first_name: str,
|
||||||
last_name: str,
|
last_name: str,
|
||||||
username: str,
|
username: str,
|
||||||
email: str,
|
email: str,
|
||||||
password: str,
|
password: str,
|
||||||
bio: str | None = None,
|
bio: str | None = None,
|
||||||
locale: str = "en_US",
|
locale: str = "en_US",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Endpoint used to create new users
|
Endpoint used to create new users
|
||||||
|
@ -546,30 +623,30 @@ async def signup(
|
||||||
email_message = json.load(f)["signup"]
|
email_message = json.load(f)["signup"]
|
||||||
verification_id = uuid.uuid4()
|
verification_id = uuid.uuid4()
|
||||||
if await send_email(
|
if await send_email(
|
||||||
SMTP_HOST,
|
SMTP_HOST,
|
||||||
SMTP_PORT,
|
SMTP_PORT,
|
||||||
email_message["content"].format(
|
email_message["content"].format(
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
|
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}",
|
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}",
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_TIMEOUT,
|
SMTP_TIMEOUT,
|
||||||
SMTP_FROM_USER,
|
SMTP_FROM_USER,
|
||||||
email,
|
email,
|
||||||
email_message["subject"].format(
|
email_message["subject"].format(
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_USER,
|
SMTP_USER,
|
||||||
SMTP_PASSWORD,
|
SMTP_PASSWORD,
|
||||||
use_tls=SMTP_USE_TLS,
|
use_tls=SMTP_USE_TLS,
|
||||||
):
|
):
|
||||||
await User.insert(user)
|
await User.insert(user)
|
||||||
await EmailVerification.insert(
|
await EmailVerification.insert(
|
||||||
|
@ -585,12 +662,12 @@ async def signup(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="An error occurred while sending verification email, please"
|
detail="An error occurred while sending verification email, please"
|
||||||
" try again later",
|
" try again later",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def validate_profile_picture(
|
async def validate_profile_picture(
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
) -> tuple[bool | None, str, bytes, str]:
|
) -> tuple[bool | None, str, bytes, str]:
|
||||||
"""
|
"""
|
||||||
Validates a profile picture's size and content to see if it fits
|
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"", ""
|
return None, "", b"", ""
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/user", tags=["Users"], status_code=200,
|
@router.patch(
|
||||||
responses={200: {"model": APIResponse},
|
"/user",
|
||||||
400: {"model": BadRequest},
|
tags=["Users"],
|
||||||
422: {"model": UnprocessableEntity},
|
status_code=200,
|
||||||
500: {"model": InternalServerError},
|
responses={
|
||||||
413: {"model": PayloadTooLarge}})
|
200: {"model": APIResponse},
|
||||||
|
400: {"model": BadRequest},
|
||||||
|
422: {"model": UnprocessableEntity},
|
||||||
|
500: {"model": InternalServerError},
|
||||||
|
413: {"model": PayloadTooLarge},
|
||||||
|
415: {"model": MediaTypeNotAcceptable}
|
||||||
|
},
|
||||||
|
)
|
||||||
async def update_user(
|
async def update_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
user: UserModel = Depends(UNVERIFIED_MANAGER),
|
||||||
first_name: str | None = None,
|
first_name: str | None = None,
|
||||||
last_name: str | None = None,
|
last_name: str | None = None,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
profile_picture: UploadFile | None = None,
|
profile_picture: UploadFile | None = None,
|
||||||
email_address: str | None = None,
|
email_address: str | None = None,
|
||||||
password: str | None = None,
|
password: str | None = None,
|
||||||
bio: str | None = None,
|
bio: str | None = None,
|
||||||
delete: bool = False,
|
delete: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Updates a user's profile information. Parameters that are not specified are left unchanged unless
|
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(
|
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(
|
raise HTTPException(
|
||||||
status_code=400, detail="At least one value has to be specified"
|
status_code=400, detail="At least one value has to be specified"
|
||||||
|
@ -678,14 +762,7 @@ async def update_user(
|
||||||
)
|
)
|
||||||
elif result is None:
|
elif result is None:
|
||||||
raise HTTPException(status_code=413, detail="The file is too large")
|
raise HTTPException(status_code=413, detail="The file is too large")
|
||||||
elif (
|
elif (m := await Media.select(Media.media_id).where(Media.media_id == digest).first()) is None:
|
||||||
await (
|
|
||||||
old_media := 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
|
# 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
|
# 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)
|
# 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"]
|
email_message = json.load(f)["password_change"]
|
||||||
verification_id = uuid.uuid4()
|
verification_id = uuid.uuid4()
|
||||||
if not await send_email(
|
if not await send_email(
|
||||||
SMTP_HOST,
|
SMTP_HOST,
|
||||||
SMTP_PORT,
|
SMTP_PORT,
|
||||||
email_message["content"].format(
|
email_message["content"].format(
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email_address,
|
email=user.email_address,
|
||||||
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
|
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}",
|
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/resetPassword/{verification_id}",
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_TIMEOUT,
|
SMTP_TIMEOUT,
|
||||||
SMTP_FROM_USER,
|
SMTP_FROM_USER,
|
||||||
user.email_address,
|
user.email_address,
|
||||||
email_message["subject"].format(
|
email_message["subject"].format(
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email_address,
|
email=user.email_address,
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_USER,
|
SMTP_USER,
|
||||||
SMTP_PASSWORD,
|
SMTP_PASSWORD,
|
||||||
use_tls=SMTP_USE_TLS,
|
use_tls=SMTP_USE_TLS,
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
500,
|
500,
|
||||||
|
@ -782,31 +859,31 @@ async def update_user(
|
||||||
email_message = json.load(f)["email_change"]
|
email_message = json.load(f)["email_change"]
|
||||||
verification_id = uuid.uuid4()
|
verification_id = uuid.uuid4()
|
||||||
if not await send_email(
|
if not await send_email(
|
||||||
SMTP_HOST,
|
SMTP_HOST,
|
||||||
SMTP_PORT,
|
SMTP_PORT,
|
||||||
email_message["content"].format(
|
email_message["content"].format(
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email_address,
|
email=user.email_address,
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
link=f"http{'s' if HAS_HTTPS else ''}://{HOST}"
|
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}",
|
f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/changeEmail/{verification_id}",
|
||||||
newMail=email_address,
|
newMail=email_address,
|
||||||
),
|
),
|
||||||
SMTP_TIMEOUT,
|
SMTP_TIMEOUT,
|
||||||
SMTP_FROM_USER,
|
SMTP_FROM_USER,
|
||||||
user.email_address,
|
user.email_address,
|
||||||
email_message["subject"].format(
|
email_message["subject"].format(
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email_address,
|
email=user.email_address,
|
||||||
platformName=PLATFORM_NAME,
|
platformName=PLATFORM_NAME,
|
||||||
),
|
),
|
||||||
SMTP_USER,
|
SMTP_USER,
|
||||||
SMTP_PASSWORD,
|
SMTP_PASSWORD,
|
||||||
use_tls=SMTP_USE_TLS,
|
use_tls=SMTP_USE_TLS,
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
500,
|
500,
|
||||||
|
@ -830,8 +907,8 @@ async def update_user(
|
||||||
fields = []
|
fields = []
|
||||||
for field in user:
|
for field in user:
|
||||||
if (
|
if (
|
||||||
field not in ["email_address", "password"]
|
field not in ["email_address", "password"]
|
||||||
and orig_user[field] != user[field]
|
and orig_user[field] != user[field]
|
||||||
):
|
):
|
||||||
fields.append((field, user[field]))
|
fields.append((field, user[field]))
|
||||||
if fields:
|
if fields:
|
||||||
|
|
9
main.py
9
main.py
|
@ -12,7 +12,7 @@ from slowapi.errors import RateLimitExceeded
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from endpoints import users
|
from endpoints import users, media
|
||||||
from config import (
|
from config import (
|
||||||
LOGGER,
|
LOGGER,
|
||||||
LIMITER,
|
LIMITER,
|
||||||
|
@ -39,7 +39,7 @@ from util.exception_handlers import (
|
||||||
from util.email import test_smtp
|
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()
|
description = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +67,10 @@ app = FastAPI(
|
||||||
"name": "Posts",
|
"name": "Posts",
|
||||||
"description": "Endpoints that handle post creation, modification and deletion",
|
"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.info("Backend starting up!")
|
||||||
LOGGER.debug("Including modules")
|
LOGGER.debug("Including modules")
|
||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
|
app.include_router(media.router)
|
||||||
app.state.limiter = LIMITER
|
app.state.limiter = LIMITER
|
||||||
LOGGER.debug("Setting exception handlers")
|
LOGGER.debug("Setting exception handlers")
|
||||||
app.add_exception_handler(RateLimitExceeded, rate_limited)
|
app.add_exception_handler(RateLimitExceeded, rate_limited)
|
||||||
|
|
16
orm/media.py
16
orm/media.py
|
@ -4,7 +4,17 @@ Media relation
|
||||||
|
|
||||||
from piccolo.table import Table
|
from piccolo.table import Table
|
||||||
from piccolo.utils.pydantic import create_pydantic_model
|
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 piccolo.columns.defaults.date import DateNow
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -35,7 +45,9 @@ class Media(Table):
|
||||||
|
|
||||||
|
|
||||||
MediaModel = create_pydantic_model(Media)
|
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(
|
async def get_media_by_column(
|
||||||
|
|
|
@ -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
|
|
@ -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"{request.client.host} got rate-limited at {str(request.url)} "
|
||||||
f"(exceeded {error.detail})"
|
f"(exceeded {error.detail})"
|
||||||
)
|
)
|
||||||
return JSONResponse(status_code=200, content=dict(status_code=429,
|
return JSONResponse(
|
||||||
msg=f"Too many requests, retry after {error.detail[error.detail.find('per') + 4:]}"))
|
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:
|
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)}")
|
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:
|
def request_invalid(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||||
|
@ -42,7 +49,10 @@ def request_invalid(request: Request, exc: RequestValidationError) -> JSONRespon
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
f"{request.client.host} sent an invalid request at {request.url!r}: {type(exc).__name__}: {exc}"
|
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(
|
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"{request.client.host} raised a {exc.status_code} error at {request.url!r}:"
|
||||||
f"{type(exc).__name__}: {exc}"
|
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:
|
else:
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
f"{request.client.host} raised an HTTP error ({exc.status_code}) at {str(request.url)}"
|
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:
|
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)}"
|
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
|
# 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")
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue