Skip to content

Commit 28f3435

Browse files
committed
use generic error enums message
1 parent deb3744 commit 28f3435

File tree

12 files changed

+118
-54
lines changed

12 files changed

+118
-54
lines changed

backend/.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"allow": [
44
"Bash(mkdir:*)",
55
"Bash(python:*)",
6-
"Bash(.venv/Scripts/python.exe:*)"
6+
"Bash(.venv/Scripts/python.exe:*)",
7+
"Bash(cat:*)",
8+
"Bash(uv run pytest:*)"
79
]
810
}
911
}

backend/README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/),
1616
From `./backend/` you can install all the dependencies with:
1717

1818
```console
19-
$ uv sync
19+
uv sync
2020
```
2121

2222
Then you can activate the virtual environment with:
2323

2424
```console
25-
$ source .venv/bin/activate
25+
source .venv/bin/activate
2626
```
2727

2828
Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`.
@@ -46,21 +46,21 @@ For example, the directory with the backend code is synchronized in the Docker c
4646
There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again:
4747

4848
```console
49-
$ docker compose watch
49+
docker compose watch
5050
```
5151

5252
There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.
5353

5454
To get inside the container with a `bash` session you can start the stack with:
5555

5656
```console
57-
$ docker compose watch
57+
docker compose watch
5858
```
5959

6060
and then in another terminal, `exec` inside the running container:
6161

6262
```console
63-
$ docker compose exec backend bash
63+
docker compose exec backend bash
6464
```
6565

6666
You should see an output like:
@@ -74,7 +74,7 @@ that means that you are in a `bash` session inside your container, as a `root` u
7474
There you can use the `fastapi run --reload` command to run the debug live reloading server.
7575

7676
```console
77-
$ fastapi run --reload app/main.py
77+
fastapi run --reload app/main.py
7878
```
7979

8080
...it will look like:
@@ -94,7 +94,7 @@ Nevertheless, if it doesn't detect a change but a syntax error, it will just sto
9494
To test the backend run:
9595

9696
```console
97-
$ bash ./scripts/test.sh
97+
bash ./scripts/test.sh
9898
```
9999

100100
The tests run with Pytest, modify and add tests to `./backend/tests/`.
@@ -134,55 +134,55 @@ From the `./backend/` directory, you can run Alembic commands using `uv run`:
134134
* Apply all pending migrations:
135135

136136
```console
137-
$ uv run alembic upgrade head
137+
uv run alembic upgrade head
138138
```
139139

140140
* Create a new migration after changing models:
141141

142142
```console
143-
$ uv run alembic revision --autogenerate -m "Add column last_name to User model"
143+
uv run alembic revision --autogenerate -m "Add column last_name to User model"
144144
```
145145

146146
* View migration history:
147147

148148
```console
149-
$ uv run alembic history
149+
uv run alembic history
150150
```
151151

152152
* Check current database revision:
153153

154154
```console
155-
$ uv run alembic current
155+
uv run alembic current
156156
```
157157

158158
* Rollback the last migration:
159159

160160
```console
161-
$ uv run alembic downgrade -1
161+
uv run alembic downgrade -1
162162
```
163163

164164
### Running Migrations with Docker
165165

166166
* Start an interactive session in the backend container:
167167

168168
```console
169-
$ docker compose exec backend bash
169+
docker compose exec backend bash
170170
```
171171

172172
* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`.
173173

174174
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
175175

176176
```console
177-
$ alembic revision --autogenerate -m "Add column last_name to User model"
177+
alembic revision --autogenerate -m "Add column last_name to User model"
178178
```
179179

180180
* Commit to the git repository the files generated in the alembic directory.
181181

182182
* After creating the revision, run the migration in the database (this is what will actually change the database):
183183

184184
```console
185-
$ alembic upgrade head
185+
alembic upgrade head
186186
```
187187

188188
If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in:
@@ -194,7 +194,7 @@ SQLModel.metadata.create_all(engine)
194194
and comment the line in the file `scripts/prestart.sh` that contains:
195195

196196
```console
197-
$ alembic upgrade head
197+
alembic upgrade head
198198
```
199199

200200
If you don't want to start with the default models and want to remove them / modify them, from the beginning, without having any previous revision, you can remove the revision files (`.py` Python files) under `./backend/app/alembic/versions/`. And then create a first migration as described above.

backend/app/modules/items/routes.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import uuid
22
from typing import Any
33

4-
from fastapi import APIRouter, HTTPException
4+
from fastapi import APIRouter, HTTPException, status
55

66
from app.modules.items.models import Item
7-
from app.modules.items.schemas import ItemCreate, ItemPublic, ItemsPublic, ItemUpdate
87
from app.modules.items.repository import ItemRepository
8+
from app.modules.items.schemas import ItemCreate, ItemPublic, ItemsPublic, ItemUpdate
99
from app.modules.rbac.deps import PermissionsDep
10-
from app.modules.shared import Message, SessionDep
10+
from app.modules.shared import ErrorMessage, Message, SessionDep
1111

