← full-stack-fastapi-template / backend/app/api/routes/users.py
| 1 | import uuid |
| 2 | from typing import Any |
| 3 | |
| 4 | from fastapi import APIRouter, Depends, HTTPException |
| 5 | from sqlmodel import col, delete, func, select |
| 6 | |
| 7 | from app import crud |
| 8 | from app.api.deps import ( |
| 9 | CurrentUser, |
| 10 | SessionDep, |
| 11 | get_current_active_superuser, |
| 12 | ) |
| 13 | from app.core.config import settings |
| 14 | from app.core.security import get_password_hash, verify_password |
| 15 | from app.models import ( |
| 16 | Item, |
| 17 | Message, |
| 18 | UpdatePassword, |
| 19 | User, |
| 20 | UserCreate, |
| 21 | UserPublic, |
| 22 | UserRegister, |
| 23 | UsersPublic, |
| 24 | UserUpdate, |
| 25 | UserUpdateMe, |
| 26 | ) |
| 27 | from app.utils import generate_new_account_email, send_email |
| 28 | |
| 29 | router = APIRouter(prefix="/users", tags=["users"]) |
| 30 | |
| 31 | |
| 32 | @router.get( |
| 33 | "/", |
| 34 | dependencies=[Depends(get_current_active_superuser)], |
| 35 | response_model=UsersPublic, |
| 36 | ) |
| 37 | def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: |
| 38 | """ |
| 39 | Retrieve users. |
| 40 | """ |
| 41 | |
| 42 | count_statement = select(func.count()).select_from(User) |
| 43 | count = session.exec(count_statement).one() |
| 44 | |
| 45 | statement = ( |
| 46 | select(User).order_by(col(User.created_at).desc()).offset(skip).limit(limit) |
| 47 | ) |
| 48 | users = session.exec(statement).all() |
| 49 | |
| 50 | users_public = [UserPublic.model_validate(user) for user in users] |
| 51 | return UsersPublic(data=users_public, count=count) |
| 52 | |
| 53 | |
| 54 | @router.post( |
| 55 | "/", dependencies=[Depends(get_current_active_superuser)], response_model=UserPublic |
| 56 | ) |
| 57 | def create_user(*, session: SessionDep, user_in: UserCreate) -> Any: |
| 58 | """ |
| 59 | Create new user. |
| 60 | """ |
| 61 | user = crud.get_user_by_email(session=session, email=user_in.email) |
| 62 | if user: |
| 63 | raise HTTPException( |
| 64 | status_code=400, |
| 65 | detail="The user with this email already exists in the system.", |
| 66 | ) |
| 67 | |
| 68 | user = crud.create_user(session=session, user_create=user_in) |
| 69 | if settings.emails_enabled and user_in.email: |
| 70 | email_data = generate_new_account_email( |
| 71 | email_to=user_in.email, username=user_in.email, password=user_in.password |
| 72 | ) |
| 73 | send_email( |
| 74 | email_to=user_in.email, |
| 75 | subject=email_data.subject, |
| 76 | html_content=email_data.html_content, |
| 77 | ) |
| 78 | return user |
| 79 | |
| 80 | |
| 81 | @router.patch("/me", response_model=UserPublic) |
| 82 | def update_user_me( |
| 83 | *, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser |
| 84 | ) -> Any: |
| 85 | """ |
| 86 | Update own user. |
| 87 | """ |
| 88 | |
| 89 | if user_in.email: |
| 90 | existing_user = crud.get_user_by_email(session=session, email=user_in.email) |
| 91 | if existing_user and existing_user.id != current_user.id: |
| 92 | raise HTTPException( |
| 93 | status_code=409, detail="User with this email already exists" |
| 94 | ) |
| 95 | user_data = user_in.model_dump(exclude_unset=True) |
| 96 | current_user.sqlmodel_update(user_data) |
| 97 | session.add(current_user) |
| 98 | session.commit() |
| 99 | session.refresh(current_user) |
| 100 | return current_user |
| 101 | |
| 102 | |
| 103 | @router.patch("/me/password", response_model=Message) |
| 104 | def update_password_me( |
| 105 | *, session: SessionDep, body: UpdatePassword, current_user: CurrentUser |
| 106 | ) -> Any: |
| 107 | """ |
| 108 | Update own password. |
| 109 | """ |
| 110 | verified, _ = verify_password(body.current_password, current_user.hashed_password) |
| 111 | if not verified: |
| 112 | raise HTTPException(status_code=400, detail="Incorrect password") |
| 113 | if body.current_password == body.new_password: |
| 114 | raise HTTPException( |
| 115 | status_code=400, detail="New password cannot be the same as the current one" |
| 116 | ) |
| 117 | hashed_password = get_password_hash(body.new_password) |
| 118 | current_user.hashed_password = hashed_password |
| 119 | session.add(current_user) |
| 120 | session.commit() |
| 121 | return Message(message="Password updated successfully") |
| 122 | |
| 123 | |
| 124 | @router.get("/me", response_model=UserPublic) |
| 125 | def read_user_me(current_user: CurrentUser) -> Any: |
| 126 | """ |
| 127 | Get current user. |
| 128 | """ |
| 129 | return current_user |
| 130 | |
| 131 | |
| 132 | @router.delete("/me", response_model=Message) |
| 133 | def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any: |
| 134 | """ |
| 135 | Delete own user. |
| 136 | """ |
| 137 | if current_user.is_superuser: |
| 138 | raise HTTPException( |
| 139 | status_code=403, detail="Super users are not allowed to delete themselves" |
| 140 | ) |
| 141 | session.delete(current_user) |
| 142 | session.commit() |
| 143 | return Message(message="User deleted successfully") |
| 144 | |
| 145 | |
| 146 | @router.post("/signup", response_model=UserPublic) |
| 147 | def register_user(session: SessionDep, user_in: UserRegister) -> Any: |
| 148 | """ |
| 149 | Create new user without the need to be logged in. |
| 150 | """ |
| 151 | user = crud.get_user_by_email(session=session, email=user_in.email) |
| 152 | if user: |
| 153 | raise HTTPException( |
| 154 | status_code=400, |
| 155 | detail="The user with this email already exists in the system", |
| 156 | ) |
| 157 | user_create = UserCreate.model_validate(user_in) |
| 158 | user = crud.create_user(session=session, user_create=user_create) |
| 159 | return user |
| 160 | |
| 161 | |
| 162 | @router.get("/{user_id}", response_model=UserPublic) |
| 163 | def read_user_by_id( |
| 164 | user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser |
| 165 | ) -> Any: |
| 166 | """ |
| 167 | Get a specific user by id. |
| 168 | """ |
| 169 | user = session.get(User, user_id) |
| 170 | if user == current_user: |
| 171 | return user |
| 172 | if not current_user.is_superuser: |
| 173 | raise HTTPException( |
| 174 | status_code=403, |
| 175 | detail="The user doesn't have enough privileges", |
| 176 | ) |
| 177 | if user is None: |
| 178 | raise HTTPException(status_code=404, detail="User not found") |
| 179 | return user |
| 180 | |
| 181 | |
| 182 | @router.patch( |
| 183 | "/{user_id}", |
| 184 | dependencies=[Depends(get_current_active_superuser)], |
| 185 | response_model=UserPublic, |
| 186 | ) |
| 187 | def update_user( |
| 188 | *, |
| 189 | session: SessionDep, |
| 190 | user_id: uuid.UUID, |
| 191 | user_in: UserUpdate, |
| 192 | ) -> Any: |
| 193 | """ |
| 194 | Update a user. |
| 195 | """ |
| 196 | |
| 197 | db_user = session.get(User, user_id) |
| 198 | if not db_user: |
| 199 | raise HTTPException( |
| 200 | status_code=404, |
| 201 | detail="The user with this id does not exist in the system", |
| 202 | ) |
| 203 | if user_in.email: |
| 204 | existing_user = crud.get_user_by_email(session=session, email=user_in.email) |
| 205 | if existing_user and existing_user.id != user_id: |
| 206 | raise HTTPException( |
| 207 | status_code=409, detail="User with this email already exists" |
| 208 | ) |
| 209 | |
| 210 | db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in) |
| 211 | return db_user |
| 212 | |
| 213 | |
| 214 | @router.delete("/{user_id}", dependencies=[Depends(get_current_active_superuser)]) |
| 215 | def delete_user( |
| 216 | session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID |
| 217 | ) -> Message: |
| 218 | """ |
| 219 | Delete a user. |
| 220 | """ |
| 221 | user = session.get(User, user_id) |
| 222 | if not user: |
| 223 | raise HTTPException(status_code=404, detail="User not found") |
| 224 | if user == current_user: |
| 225 | raise HTTPException( |
| 226 | status_code=403, detail="Super users are not allowed to delete themselves" |
| 227 | ) |
| 228 | statement = delete(Item).where(col(Item.owner_id) == user_id) |
| 229 | session.exec(statement) |
| 230 | session.delete(user) |
| 231 | session.commit() |
| 232 | return Message(message="User deleted successfully") |
| 233 |