Skip to content

Commit 92240fc

Browse files
Moved enums and exceptions to separate files
1 parent 2cb3ca4 commit 92240fc

File tree

10 files changed

+181
-174
lines changed

10 files changed

+181
-174
lines changed

exceptions/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from utils.models import User
2+
3+
4+
class NeedsNewTokens(Exception):
5+
def __init__(self, user: User, access_token: str, refresh_token: str):
6+
self.user = user
7+
self.access_token = access_token
8+
self.refresh_token = refresh_token

exceptions/http_exceptions.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from fastapi import HTTPException, status
2+
from utils.enums import ValidPermissions
3+
4+
class EmailAlreadyRegisteredError(HTTPException):
5+
def __init__(self):
6+
super().__init__(
7+
status_code=409,
8+
detail="This email is already registered"
9+
)
10+
11+
12+
class CredentialsError(HTTPException):
13+
def __init__(self, message: str = "Invalid credentials"):
14+
super().__init__(
15+
status_code=401,
16+
detail=message
17+
)
18+
19+
20+
class AuthenticationError(HTTPException):
21+
def __init__(self):
22+
super().__init__(
23+
status_code=status.HTTP_303_SEE_OTHER,
24+
headers={"Location": "/login"}
25+
)
26+
27+
28+
class PasswordValidationError(HTTPException):
29+
def __init__(self, field: str, message: str):
30+
super().__init__(
31+
status_code=422,
32+
detail={
33+
"field": field,
34+
"message": message
35+
}
36+
)
37+
38+
39+
class PasswordMismatchError(PasswordValidationError):
40+
def __init__(self, field: str = "confirm_password"):
41+
super().__init__(
42+
field=field,
43+
message="The passwords you entered do not match"
44+
)
45+
46+
47+
class InsufficientPermissionsError(HTTPException):
48+
def __init__(self):
49+
super().__init__(
50+
status_code=403,
51+
detail="You don't have permission to perform this action"
52+
)
53+
54+
55+
class EmptyOrganizationNameError(HTTPException):
56+
def __init__(self):
57+
super().__init__(
58+
status_code=400,
59+
detail="Organization name cannot be empty"
60+
)
61+
62+
63+
class OrganizationNotFoundError(HTTPException):
64+
def __init__(self):
65+
super().__init__(
66+
status_code=404,
67+
detail="Organization not found"
68+
)
69+
70+
71+
class OrganizationNameTakenError(HTTPException):
72+
def __init__(self):
73+
super().__init__(
74+
status_code=400,
75+
detail="Organization name already taken"
76+
)
77+
78+
79+
class InvalidPermissionError(HTTPException):
80+
"""Raised when a user attempts to assign an invalid permission to a role"""
81+
82+
def __init__(self, permission: ValidPermissions):
83+
super().__init__(
84+
status_code=400,
85+
detail=f"Invalid permission: {permission}"
86+
)
87+
88+
89+
class RoleAlreadyExistsError(HTTPException):
90+
"""Raised when attempting to create a role with a name that already exists"""
91+
92+
def __init__(self):
93+
super().__init__(status_code=400, detail="Role already exists")
94+
95+
96+
class RoleNotFoundError(HTTPException):
97+
"""Raised when a requested role does not exist"""
98+
99+
def __init__(self):
100+
super().__init__(status_code=404, detail="Role not found")
101+
102+
103+
class RoleHasUsersError(HTTPException):
104+
"""Raised when a requested role to be deleted has users"""
105+
106+
def __init__(self):
107+
super().__init__(
108+
status_code=400,
109+
detail="Role cannot be deleted until users with that role are reassigned"
110+
)
111+
112+
113+
class DataIntegrityError(HTTPException):
114+
def __init__(
115+
self,
116+
resource: str = "Database resource"
117+
):
118+
super().__init__(
119+
status_code=500,
120+
detail=(
121+
f"{resource} is in a broken state; please contact a system administrator"
122+
)
123+
)
124+
125+
126+
class InvalidImageError(HTTPException):
127+
"""Raised when an invalid image is uploaded"""
128+
129+
def __init__(self, message: str = "Invalid image file"):
130+
super().__init__(status_code=400, detail=message)

