Skip to content

Commit 5b11eb9

Browse files
committed
edit authentication and config
1 parent 1329444 commit 5b11eb9

19 files changed

+177
-234
lines changed

.env

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
POSTGRES_USER=postgres
22
POSTGRES_PASSWORD=1234
3-
POSTGRES_DB=postgres
3+
POSTGRES_DB=postgres
4+
5+
DB_USER = postgres
6+
DB_PASSWORD = 1234
7+
DB_HOST = notesdb
8+
DB_NAME = postgres
9+
DB_URL = postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}

Makefile

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
run:
2-
uvicorn main:app --reload
2+
docker compose up --build
33

4-
review:
5-
alembic revision --autogenerate -m "Create User model"
6-
74
upgrade:
85
alembic upgrade head
96

107
downgrade:
118
alembic downgrade base
129

13-
generate_migrations: upgrade
10+
generate_migrations:
1411
alembic revision --autogenerate -m "Create User and Note models"

alembic.ini

+1-76
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,11 @@
1-
# A generic, single database configuration.
2-
31
[alembic]
4-
# path to migration scripts
5-
# Use forward slashes (/) also on windows to provide an os agnostic path
62
script_location = migrations
7-
8-
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
9-
# Uncomment the line below if you want the files to be prepended with date and time
10-
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
11-
# for all available tokens
12-
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
13-
14-
# sys.path path, will be prepended to sys.path if present.
15-
# defaults to the current working directory.
163
prepend_sys_path = .
17-
18-
# timezone to use when rendering the date within the migration file
19-
# as well as the filename.
20-
# If specified, requires the python>=3.9 or backports.zoneinfo library.
21-
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
22-
# string value is passed to ZoneInfo()
23-
# leave blank for localtime
24-
# timezone =
25-
26-
# max length of characters to apply to the "slug" field
27-
# truncate_slug_length = 40
28-
29-
# set to 'true' to run the environment during
30-
# the 'revision' command, regardless of autogenerate
31-
# revision_environment = false
32-
33-
# set to 'true' to allow .pyc and .pyo files without
34-
# a source .py file to be detected as revisions in the
35-
# versions/ directory
36-
# sourceless = false
37-
38-
# version location specification; This defaults
39-
# to alembic/versions. When using multiple version
40-
# directories, initial revisions must be specified with --version-path.
41-
# The path separator used here should be the separator specified by "version_path_separator" below.
42-
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43-
44-
# version path separator; As mentioned above, this is the character used to split
45-
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46-
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47-
# Valid values for version_path_separator are:
48-
#
49-
# version_path_separator = :
50-
# version_path_separator = ;
51-
# version_path_separator = space
52-
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53-
54-
# set to 'true' to search source files recursively
55-
# in each "version_locations" directory
56-
# new in Alembic version 1.10
57-
# recursive_version_locations = false
58-
59-
# the output encoding used when revision files
60-
# are written from script.py.mako
61-
# output_encoding = utf-8
62-
4+
version_path_separator = os
635
sqlalchemy.url = postgresql://postgres:1234@notesdb/postgres
646

65-
667
[post_write_hooks]
67-
# post_write_hooks defines scripts or Python functions that are run
68-
# on newly generated revision scripts. See the documentation for further
69-
# detail and examples
70-
71-
# format using "black" - use the console_scripts runner, against the "black" entrypoint
72-
# hooks = black
73-
# black.type = console_scripts
74-
# black.entrypoint = black
75-
# black.options = -l 79 REVISION_SCRIPT_FILENAME
76-
77-
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
78-
# hooks = ruff
79-
# ruff.type = exec
80-
# ruff.executable = %(here)s/.venv/bin/ruff
81-
# ruff.options = --fix REVISION_SCRIPT_FILENAME
828

83-
# Logging configuration
849
[loggers]
8510
keys = root,sqlalchemy,alembic
8611

models/notes_models.py renamed to api/models/notes_models.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, func
2-
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, relationship
1+
from sqlalchemy import Integer, Text, DateTime, ForeignKey, func
2+
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
33

44
from datetime import datetime
5-
from models.user_models import UserModel
5+
from api.models.user_models import UserModel
66

77

88
class Base(DeclarativeBase):
File renamed without changes.

repository/auth_repository.py renamed to api/repository/auth_repository.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55
from datetime import datetime, timedelta
66
from typing import Annotated
77

