Skip to content
Open
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
25 changes: 17 additions & 8 deletions backend/app/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from sqlmodel import col, delete, func, select

from app import crud
Expand All @@ -23,6 +24,7 @@
UsersPublic,
UserUpdate,
UserUpdateMe,
ConflictError,
)
from app.utils import generate_new_account_email, send_email

Expand All @@ -49,17 +51,20 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:


@router.post(
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UserPublic
"/",
dependencies=[Depends(get_current_active_superuser)],
response_model=UserPublic,
responses={409: {"model": ConflictError}},
)
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
"""
Create new user.
"""
user = crud.get_user_by_email(session=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
return JSONResponse(
status_code=409,
content={"error": "conflict", "field": "email", "message": "Already in use"},
)

user = crud.create_user(session=session, user_create=user_in)
Expand Down Expand Up @@ -139,16 +144,20 @@ def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
return Message(message="User deleted successfully")


@router.post("/signup", response_model=UserPublic)
@router.post(
"/signup",
response_model=UserPublic,
responses={409: {"model": ConflictError}},
)
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
"""
Create new user without the need to be logged in.
"""
user = crud.get_user_by_email(session=session, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system",
return JSONResponse(
status_code=409,
content={"error": "conflict", "field": "email", "message": "Already in use"},
)
user_create = UserCreate.model_validate(user_in)
user = crud.create_user(session=session, user_create=user_create)
Expand Down
7 changes: 7 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class Message(SQLModel):
message: str


# Error model for consistent API error responses
class ConflictError(SQLModel):
error: str = "conflict"
field: str
message: str = "Already in use"


# JSON payload containing access token
class Token(SQLModel):
access_token: str
Expand Down
8 changes: 4 additions & 4 deletions backend/tests/api/routes/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ def test_create_user_existing_username(
json=data,
)
created_user = r.json()
assert r.status_code == 400
assert "_id" not in created_user
assert r.status_code == 409
assert created_user == {"error": "conflict", "field": "email", "message": "Already in use"}


def test_create_user_by_normal_user(
Expand Down Expand Up @@ -316,8 +316,8 @@ def test_register_user_already_exists_error(client: TestClient) -> None:
f"{settings.API_V1_STR}/users/signup",
json=data,
)
assert r.status_code == 400
assert r.json()["detail"] == "The user with this email already exists in the system"
assert r.status_code == 409
assert r.json() == {"error": "conflict", "field": "email", "message": "Already in use"}


def test_update_user(
Expand Down
23 changes: 19 additions & 4 deletions frontend/src/routes/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
import { type SubmitHandler, useForm } from "react-hook-form"
import { FiLock, FiUser } from "react-icons/fi"

import type { UserRegister } from "@/client"
import type { ApiError, UserRegister } from "@/client"
import { Button } from "@/components/ui/button"
import { Field } from "@/components/ui/field"
import { InputGroup } from "@/components/ui/input-group"
import { PasswordInput } from "@/components/ui/password-input"
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
import { confirmPasswordRules, emailPattern, passwordRules } from "@/utils"
import { confirmPasswordRules, emailPattern, passwordRules, handleError } from "@/utils"
import Logo from "/assets/images/fastapi-logo.svg"

export const Route = createFileRoute("/signup")({
Expand All @@ -37,6 +37,7 @@ function SignUp() {
register,
handleSubmit,
getValues,
setError,
formState: { errors, isSubmitting },
} = useForm<UserRegisterForm>({
mode: "onBlur",
Expand All @@ -49,8 +50,22 @@ function SignUp() {
},
})

const onSubmit: SubmitHandler<UserRegisterForm> = (data) => {
signUpMutation.mutate(data)
const onSubmit: SubmitHandler<UserRegisterForm> = async (data) => {
try {
await signUpMutation.mutateAsync(data)
} catch (err) {
const apiErr = err as ApiError
const body = apiErr.body as any
if (apiErr.status === 409 && body?.error === "conflict" && body?.field) {
// Map conflict to the specific field inline error
setError(body.field as keyof UserRegisterForm, {
type: "server",
message: body.message ?? "Already in use",
})
} else {
handleError(apiErr)
}
}
}

return (
Expand Down
5 changes: 2 additions & 3 deletions frontend/tests/sign-up.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ test("Sign up with existing email", async ({ page }) => {
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()

await page
.getByText("The user with this email already exists in the system")
.click()
// Expect inline error on the email field
await expect(page.getByText("Already in use")).toBeVisible()
})

test("Sign up with weak password", async ({ page }) => {
Expand Down