routers/account.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Optional
44
from urllib.parse import urlparse
55
from datetime import datetime
6-
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Form, Request
6+
from fastapi import APIRouter, Depends, BackgroundTasks, Form, Request
77
from fastapi.responses import RedirectResponse
88
from fastapi.templating import Jinja2Templates
99
from pydantic import BaseModel, EmailStr, ConfigDict
@@ -28,6 +28,7 @@
2828
PasswordValidationError,
2929
get_optional_user
3030
)
31+
from exceptions.http_exceptions import EmailAlreadyRegisteredError, CredentialsError
3132

3233
logger = getLogger("uvicorn.error")
3334

@@ -37,20 +38,7 @@
3738
# --- Custom Exceptions ---
3839

3940

40-
class EmailAlreadyRegisteredError(HTTPException):
41-
def __init__(self):
42-
super().__init__(
43-
status_code=409,
44-
detail="This email is already registered"
45-
)
46-
4741

48-
class AuthenticationError(HTTPException):
49-
def __init__(self, message: str = "Invalid credentials"):
50-
super().__init__(
51-
status_code=401,
52-
detail=message
53-
)
5442

5543

5644
# --- Server Request and Response Models ---
@@ -246,7 +234,7 @@ async def read_reset_password(
246234

247235
# Raise informative error to let user know the token is invalid and may have expired
248236
if not authorized_user:
249-
raise HTTPException(status_code=400, detail="Invalid or expired token")
237+
raise CredentialsError(message="Invalid or expired token")
250238

251239
return templates.TemplateResponse(
252240
"authentication/reset_password.html",
@@ -310,7 +298,7 @@ async def login(
310298
User.email == user.email)).first()
311299

312300
if not db_user or not db_user.password or not verify_password(user.password, db_user.password.hashed_password):
313-
raise AuthenticationError()
301+
raise CredentialsError()
314302

315303
# Create access token
316304
access_token = create_access_token(
@@ -416,7 +404,7 @@ async def reset_password(
416404
user.email, user.token, session)
417405

418406
if not authorized_user or not reset_token:
419-
raise AuthenticationError("Invalid or expired password reset token; please request a new one")
407+
raise CredentialsError("Invalid or expired password reset token; please request a new one")
420408

421409
# Update password and mark token as used
422410
if authorized_user.password:
@@ -488,7 +476,7 @@ async def confirm_email_update(
488476
)
489477

490478
if not user or not update_token:
491-
raise AuthenticationError("Invalid or expired email update token; please request a new one")
479+
raise CredentialsError("Invalid or expired email update token; please request a new one")
492480

493481
# Update email and mark token as used
494482
user.email = new_email

routers/organization.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,21 @@
11
from logging import getLogger
2-
from fastapi import APIRouter, Depends, HTTPException, Form, Request
2+
from datetime import datetime
3+
from fastapi import APIRouter, Depends, Form, Request
34
from fastapi.responses import RedirectResponse
45
from fastapi.templating import Jinja2Templates
56
from pydantic import BaseModel, ConfigDict, field_validator
67
from sqlmodel import Session, select
78
from utils.db import get_session
8-
from utils.auth import get_authenticated_user, get_user_with_relations, InsufficientPermissionsError
9+
from utils.auth import get_authenticated_user, get_user_with_relations
910
from utils.models import Organization, User, Role, utc_time, default_roles, ValidPermissions
10-
from datetime import datetime
11+
from exceptions.http_exceptions import EmptyOrganizationNameError, OrganizationNotFoundError, OrganizationNameTakenError, InsufficientPermissionsError
1112

1213
logger = getLogger("uvicorn.error")
1314

1415
router = APIRouter(prefix="/organizations", tags=["organizations"])
1516
templates = Jinja2Templates(directory="templates")
1617

1718

18-
# --- Custom Exceptions ---
19-
20-
21-
class EmptyOrganizationNameError(HTTPException):
22-
def __init__(self):
23-
super().__init__(
24-
status_code=400,
25-
detail="Organization name cannot be empty"
26-
)
27-
28-
29-
class OrganizationNotFoundError(HTTPException):
30-
def __init__(self):
31-
super().__init__(
32-
status_code=404,
33-
detail="Organization not found"
34-
)
35-
36-
37-
class OrganizationNameTakenError(HTTPException):
38-
def __init__(self):
39-
super().__init__(
40-
status_code=400,
41-
detail="Organization name already taken"
42-
)
43-
44-
4519
# --- Server Request and Response Models ---
4620

4721

routers/role.py

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,20 @@
22
# they themselves have.
33
from typing import List, Sequence, Optional
44
from logging import getLogger
5-
from fastapi import APIRouter, Depends, Form, HTTPException
5+
from fastapi import APIRouter, Depends, Form
66
from fastapi.responses import RedirectResponse
77
from pydantic import BaseModel, ConfigDict
88
from sqlmodel import Session, select, col
99
from sqlalchemy.orm import selectinload
1010
from utils.db import get_session
11-
from utils.auth import get_authenticated_user, InsufficientPermissionsError
11+
from utils.auth import get_authenticated_user
1212
from utils.models import Role, Permission, ValidPermissions, utc_time, User, DataIntegrityError
13+
from exceptions.http_exceptions import InsufficientPermissionsError, InvalidPermissionError, RoleAlreadyExistsError, RoleNotFoundError, RoleHasUsersError
1314

1415
logger = getLogger("uvicorn.error")
1516

1617
router = APIRouter(prefix="/roles", tags=["roles"])
1718

18-
19-
# --- Custom Exceptions ---
20-
21-
22-
class InvalidPermissionError(HTTPException):
23-
"""Raised when a user attempts to assign an invalid permission to a role"""
24-
25-
def __init__(self, permission: ValidPermissions):
26-
super().__init__(
27-
status_code=400,
28-
detail=f"Invalid permission: {permission}"
29-
)
30-
31-
32-
class RoleAlreadyExistsError(HTTPException):
33-
"""Raised when attempting to create a role with a name that already exists"""
34-
35-
def __init__(self):
36-
super().__init__(status_code=400, detail="Role already exists")
37-
38-
39-
class RoleNotFoundError(HTTPException):
40-
"""Raised when a requested role does not exist"""
41-
42-
def __init__(self):
43-
super().__init__(status_code=404, detail="Role not found")
44-
45-
46-
class RoleHasUsersError(HTTPException):
47-
"""Raised when a requested role to be deleted has users"""
48-
49-
def __init__(self):
50-
super().__init__(
51-
status_code=400,
52-
detail="Role cannot be deleted until users with that role are reassigned"
53-
)
54-
55-
5619
# --- Server Request Models ---
5720

5821
class RoleCreate(BaseModel):

utils/auth.py

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
from typing import Optional
1515
from jinja2.environment import Template
1616
from fastapi.templating import Jinja2Templates
17-
from fastapi import Depends, Cookie, HTTPException, status, Request
17+
from fastapi import Depends, Cookie
1818
from utils.db import get_session
1919
from utils.models import User, Role, PasswordResetToken, EmailUpdateToken
20+
from exceptions.http_exceptions import PasswordValidationError, PasswordMismatchError, AuthenticationError
21+
from exceptions.exceptions import NeedsNewTokens
2022

2123
load_dotenv()
2224
resend.api_key = os.environ["RESEND_API_KEY"]
@@ -84,51 +86,6 @@ def replacer(match: re.Match) -> str:
8486
)
8587

8688

87-
# --- Custom Exceptions ---
88-
89-
90-
class NeedsNewTokens(Exception):
91-
def __init__(self, user: User, access_token: str, refresh_token: str):
92-
self.user = user
93-
self.access_token = access_token
94-
self.refresh_token = refresh_token
95-
96-
97-
class AuthenticationError(HTTPException):
98-
def __init__(self):
99-
super().__init__(
100-
status_code=status.HTTP_303_SEE_OTHER,
101-
headers={"Location": "/login"}
102-
)
103-
104-
105-
class PasswordValidationError(HTTPException):
106-
def __init__(self, field: str, message: str):
107-
super().__init__(
108-
status_code=422,
109-
detail={
110-
"field": field,
111-
"message": message
112-
}
113-
)
114-
115-
116-
class PasswordMismatchError(PasswordValidationError):
117-
def __init__(self, field: str = "confirm_password"):
118-
super().__init__(
119-
field=field,
120-
message="The passwords you entered do not match"
121-
)
122-
123-
124-
class InsufficientPermissionsError(HTTPException):
125-
def __init__(self):
126-
super().__init__(
127-
status_code=403,
128-
detail="You don't have permission to perform this action"
129-
)
130-
131-
13289
# --- Helpers ---
13390

13491

0 commit comments

Comments
 (0)