8-
from models.user_models import UserModel
9-
from schemas.user_schemas import (
8+
from api.models.user_models import UserModel
9+
from api.schemas.user_schemas import (
1010
CreateUserInput,
1111
bcrypt_context,
1212
SECRET_KEY,
1313
ALGORITHM,
1414
oauth2_bearer,
1515
)
1616
from config.database import SessionLocal
17+
from helpers.helpers import NotFoundError
1718

1819

1920
class AuthRepository:
2021
@classmethod
2122
async def create_user(cls, data: CreateUserInput) -> int:
23+
if await cls.is_existing_user(user=data):
24+
raise NotFoundError
2225
async with SessionLocal() as session:
2326
user = UserModel(
2427
name=data.name, password_hash=bcrypt_context.hash(data.password)
@@ -40,6 +43,16 @@ async def authenticate_user(cls, name: str, password: str):
4043
return False
4144
return user
4245

46+
@classmethod
47+
async def is_existing_user(cls, user: UserModel) -> bool:
48+
async with SessionLocal() as session:
49+
query = select(UserModel).where(UserModel.name == user.name)
50+
result = await session.execute(query)
51+
user = result.scalars().one_or_none()
52+
if not user:
53+
return False
54+
return True
55+
4356
@classmethod
4457
def create_access_token(cls, name: str, id: int, ttl: timedelta):
4558
data_to_encode = {"sub": name, "id": id}

repository/notes_repository.py renamed to api/repository/notes_repository.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
from sqlalchemy.future import select
22

33
from config.database import SessionLocal
4-
from schemas.notes_schemas import NoteOutput, NoteInput
5-
from models.notes_models import NoteModel
4+
from api.schemas.notes_schemas import NoteOutput, NoteInput
5+
from api.models.notes_models import NoteModel
66
from helpers.helpers import NotFoundError
77

88

99
class NotesRepository:
1010
@classmethod
11-
async def add_note(cls, data: NoteInput) -> int:
11+
async def add_note(cls, data: NoteInput, user_id: int) -> int:
1212
async with SessionLocal() as session:
1313
note_dict = data.model_dump()
1414
note = NoteModel(**note_dict)
15-
print(note, "HEREEEEE")
15+
note.user_id = user_id
1616
session.add(note)
1717
await session.commit()
1818
await session.refresh(note)
1919
return note.id
2020

2121
@classmethod
22-
async def get_notes(cls) -> list[NoteOutput]:
22+
async def get_notes(cls, user_id: int) -> list[NoteOutput]:
2323
async with SessionLocal() as session:
24-
query = select(NoteModel)
24+
query = select(NoteModel).where(NoteModel.user_id == user_id)
2525
result = await session.execute(query)
2626
note_models = result.scalars().all()
2727
notes_schemas = [
@@ -30,9 +30,11 @@ async def get_notes(cls) -> list[NoteOutput]:
3030
return notes_schemas
3131

3232
@classmethod
33-
async def get_note(cls, note_id: int) -> NoteOutput:
33+
async def get_note(cls, note_id: int, user_id: int) -> NoteOutput:
3434
async with SessionLocal() as session:
35-
query = select(NoteModel).where(NoteModel.id == note_id)
35+
query = select(NoteModel).where(
36+
NoteModel.id == note_id, NoteModel.user_id == user_id
37+
)
3638
result = await session.execute(query)
3739
note_model = result.scalars().one_or_none()
3840
if not note_model:
@@ -41,9 +43,13 @@ async def get_note(cls, note_id: int) -> NoteOutput:
4143
return note
4244

4345
@classmethod
44-
async def update_note(cls, note_id: int, data: NoteInput) -> NoteOutput:
46+
async def update_note(
47+
cls, note_id: int, data: NoteInput, user_id: int
48+
) -> NoteOutput:
4549
async with SessionLocal() as session:
46-
query = select(NoteModel).where(NoteModel.id == note_id)
50+
query = select(NoteModel).where(
51+
NoteModel.id == note_id, NoteModel.user_id == user_id
52+
)
4753
result = await session.execute(query)
4854
note_model = result.scalars().one_or_none()
4955
if not note_model:
@@ -58,9 +64,11 @@ async def update_note(cls, note_id: int, data: NoteInput) -> NoteOutput:
5864
return note
5965

6066
@classmethod
61-
async def delete_note(cls, note_id: int):
67+
async def delete_note(cls, note_id: int, user_id: int):
6268
async with SessionLocal() as session:
63-
query = select(NoteModel).where(NoteModel.id == note_id)
69+
query = select(NoteModel).where(
70+
NoteModel.id == note_id, NoteModel.user_id == user_id
71+
)
6472
result = await session.execute(query)
6573
note_model = result.scalars().one_or_none()
6674
if not note_model:

router/auth_routes.py renamed to api/router/auth_routes.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from starlette import status
44

55
from typing import Annotated
6-
from datetime import datetime, timedelta
6+
from datetime import timedelta
77

8-
from repository.auth_repository import AuthRepository
9-
from schemas.user_schemas import CreateUserInput, Token
8+
from api.repository.auth_repository import AuthRepository
9+
from api.schemas.user_schemas import CreateUserInput, Token
10+
from helpers.helpers import NotFoundError
1011

1112

1213
router = APIRouter(
@@ -16,13 +17,21 @@
1617

1718

1819
@router.post("", status_code=status.HTTP_201_CREATED)
19-
async def create_user(data: Annotated[CreateUserInput, Depends()]):
20-
user_id = await AuthRepository.create_user(data)
20+
async def create_user(data: Annotated[CreateUserInput, Depends()]) -> dict:
21+
try:
22+
user_id = await AuthRepository.create_user(data)
23+
except NotFoundError:
24+
raise HTTPException(
25+
status_code=status.HTTP_400_BAD_REQUEST,
26+
detail="User with this name already exists",
27+
)
2128
return {"id": user_id}
2229

2330

2431
@router.post("/token", response_model=Token)
25-
async def get_access_token(data: Annotated[OAuth2PasswordRequestForm, Depends()]):
32+
async def get_access_token(
33+
data: Annotated[OAuth2PasswordRequestForm, Depends()]
34+
) -> dict:
2635
user = await AuthRepository.authenticate_user(data.username, data.password)
2736
if not user:
2837
raise HTTPException(

api/router/note_routes.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from starlette import status
3+
4+
from typing import Annotated
5+
6+
from api.schemas.notes_schemas import NoteInput, NoteOutput
7+
from api.repository.notes_repository import NotesRepository
8+
from api.repository.auth_repository import AuthRepository
9+
from helpers.helpers import NotFoundError
10+
11+
12+
router = APIRouter(
13+
prefix="/api/v1",
14+
tags=["Notes"],
15+
)
16+
17+
18+
@router.post("", status_code=status.HTTP_201_CREATED)
19+
async def add_note(
20+
user: Annotated[dict, Depends(AuthRepository.parse_access_token)],
21+
data: Annotated[NoteInput, Depends()],
22+
) -> dict:
23+
if not user:
24+
raise HTTPException(
25+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed"
26+
)
27+
note_id = await NotesRepository.add_note(data, user.get("user_id"))
28+
return {"okay": note_id}
29+
30+
31+
@router.get("", response_model=list[NoteOutput])
32+
async def get_notes(
33+
user: Annotated[dict, Depends(AuthRepository.parse_access_token)]
34+
) -> list[NoteOutput]:
35+
if not user:
36+
raise HTTPException(
37+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed"
38+
)
39+
notes = await NotesRepository.get_notes(user.get("user_id"))
40+
return notes
41+
42+
43+
@router.get("/{note_id}", response_model=NoteOutput)
44+
async def get_note(
45+
user: Annotated[dict, Depends(AuthRepository.parse_access_token)], note_id: int
46+
) -> NoteOutput:
47+
if not user:
48+
raise HTTPException(
49+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed"
50+
)
51+
try:
52+
note = await NotesRepository.get_note(note_id, user.get("user_id"))
53+
except NotFoundError:
54+
raise HTTPException(
55+
status_code=status.HTTP_404_NOT_FOUND,
56+
detail=f"Note with id {note_id} not found",
57+
)
58+
return note
59+
60+
61+
@router.put("/{note_id}", response_model=NoteOutput)
62+
async def update_note(
63+
user: Annotated[dict, Depends(AuthRepository.parse_access_token)],
64+
note_id: int,
65+
data: Annotated[NoteInput, Depends()],
66+
) -> NoteOutput:
67+
if not user:
68+
raise HTTPException(
69+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed"
70+
)
71+
try:
72+
note = await NotesRepository.update_note(note_id, data, user.get("user_id"))
73+
except NotFoundError:
74+
raise HTTPException(status_code=404, detail=f"Note with id {note_id} not found")
75+
return note
76+
77+
78+
@router.delete("/{note_id}")
79+
async def delete_note(
80+
user: Annotated[dict, Depends(AuthRepository.parse_access_token)], note_id: int
81+
) -> dict:
82+
if not user:
83+
raise HTTPException(
84+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed"
85+
)
86+
try:
87+
await NotesRepository.delete_note(note_id, user.get("user_id"))
88+
except NotFoundError:
89+
raise HTTPException(
90+
status_code=status.HTTP_404_NOT_FOUND,
91+
detail=f"Note with id {note_id} not found",
92+
)
93+
return {"okay": note_id}

schemas/notes_schemas.py renamed to api/schemas/notes_schemas.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
class NoteInput(BaseModel):
77
content: str
8-
user_id: int
9-
created_at: datetime = datetime.now()
10-
updated_at: datetime = datetime.now()
118

129

1310
class NoteOutput(NoteInput):
1411
id: int
12+
created_at: datetime = datetime.now()
13+
updated_at: datetime = datetime.now()
1514

1615
model_config = ConfigDict(from_attributes=True)

0 commit comments

Comments
 (0)