Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/src/features/auth/auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export type UserRecord = {
username: string
name: string
last_name: string
phone: number | null
email: string
role: UserRole
}
Expand Down
4 changes: 2 additions & 2 deletions webapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
18 changes: 12 additions & 6 deletions webapi/api/endpoints/v1/auths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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")
Expand Down
7 changes: 1 addition & 6 deletions webapi/api/endpoints/v1/users.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions webapi/models/user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Optional, List

from pydantic import EmailStr
from sqlalchemy import BIGINT
from sqlmodel import Field, Relationship, SQLModel


Expand All @@ -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)
Expand Down
39 changes: 29 additions & 10 deletions webapi/schemas/user_schema.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 0 additions & 2 deletions webapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand All @@ -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",
)
Expand Down
31 changes: 23 additions & 8 deletions webapi/tests/functional/test_auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
Expand All @@ -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",
},
Expand Down Expand Up @@ -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"),
)
Expand Down Expand Up @@ -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"),
)
Expand All @@ -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"),
)
Expand Down Expand Up @@ -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"),
)
Expand Down Expand Up @@ -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"),
)
Expand All @@ -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"),
)
Expand Down
1 change: 0 additions & 1 deletion webapi/tests/functional/test_cd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
4 changes: 0 additions & 4 deletions webapi/tests/functional/test_prompts_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 4 additions & 14 deletions webapi/tests/functional/test_users_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
}
Expand All @@ -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)
Expand All @@ -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",
}
Expand All @@ -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"),
)
Expand All @@ -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",
}
Expand Down
Loading
Loading