diff --git a/frontend/src/features/auth/auth-types.ts b/frontend/src/features/auth/auth-types.ts index 6d00907..df96067 100644 --- a/frontend/src/features/auth/auth-types.ts +++ b/frontend/src/features/auth/auth-types.ts @@ -15,7 +15,6 @@ export type UserRecord = { username: string name: string last_name: string - phone: number | null email: string role: UserRole } diff --git a/webapi/README.md b/webapi/README.md index 2cdf9e1..554c9af 100644 --- a/webapi/README.md +++ b/webapi/README.md @@ -207,10 +207,10 @@ Check the backend root endpoint directly: curl http://127.0.0.1:8000/ ``` -Create a user through the frontend proxy. Change `username`, `phone`, or `email` before rerunning because those fields must be unique: +Create a user through the frontend proxy. Change `username` or `email` before rerunning because those fields must be unique: ```bash -curl -X POST http://127.0.0.1:8080/api/v1/auth/signup -H "Content-Type: application/json" -d '{"username":"demo_user_001","name":"Demo","last_name":"User","phone":"5550000001","email":"demo001@example.com","hashed_password":"demo-password"}' +curl -X POST http://127.0.0.1:8080/api/v1/auth/signup -H "Content-Type: application/json" -d '{"username":"demo_user_001","name":"Demo","last_name":"User","email":"demo001@example.com","hashed_password":"demo-password"}' ``` Login and store the token without requiring `jq`: diff --git a/webapi/api/endpoints/v1/auths.py b/webapi/api/endpoints/v1/auths.py index d3865c8..d42fe61 100644 --- a/webapi/api/endpoints/v1/auths.py +++ b/webapi/api/endpoints/v1/auths.py @@ -14,7 +14,7 @@ from db.redis_connection import get_redis from pydantic import BaseModel, Field from schemas.login_schema import LoginRequest -from schemas.user_schema import UserRead +from schemas.user_schema import UserCreate, UserRead router = APIRouter() @@ -29,18 +29,24 @@ class RecoveryRedeemRequest(BaseModel): @router.post("/signup") -def signup(user: User, session: Session = Depends(get_session)): - statement = select(User).where(User.username == user.username) +def signup(payload: UserCreate, session: Session = Depends(get_session)): + statement = select(User).where(User.username == payload.username) result = session.exec(statement) user_exists = result.one_or_none() if user_exists: raise HTTPException(status_code=400, detail="username already taken") - if user.role not in {"user", "admin", "god"}: + if payload.role not in {"user", "admin", "god"}: raise HTTPException(status_code=400, detail="invalid role") - user.hashed_password = sha256_crypt.hash(user.hashed_password) + user = User( + username=payload.username, + name=payload.name, + last_name=payload.last_name, + email=payload.email, + hashed_password=sha256_crypt.hash(payload.hashed_password), + role=payload.role, + ) session.add(user) session.commit() - session.refresh(user) return {"message": "User created successfully"} @router.post("/login") diff --git a/webapi/api/endpoints/v1/users.py b/webapi/api/endpoints/v1/users.py index ede65c1..66359cc 100644 --- a/webapi/api/endpoints/v1/users.py +++ b/webapi/api/endpoints/v1/users.py @@ -1,7 +1,6 @@ from fastapi import APIRouter, HTTPException, Depends from sqlmodel import Session, select from models.user import User -from typing import Optional from schemas.user_schema import UserRead, UserReadWithPrompts from db.db_connection import get_session from auth.auth_service import get_current_user @@ -11,13 +10,10 @@ router = APIRouter() @router.get("", response_model=list[UserRead]) -def read_users(phone: Optional[int] = None, skip: int = 0, limit: int = 10, +def read_users(skip: int = 0, limit: int = 10, session: Session = Depends(get_session), current_user: dict = Depends(get_current_user)): statement = select(User).offset(skip).limit(limit) - # If phone is provided, filter by phone number - if phone: - statement = statement.where(User.phone == phone) users = session.exec(statement).all() if not users: raise HTTPException(status_code=404, detail="User not found") @@ -56,7 +52,6 @@ def update_user(user_id: int, user: User, raise HTTPException(status_code=404, detail="User not found") existing_user.name = user.name existing_user.last_name = user.last_name - existing_user.phone = user.phone existing_user.email = user.email existing_user.hashed_password = sha256_crypt.hash(user.hashed_password) # Ensure the username is unique diff --git a/webapi/models/user.py b/webapi/models/user.py index 93f5033..1a3eb5d 100644 --- a/webapi/models/user.py +++ b/webapi/models/user.py @@ -1,7 +1,6 @@ from typing import Optional, List from pydantic import EmailStr -from sqlalchemy import BIGINT from sqlmodel import Field, Relationship, SQLModel @@ -10,7 +9,6 @@ class User(SQLModel, table=True): username: str = Field(max_length=50, index=True, nullable=False) name: str = Field(max_length=100, index=True, nullable=False) last_name: str = Field(max_length=100, index=True, nullable=False) - phone: Optional[int] = Field(default=None, sa_type=BIGINT, index=True, unique=True) email: EmailStr = Field(max_length=100, nullable=False) hashed_password: str = Field(max_length=255) # Longer for bcrypt hashes role: str = Field(default="user", max_length=20, nullable=False) diff --git a/webapi/schemas/user_schema.py b/webapi/schemas/user_schema.py index 3354e28..585fdb3 100644 --- a/webapi/schemas/user_schema.py +++ b/webapi/schemas/user_schema.py @@ -1,29 +1,48 @@ -from typing import Optional, List -from pydantic import BaseModel +from typing import List +from pydantic import BaseModel, ConfigDict, EmailStr, Field from .prompt_schema import PromptRead +class UserCreate(BaseModel): + model_config = ConfigDict( + extra="forbid", + json_schema_extra={ + "example": { + "username": "usertest", + "name": "user", + "last_name": "testing", + "email": "user@example.com", + "hashed_password": "usertest", + "role": "user", + } + }, + ) + + username: str = Field(max_length=50) + name: str = Field(max_length=100) + last_name: str = Field(max_length=100) + email: EmailStr = Field(max_length=100) + hashed_password: str = Field(max_length=255) + role: str = "user" + + class UserRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: int username: str name: str last_name: str - phone: Optional[int] email: str role: str - class Config: - from_attributes = True - class UserReadWithPrompts(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: int username: str name: str last_name: str - phone: Optional[int] email: str role: str prompts: List[PromptRead] = [] - - class Config: - from_attributes = True diff --git a/webapi/tests/conftest.py b/webapi/tests/conftest.py index 6cc165b..a4263d1 100644 --- a/webapi/tests/conftest.py +++ b/webapi/tests/conftest.py @@ -76,7 +76,6 @@ def user_payload(): "username": "pytest_user", "name": "Py", "last_name": "Tester", - "phone": 5512345678, "email": "pytest_user@example.com", "hashed_password": "pytest_password", } @@ -88,7 +87,6 @@ def created_user(db_session): username="base_user", name="Base", last_name="User", - phone=5500000001, email="base_user@example.com", hashed_password="base_password", ) diff --git a/webapi/tests/functional/test_auth_routes.py b/webapi/tests/functional/test_auth_routes.py index 4357b6d..8b98b20 100644 --- a/webapi/tests/functional/test_auth_routes.py +++ b/webapi/tests/functional/test_auth_routes.py @@ -16,17 +16,39 @@ def test_signup_success(client, user_payload, db_session): created = db_session.exec(select(User).where(User.username == user_payload["username"])).first() assert created is not None + assert isinstance(created.id, int) assert created.hashed_password != user_payload["hashed_password"] assert sha256_crypt.verify(user_payload["hashed_password"], created.hashed_password) +def test_signup_rejects_client_supplied_id(client, user_payload, db_session): + response = client.post("/api/v1/auth/signup", json={**user_payload, "id": 8}) + + assert response.status_code == 422 + created = db_session.exec(select(User).where(User.username == user_payload["username"])).first() + assert created is None + + +def test_signup_openapi_schema_does_not_expose_id(client): + response = client.get("/openapi.json") + + assert response.status_code == 200 + body_schema = response.json()["paths"]["/api/v1/auth/signup"]["post"]["requestBody"]["content"]["application/json"]["schema"] + ref_name = body_schema["$ref"].rsplit("/", 1)[-1] + user_create_schema = response.json()["components"]["schemas"][ref_name] + example = user_create_schema["example"] + + assert "id" not in user_create_schema["properties"] + assert "id" not in user_create_schema.get("required", []) + assert "id" not in example + + def test_signup_duplicate_username_returns_400(client, db_session): db_session.add( User( username="dup_user", name="dup", last_name="user", - phone=5500000010, email="dup_user@example.com", hashed_password=sha256_crypt.hash("password"), ) @@ -39,7 +61,6 @@ def test_signup_duplicate_username_returns_400(client, db_session): "username": "dup_user", "name": "new", "last_name": "user", - "phone": 5500000011, "email": "new_dup_user@example.com", "hashed_password": "password", }, @@ -83,7 +104,6 @@ def test_login_invalid_credentials_returns_401(client, db_session): username="known_user", name="known", last_name="user", - phone=5500000012, email="known_user@example.com", hashed_password=sha256_crypt.hash("right_password"), ) @@ -150,7 +170,6 @@ def test_generate_password_key_exists_returns_400(client, db_session, fake_redis username="recover_user", name="recover", last_name="user", - phone=5500000013, email="recover_user@example.com", hashed_password=sha256_crypt.hash("original_password"), ) @@ -172,7 +191,6 @@ def test_generate_password_success_saves_password_and_calls_email(client, db_ses username="mail_user", name="mail", last_name="user", - phone=5500000014, email="mail_user@example.com", hashed_password=sha256_crypt.hash("old_password"), ) @@ -210,7 +228,6 @@ def test_generate_password_email_failure_returns_500(client, db_session, monkeyp username="mail_fail_user", name="mail", last_name="fail", - phone=5500000015, email="mail_fail_user@example.com", hashed_password=sha256_crypt.hash("old_password"), ) @@ -265,7 +282,6 @@ def test_recover_password_not_found_in_redis_returns_404(client, db_session): username="recover_missing_pwd", name="recover", last_name="missing", - phone=5500000016, email="recover_missing_pwd@example.com", hashed_password=sha256_crypt.hash("old_password"), ) @@ -284,7 +300,6 @@ def test_recover_success_returns_key_and_password(client, db_session, fake_redis username="recover_ok", name="recover", last_name="ok", - phone=5500000017, email="recover_ok@example.com", hashed_password=sha256_crypt.hash("old_password"), ) diff --git a/webapi/tests/functional/test_cd.py b/webapi/tests/functional/test_cd.py index 28a1f76..e8587b6 100644 --- a/webapi/tests/functional/test_cd.py +++ b/webapi/tests/functional/test_cd.py @@ -11,7 +11,6 @@ def test_login_and_access_private(client): "username": "pytest", "name": "Py", "last_name": "Tester", - "phone": 5511111111, "email": "pytest@example.com", "hashed_password": "pytest", }, diff --git a/webapi/tests/functional/test_prompts_routes.py b/webapi/tests/functional/test_prompts_routes.py index 7cb1c03..f47cf9c 100644 --- a/webapi/tests/functional/test_prompts_routes.py +++ b/webapi/tests/functional/test_prompts_routes.py @@ -14,7 +14,6 @@ def create_user(db_session, username: str, role: str = "user") -> User: username=username, name=username, last_name="User", - phone=5500001000 + len(username), email=f"{username}@example.com", hashed_password="password", role=role, @@ -353,7 +352,6 @@ def test_regular_user_cannot_read_another_users_prompt(client, auth_header, db_s username="other_prompt_user", name="Other", last_name="User", - phone=5500000190, email="other_prompt_user@example.com", hashed_password="password", ) @@ -381,7 +379,6 @@ def test_admin_can_read_all_prompts_but_cannot_update(client, db_session, create username="admin_user", name="Admin", last_name="User", - phone=5500000191, email="admin_user@example.com", hashed_password="password", role="admin", @@ -416,7 +413,6 @@ def test_god_can_delete_another_users_prompt(client, db_session, created_prompt) username="god_user", name="God", last_name="User", - phone=5500000192, email="god_user@example.com", hashed_password="password", role="god", diff --git a/webapi/tests/functional/test_users_routes.py b/webapi/tests/functional/test_users_routes.py index de931dc..46a2992 100644 --- a/webapi/tests/functional/test_users_routes.py +++ b/webapi/tests/functional/test_users_routes.py @@ -14,16 +14,11 @@ def test_read_users_success(client, auth_header, created_user): assert "hashed_password" not in response.json()[0] -def test_read_users_with_phone_filter_success(client, auth_header, created_user): - response = client.get(f"/api/v1/users?phone={created_user.phone}", headers=auth_header) - - assert response.status_code == 200 - assert len(response.json()) == 1 - assert response.json()[0]["phone"] == created_user.phone - +def test_read_users_not_found_returns_404(client, auth_header, created_user, db_session): + db_session.delete(created_user) + db_session.commit() -def test_read_users_not_found_returns_404(client, auth_header): - response = client.get("/api/v1/users?phone=5599998888", headers=auth_header) + response = client.get("/api/v1/users", headers=auth_header) assert response.status_code == 404 assert response.json()["detail"] == "User not found" @@ -72,7 +67,6 @@ def test_update_user_success(client, auth_header, created_user, db_session): "username": created_user.username, "name": "UPDATED", "last_name": "USER", - "phone": 5599990000, "email": "updated_user@example.com", "hashed_password": "new_password", } @@ -83,7 +77,6 @@ def test_update_user_success(client, auth_header, created_user, db_session): body = response.json() assert body["name"] == "updated" assert body["last_name"] == "user" - assert body["phone"] == 5599990000 assert body["email"] == "updated_user@example.com" updated = db_session.get(User, created_user.id) @@ -95,7 +88,6 @@ def test_update_user_missing_returns_404(client, auth_header): "username": "missing", "name": "Missing", "last_name": "User", - "phone": 5511112222, "email": "missing_user@example.com", "hashed_password": "password", } @@ -111,7 +103,6 @@ def test_update_user_duplicate_username_returns_400(client, auth_header, created username="taken_username", name="Another", last_name="User", - phone=5500000090, email="another_user@example.com", hashed_password=sha256_crypt.hash("password"), ) @@ -122,7 +113,6 @@ def test_update_user_duplicate_username_returns_400(client, auth_header, created "username": "taken_username", "name": "Updated", "last_name": "User", - "phone": created_user.phone, "email": "updated_conflict@example.com", "hashed_password": "new_password", } diff --git a/webapi/tests/nonfunctional/test_ci.py b/webapi/tests/nonfunctional/test_ci.py index 7cc2b01..24a6982 100644 --- a/webapi/tests/nonfunctional/test_ci.py +++ b/webapi/tests/nonfunctional/test_ci.py @@ -21,14 +21,16 @@ def test_register_user(): "username": TEST_USER, "name": "user to test", "last_name": "my webapp", - "phone": 3132333435, "email": "pytestmyapp@testing.com", "hashed_password": TEST_PSW }) - assert response.status_code == 200 - assert response.json() == { - "message": "User created successfully" - } + assert response.status_code in (200, 400) + if response.status_code == 400: + assert response.json().get("detail") == "username already taken" + else: + assert response.json() == { + "message": "User created successfully" + } def get_token(): response = client.post("/api/v1/auth/login", json={ @@ -51,14 +53,11 @@ def test_register_prompt(): "rate": 5, }, headers=headers) assert response.status_code == 200 - assert response.json() == { - "id": 1, - "user_id": 1, - "model_name": "gpt-4.1", - "prompt_text": "Generate a test response", - "category": "qa", - "rate": 5 - } + assert response.json()["user_id"] == 1 + assert response.json()["model_name"] == "gpt-4.1" + assert response.json()["prompt_text"] == "Generate a test response" + assert response.json()["category"] == "qa" + assert response.json()["rate"] == 5 def test_read_users(): token = get_token()