diff --git a/src/backend/app/models/user.py b/src/backend/app/models/user.py index 96d447e1..032c6a50 100644 --- a/src/backend/app/models/user.py +++ b/src/backend/app/models/user.py @@ -16,11 +16,6 @@ class UserCreate(SQLModel): class UserOut(SQLModel): - username: str = Field(index=True, unique=True) - email: str | None = Field(default=None, index=True, unique=True) - - -class User(UserOut, table=True): id: UUID = Field( default=None, primary_key=True, @@ -30,5 +25,9 @@ class User(UserOut, table=True): ) }, ) + username: str = Field(index=True, unique=True) + email: str | None = Field(default=None, index=True, unique=True) + +class User(UserOut, table=True): password_hash: str = Field() diff --git a/src/backend/app/routes/http/admin/user.py b/src/backend/app/routes/http/admin/user.py index 02bf5de3..9c3c1c12 100644 --- a/src/backend/app/routes/http/admin/user.py +++ b/src/backend/app/routes/http/admin/user.py @@ -1,11 +1,25 @@ -from fastapi import APIRouter +from uuid import UUID -from app.deps import CurrentUser, SessionDep -from app.models.user import UserOut, UserUpdate +from fastapi import APIRouter, HTTPException +from sqlmodel import select + +from app.deps import CurrentUser, PaginationDep, SessionDep +from app.models.user import User, UserCreate, UserOut, UserUpdate +from app.pagination import Page, paginate +from app.security import get_password_hash router = APIRouter() +@router.get("/users", response_model=Page[UserOut]) +async def get_users( + session: SessionDep, + _: CurrentUser, + pagination: PaginationDep, +): + return await paginate(select(User), session, pagination) + + @router.patch("/user", response_model=UserOut) async def change_user( session: SessionDep, @@ -18,3 +32,34 @@ async def change_user( await session.commit() await session.refresh(user) return user + + +@router.post("/user", response_model=UserOut) +async def create_user( + session: SessionDep, + user_in: UserCreate, + _: CurrentUser, +): + user = User( + username=user_in.username, + email=user_in.email, + password_hash=get_password_hash(user_in.password), + ) + session.add(user) + await session.commit() + await session.refresh(user) + return user + + +@router.delete("/user/{user_id}", response_model=UserOut) +async def delete_user( + session: SessionDep, + user_id: UUID, + _: CurrentUser, +): + user = await session.get(User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + await session.delete(user) + await session.commit() + return user diff --git a/src/frontend/src/lib/consts/backend.ts b/src/frontend/src/lib/consts/backend.ts index e6ffb623..e0870b99 100644 --- a/src/frontend/src/lib/consts/backend.ts +++ b/src/frontend/src/lib/consts/backend.ts @@ -65,6 +65,9 @@ export class Api { return { CONFIG: this.#url('admin/config'), USER_UPDATE: this.#url('admin/user'), + USERS: this.#url('admin/users'), + USER_CREATE: this.#url('admin/user'), + USER_DELETE: (id: string) => this.#url(`admin/user/${id}`), FILES: this.#url('admin/files'), FILE_REVOKE: (id: string) => this.#url(`admin/files/${id}`) }; diff --git a/src/frontend/src/lib/queries/admin_users.ts b/src/frontend/src/lib/queries/admin_users.ts new file mode 100644 index 00000000..8ea25679 --- /dev/null +++ b/src/frontend/src/lib/queries/admin_users.ts @@ -0,0 +1,61 @@ +import { Api } from '#consts/backend'; +import { createQuery, useQueryClient } from '@tanstack/svelte-query'; + +export const usersQueryKey = ['admin-users']; + +export const useUsersQuery = (page: () => number, size: number) => { + const queryClient = useQueryClient(); + + const users = createQuery(() => ({ + queryKey: [...usersQueryKey, page()], + queryFn: async () => { + const res = await fetch(`${Api.ADMIN.USERS}?page=${page()}&size=${size}`, { + credentials: 'include' + }); + + if (!res.ok) { + throw new Error('Failed to fetch users'); + } + + return res.json(); + } + })); + + const createUser = async (user_in: any) => { + const res = await fetch(Api.ADMIN.USER_CREATE, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(user_in), + credentials: 'include' + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.detail || 'Failed to create user'); + } + + const data = await res.json(); + queryClient.invalidateQueries({ queryKey: usersQueryKey }); + return data; + }; + + const deleteUser = async (user_id: string) => { + const res = await fetch(Api.ADMIN.USER_DELETE(user_id), { + method: 'DELETE', + credentials: 'include' + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({})); + throw new Error(error.detail || 'Failed to delete user'); + } + + const data = await res.json(); + queryClient.invalidateQueries({ queryKey: usersQueryKey }); + return data; + }; + + return { users, createUser, deleteUser }; +}; diff --git a/src/frontend/src/lib/queries/files.ts b/src/frontend/src/lib/queries/files.ts index 0293a40d..bc0bcd20 100644 --- a/src/frontend/src/lib/queries/files.ts +++ b/src/frontend/src/lib/queries/files.ts @@ -29,10 +29,12 @@ export type PaginatedFiles = { }; }; +const queryKey = ['admin-files']; + export const useFilesQuery = (page: () => number = () => 1, pageSize: number = 20) => { const queryClient = useQueryClient(); const query = createQuery(() => ({ - queryKey: ['admin-files', page(), pageSize], + queryKey: [...queryKey, page(), pageSize], queryFn: async () => { const url = new URL(Api.ADMIN.FILES, window.location.origin); url.searchParams.set('page', page().toString()); @@ -61,7 +63,7 @@ export const useFilesQuery = (page: () => number = () => 1, pageSize: number = 2 }); if (res.ok) { - await queryClient.invalidateQueries({ queryKey: ['admin-files'] }); + await queryClient.invalidateQueries({ queryKey: [...queryKey] }); } else { throw new Error('Failed to revoke file'); } diff --git a/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/app-sidebar.svelte b/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/app-sidebar.svelte index 30e7edee..f1c5ac34 100644 --- a/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/app-sidebar.svelte +++ b/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/app-sidebar.svelte @@ -1,6 +1,6 @@ diff --git a/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/users/+page.svelte b/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/users/+page.svelte new file mode 100644 index 00000000..d745079e --- /dev/null +++ b/src/frontend/src/routes/(needs_onboarding)/(login_required)/admin/users/+page.svelte @@ -0,0 +1,131 @@ + + +
Manage system users.
+