1212
router = APIRouter(prefix="/items", tags=["items"])
1313

@@ -39,7 +39,9 @@ def read_item(
3939

4040
item = session.get(Item, id)
4141
if not item:
42-
raise HTTPException(status_code=404, detail="Item not found")
42+
raise HTTPException(
43+
status_code=status.HTTP_404_NOT_FOUND, detail=ErrorMessage.ITEM_NOT_FOUND
44+
)
4345
return item
4446

4547

@@ -69,7 +71,9 @@ def update_item(
6971
repository = ItemRepository(session)
7072
item = repository.get_by_id(id)
7173
if not item:
72-
raise HTTPException(status_code=404, detail="Item not found")
74+
raise HTTPException(
75+
status_code=status.HTTP_404_NOT_FOUND, detail=ErrorMessage.ITEM_NOT_FOUND
76+
)
7377

7478
auth.require_owner(item.owner_id)
7579
return repository.update(db_item=item, item_in=item_in)
@@ -87,7 +91,9 @@ def delete_item(
8791
repository = ItemRepository(session)
8892
item = repository.get_by_id(id)
8993
if not item:
90-
raise HTTPException(status_code=404, detail="Item not found")
94+
raise HTTPException(
95+
status_code=status.HTTP_404_NOT_FOUND, detail=ErrorMessage.ITEM_NOT_FOUND
96+
)
9197

9298
auth.require_owner(item.owner_id)
9399
repository.delete(item)

backend/app/modules/rbac/deps.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import uuid
44
from typing import Annotated
55

6-
from fastapi import Depends, HTTPException
6+
from fastapi import Depends, HTTPException, status
77

88
from app.modules.rbac.service import UserRoleService
99
from app.modules.shared.dependencies import CurrentUser, SessionDep
10+
from app.modules.shared.errors import ErrorMessage
1011
from app.modules.users.models import User
1112

1213

@@ -31,8 +32,8 @@ def check_permission(current_user: CurrentUser, session: SessionDep):
3132
missing = [p for p in permissions if p not in user_permissions]
3233
if missing:
3334
raise HTTPException(
34-
status_code=403,
35-
detail="Access denied",
35+
status_code=status.HTTP_403_FORBIDDEN,
36+
detail=ErrorMessage.ACCESS_DENIED,
3637
)
3738
return current_user
3839

@@ -91,8 +92,8 @@ def require(self, *permissions: str) -> None:
9192
missing = [p for p in permissions if p not in self.permissions]
9293
if missing:
9394
raise HTTPException(
94-
status_code=403,
95-
detail="Access denied",
95+
status_code=status.HTTP_403_FORBIDDEN,
96+
detail=ErrorMessage.ACCESS_DENIED,
9697
)
9798

9899
def require_any(self, *permissions: str) -> None:
@@ -106,8 +107,8 @@ def require_any(self, *permissions: str) -> None:
106107
"""
107108
if not self.has_any(*permissions):
108109
raise HTTPException(
109-
status_code=403,
110-
detail="Access denied",
110+
status_code=status.HTTP_403_FORBIDDEN,
111+
detail=ErrorMessage.ACCESS_DENIED,
111112
)
112113

113114
def require_owner(self, owner_id: uuid.UUID) -> None:
@@ -117,7 +118,9 @@ def require_owner(self, owner_id: uuid.UUID) -> None:
117118
Raises 403 Forbidden if user is not the owner.
118119
"""
119120
if owner_id != self._user.id:
120-
raise HTTPException(status_code=403, detail="Access denied")
121+
raise HTTPException(
122+
status_code=status.HTTP_403_FORBIDDEN, detail=ErrorMessage.ACCESS_DENIED
123+
)
121124

122125
def require_owner_or(self, owner_id: uuid.UUID, permission: str) -> None:
123126
"""
@@ -129,7 +132,9 @@ def require_owner_or(self, owner_id: uuid.UUID, permission: str) -> None:
129132
auth.require_owner_or(item.owner_id, "items:delete")
130133
"""
131134
if owner_id != self._user.id and not self.has(permission):
132-
raise HTTPException(status_code=403, detail="Access denied")
135+
raise HTTPException(
136+
status_code=status.HTTP_403_FORBIDDEN, detail=ErrorMessage.ACCESS_DENIED
137+
)
133138

134139
@property
135140
def user(self) -> User:

backend/app/modules/shared/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
get_current_user,
88
get_db,
99
)
10+
from app.modules.shared.errors import ErrorMessage
1011
from app.modules.shared.schemas import Message
1112

1213
# Note: require_permission moved to app.modules.rbac.deps
1314
# Import directly: from app.modules.rbac.deps import require_permission
1415

1516
__all__ = [
1617
"CurrentUser",
18+
"ErrorMessage",
1719
"Message",
1820
"SessionDep",
1921
"TokenDep",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Common error messages used across the application."""
2+
3+
from enum import StrEnum
4+
5+
6+
class ErrorMessage(StrEnum):
7+
"""Enumeration of common error messages for consistent error handling."""
8+
9+
# Authentication errors (401)
10+
NOT_AUTHENTICATED = "Not authenticated"
11+
INVALID_CREDENTIALS = "Could not validate credentials"
12+
INVALID_TOKEN = "Invalid token"
13+
INVALID_TOKEN_TYPE = "Invalid token type"
14+
REFRESH_TOKEN_NOT_FOUND = "Refresh token not found"
15+
INVALID_REFRESH_TOKEN = "Invalid refresh token"
16+
INCORRECT_EMAIL_OR_PASSWORD = "Incorrect email or password"
17+
18+
# Authorization errors (403)
19+
ACCESS_DENIED = "Access denied"
20+
NOT_ENOUGH_PRIVILEGES = "The user doesn't have enough privileges"
21+
NOT_ENOUGH_PERMISSIONS = "Not enough permissions"
22+
INSUFFICIENT_PERMISSIONS = "Insufficient permissions"
23+
24+
# User errors (400/404)
25+
USER_NOT_FOUND = "User not found"
26+
USER_INACTIVE = "Inactive user"
27+
USER_EMAIL_EXISTS = "The user with this email already exists in the system"
28+
USER_NOT_EXISTS = "The user with this email does not exist in the system"
29+
INCORRECT_PASSWORD = "Incorrect password"
30+
SAME_PASSWORD = "New password cannot be the same as the current one"
31+
SUPERUSER_CANNOT_DELETE_SELF = "Super users are not allowed to delete themselves"
32+
SUPERUSER_CANNOT_MODIFY_SELF = "Super users cannot modify their own record through admin panel"
33+
USERS_CANNOT_MODIFY_OWN_ROLES = "Users cannot modify their own roles through admin panel"
34+
35+
# Resource errors (404)
36+
ITEM_NOT_FOUND = "Item not found"
37+
SHORT_URL_NOT_FOUND = "Short URL not found"
38+
ROLE_NOT_FOUND = "Role not found"
39+
PERMISSION_NOT_FOUND = "Permission not found"
40+
41+
# Conflict errors (409)
42+
ROLE_NAME_EXISTS = "Role with this name already exists"
43+
CUSTOM_CODE_TAKEN = "This custom code is already taken. Please choose another one."
44+
45+
# System errors (400)
46+
CANNOT_DELETE_SYSTEM_ROLES = "Cannot delete system roles"

backend/tests/api/routes/test_items.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sqlmodel import Session
55

66
from app.core.config import settings
7+
from app.modules.shared.errors import ErrorMessage
78
from app.modules.users.models import User
89
from tests.utils.item import create_random_item
910

@@ -50,7 +51,7 @@ def test_read_item_not_found(
5051
)
5152
assert response.status_code == 404
5253
content = response.json()
53-
assert content["detail"] == "Item not found"
54+
assert content["detail"] == ErrorMessage.ITEM_NOT_FOUND
5455

5556

5657
def test_read_item_not_enough_permissions(
@@ -63,7 +64,7 @@ def test_read_item_not_enough_permissions(
6364
)
6465
assert response.status_code == 403
6566
content = response.json()
66-
assert content["detail"] == "Permission 'items:read' required"
67+
assert content["detail"] == ErrorMessage.ACCESS_DENIED
6768

6869

6970
def test_read_items(
@@ -113,7 +114,7 @@ def test_update_item_not_found(
113114
)
114115
assert response.status_code == 404
115116
content = response.json()
116-
assert content["detail"] == "Item not found"
117+
assert content["detail"] == ErrorMessage.ITEM_NOT_FOUND
117118

118119

119120
def test_update_item_not_enough_permissions(
@@ -128,7 +129,7 @@ def test_update_item_not_enough_permissions(
128129
)
129130
assert response.status_code == 403
130131
content = response.json()
131-
assert content["detail"] == "Permission 'items:update' required"
132+
assert content["detail"] == ErrorMessage.ACCESS_DENIED
132133

133134

134135
def test_delete_item(
@@ -157,7 +158,7 @@ def test_delete_item_not_found(
157158
)
158159
assert response.status_code == 404
159160
content = response.json()
160-
assert content["detail"] == "Item not found"
161+
assert content["detail"] == ErrorMessage.ITEM_NOT_FOUND
161162

162163

163164
def test_delete_item_not_enough_permissions(
@@ -170,4 +171,4 @@ def test_delete_item_not_enough_permissions(
170171
)
171172
assert response.status_code == 403
172173
content = response.json()
173-
assert content["detail"] == "Permission 'items:delete' required"
174+
assert content["detail"] == ErrorMessage.ACCESS_DENIED

0 commit comments

Comments
 (0)