diff --git a/backend/src/backend/__pycache__/dependencies.cpython-314.pyc b/backend/src/backend/__pycache__/dependencies.cpython-314.pyc deleted file mode 100644 index 8fe1123..0000000 Binary files a/backend/src/backend/__pycache__/dependencies.cpython-314.pyc and /dev/null differ diff --git a/backend/src/backend/internal/__pycache__/user_code_gen.cpython-314.pyc b/backend/src/backend/internal/__pycache__/user_code_gen.cpython-314.pyc index 7a5dca9..fa72035 100644 Binary files a/backend/src/backend/internal/__pycache__/user_code_gen.cpython-314.pyc and b/backend/src/backend/internal/__pycache__/user_code_gen.cpython-314.pyc differ diff --git a/backend/src/backend/internal/authentication.py b/backend/src/backend/internal/authentication.py new file mode 100644 index 0000000..68f4549 --- /dev/null +++ b/backend/src/backend/internal/authentication.py @@ -0,0 +1,35 @@ +from sqlmodel import select +from backend.models.user import User +from backend.internal.database import get_session +from sqlmodel import Session +from fastapi import HTTPException +from fastapi.security.oauth2 import OAuth2PasswordBearer +from fastapi import Depends +from datetime import datetime +from datetime import timedelta + + +SECRET_KEY = "secret-key" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_DAYS = 360 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + +async def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_code: str = payload.get("sub") + if user_code is None: + raise HTTPException(status_code=401, detail="Invalid token") + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") + user = session.exec(select(User).where(User.user_code == user_code)) + if user is None: + raise HTTPException(status_code=401, detail="User not found") + return user \ No newline at end of file diff --git a/backend/src/backend/internal/user_code_gen.py b/backend/src/backend/internal/user_code_gen.py index bf82c3d..4c098a2 100644 --- a/backend/src/backend/internal/user_code_gen.py +++ b/backend/src/backend/internal/user_code_gen.py @@ -1,4 +1,5 @@ import string import random + def generate_user_code() -> str: - return ''.join(random.choices(string.ascii_letters, k=4)).upper() + return ''.join(random.choices(string.ascii_uppercase, k=4)) diff --git a/backend/src/backend/models/__pycache__/comment.cpython-314.pyc b/backend/src/backend/models/__pycache__/comment.cpython-314.pyc index e3d9eaa..45f2bc9 100644 Binary files a/backend/src/backend/models/__pycache__/comment.cpython-314.pyc and b/backend/src/backend/models/__pycache__/comment.cpython-314.pyc differ diff --git a/backend/src/backend/models/__pycache__/subscription.cpython-314.pyc b/backend/src/backend/models/__pycache__/subscription.cpython-314.pyc deleted file mode 100644 index c0fd5b7..0000000 Binary files a/backend/src/backend/models/__pycache__/subscription.cpython-314.pyc and /dev/null differ diff --git a/backend/src/backend/models/__pycache__/user.cpython-314.pyc b/backend/src/backend/models/__pycache__/user.cpython-314.pyc index 1475983..637d2c9 100644 Binary files a/backend/src/backend/models/__pycache__/user.cpython-314.pyc and b/backend/src/backend/models/__pycache__/user.cpython-314.pyc differ diff --git a/backend/src/backend/models/comment.py b/backend/src/backend/models/comment.py index e1fb5dc..2e61a7e 100644 --- a/backend/src/backend/models/comment.py +++ b/backend/src/backend/models/comment.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from datetime import datetime import uuid from typing import TYPE_CHECKING @@ -11,10 +12,11 @@ if TYPE_CHECKING: class CommentBase(SQLModel): pass +@dataclass class Comment(CommentBase, table=True): - id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) + id: uuid.UUID | None = Field(default_factory=uuid.uuid7, primary_key=True) slug: str = Field(index=True) content: str = Field() - date_time: datetime = Field(default=datetime.now()) + date_time: datetime = Field(default_factory=datetime.now) user_id: uuid.UUID = Field(foreign_key="user.id") user: "User" = Relationship(back_populates="comments") \ No newline at end of file diff --git a/backend/src/backend/models/user.py b/backend/src/backend/models/user.py index 844b898..3d922fc 100644 --- a/backend/src/backend/models/user.py +++ b/backend/src/backend/models/user.py @@ -1,5 +1,6 @@ +from uuid import uuid7 +from datetime import datetime from backend.internal.user_code_gen import generate_user_code -from dataclasses import dataclass import uuid from typing import TYPE_CHECKING from sqlmodel import Field @@ -13,15 +14,40 @@ class UserBase(SQLModel): pass class User(UserBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True) email: str = Field(unique=True) name: str = Field(unique=True) user_code: str = Field(default_factory=generate_user_code,unique=True) subscription: bool = Field() + created_at: datetime = Field(default_factory=datetime.now) + comments: list["Comment"] = Relationship(back_populates="user") -@dataclass + def factory(self, + id: uuid.UUID = uuid.uuid7(), + email: str = "Email@Test.mail", + name: str = "TestName", + user_code: str = "WYZZ", + subscription: bool = False, + ) -> User: + self.id=id + self.email=email + self.name=name + self.user_code=user_code + self.subscription=subscription + return self + class NewUser(UserBase): email: str name: str subscription: bool + + def factory(self, + email: str = "Email@Test.mail", + name: str = "TestName", + subscription: bool = False + ) -> NewUser: + self.email = email + self.name = name + self.subscription = subscription + return self diff --git a/backend/src/backend/routers/__pycache__/subscriptions.cpython-314.pyc b/backend/src/backend/routers/__pycache__/subscriptions.cpython-314.pyc deleted file mode 100644 index cfa77d6..0000000 Binary files a/backend/src/backend/routers/__pycache__/subscriptions.cpython-314.pyc and /dev/null differ diff --git a/backend/src/backend/routers/__pycache__/test_subscriptions.cpython-314-pytest-9.1.1.pyc b/backend/src/backend/routers/__pycache__/test_subscriptions.cpython-314-pytest-9.1.1.pyc deleted file mode 100644 index 1704d6f..0000000 Binary files a/backend/src/backend/routers/__pycache__/test_subscriptions.cpython-314-pytest-9.1.1.pyc and /dev/null differ diff --git a/backend/src/backend/routers/__pycache__/test_users.cpython-314-pytest-9.1.1.pyc b/backend/src/backend/routers/__pycache__/test_users.cpython-314-pytest-9.1.1.pyc index 7997263..ede2373 100644 Binary files a/backend/src/backend/routers/__pycache__/test_users.cpython-314-pytest-9.1.1.pyc and b/backend/src/backend/routers/__pycache__/test_users.cpython-314-pytest-9.1.1.pyc differ diff --git a/backend/src/backend/routers/__pycache__/users.cpython-314.pyc b/backend/src/backend/routers/__pycache__/users.cpython-314.pyc index d95f89b..223c615 100644 Binary files a/backend/src/backend/routers/__pycache__/users.cpython-314.pyc and b/backend/src/backend/routers/__pycache__/users.cpython-314.pyc differ diff --git a/backend/src/backend/routers/test_users.py b/backend/src/backend/routers/test_users.py index 78e2219..e31b0d1 100644 --- a/backend/src/backend/routers/test_users.py +++ b/backend/src/backend/routers/test_users.py @@ -39,7 +39,7 @@ def test_register_user_saved(session:Session, client: TestClient): "subscription": True } response = client.post(url="/users/", json=new_user) - assert response.status_code == 200 + assert response.status_code == 201 db_user = session.exec(select(User).where(User.email == "Test@Test.test")).first() assert db_user @@ -48,19 +48,37 @@ def test_register_user_saved(session:Session, client: TestClient): assert db_user.subscription == True def test_email_not_unique_returns_400(session: Session, client: TestClient): - new_user = NewUser(email="Not@Unique.de",name="Tim", subscription=True) + new_user = NewUser(email="NotUnique",name="Müller", subscription=True) # TODO: durch factory ersetzen - user = User.model_validate(new_user.model_copy(update={'name':'Different'})) + user = User().factory(email="NotUnique") session.add(user) session.commit() response = client.post(url="/users/", json=new_user.model_dump()) assert response.status_code == 400 def test_name_not_unique_returns_400(session: Session, client: TestClient): - new_user = NewUser(email="Absolutly@Unique.de",name="Chiara", subscription=True) + new_user = NewUser(email="wewqe@2ewew",name="Chiara", subscription=True) # TODO: durch factory ersetzen - user = User.model_validate(new_user.model_copy(update={'email':'Different@Different.de'})) + user = User().factory(name="Chiara") session.add(user) session.commit() response = client.post(url="/users/", json=new_user.model_dump()) - assert response.status_code == 400 \ No newline at end of file + assert response.status_code == 400 + +def test_login_successful(session: Session, client: TestClient): + user = User().factory() + session.add(user) + session.commit() + session.refresh(user) + + payload = { + "user_code": user.user_code + } + + response = client.post( + url="/users/login", + data=payload + ) + + assert response.status_code == 200 + assert response.json() == "token" diff --git a/backend/src/backend/routers/users.py b/backend/src/backend/routers/users.py index 8b8cef1..bc502e0 100644 --- a/backend/src/backend/routers/users.py +++ b/backend/src/backend/routers/users.py @@ -1,13 +1,5 @@ -# CreateUser <- mail, Name -# Login Rate Limit! -> token for everything else -# Forgot Code <- email (rate limit) -# Delete User <- token -# change mail <- token, new mail -# subscribe -# unsubscribe - -# delete all deactivated users after 48h - +from fastapi import Form +from sqlmodel import select from starlette.status import HTTP_400_BAD_REQUEST from fastapi import HTTPException from sqlalchemy.exc import IntegrityError @@ -25,13 +17,32 @@ router = APIRouter( responses={404: {"description": "Not found"}}, ) -@router.post("/") +@router.post("/", status_code=201) def register_user(new_user: NewUser, session: Session = Depends(get_session)): user = User.model_validate(new_user) try: session.add(user) session.commit() + # TODO: send mail except IntegrityError as e: session.rollback() raise HTTPException(HTTP_400_BAD_REQUEST) + +@router.post("/login") # TODO: rate limit +def login(user_code: str = Form(), session: Session = Depends(get_session)) -> str: + user = session.exec(select(User).where(User.user_code == user_code)).one + + if not user: + raise HTTPException() + return "token" + + +# Login Rate Limit! -> token for everything else +# Forgot Code <- email (rate limit) +# Delete User <- token +# change mail <- token, new mail +# subscribe +# unsubscribe + +# delete all deactivated users after 48h \ No newline at end of file