feat: Consolidate Admin User APIs (#5050)

Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Michael Genson 2025-06-30 05:13:42 -05:00 committed by GitHub
parent 31f90c79c0
commit 47eb1ebbb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 24 additions and 53 deletions

View file

@ -1,4 +1,4 @@
import { useUserApi } from "~/composables/api";
import { useAdminApi } from "~/composables/api";
import type { UserIn, UserOut } from "~/lib/api/types/user";
/*
@ -8,7 +8,7 @@ to control whether the object is substantiated... but some of the others rely on
*/
export const useAllUsers = function () {
const api = useUserApi();
const api = useAdminApi();
const asyncKey = String(Date.now());
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
const { data } = await api.users.getAll();
@ -24,7 +24,7 @@ export const useAllUsers = function () {
};
export const useUser = function (refreshFunc: CallableFunction | null = null) {
const api = useUserApi();
const api = useAdminApi();
const loading = ref(false);
function getUser(id: string) {

View file

@ -1,6 +1,6 @@
from functools import cached_property
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import UUID4
from mealie.core import security
@ -42,6 +42,11 @@ class AdminUserManagementRoutes(BaseAdminController):
@router.post("", response_model=UserOut, status_code=201)
def create_one(self, data: UserIn):
if self.repos.users.get_by_username(data.username):
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.username-conflict-error")})
elif self.repos.users.get_one(data.email, "email"):
raise HTTPException(status.HTTP_409_CONFLICT, {"message": self.t("exceptions.email-conflict-error")})
data.password = security.hash_password(data.password)
return self.mixins.create_one(data)

View file

@ -9,7 +9,6 @@ router = APIRouter()
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
router.include_router(crud.user_router)
router.include_router(crud.admin_router)
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
router.include_router(api_tokens.router)

View file

@ -1,52 +1,17 @@
from fastapi import Depends, HTTPException, status
from fastapi import HTTPException, status
from pydantic import UUID4
from mealie.core.security import hash_password
from mealie.core.security.providers.credentials_provider import CredentialsProvider
from mealie.db.models.users.users import AuthMethod
from mealie.routes._base import BaseAdminController, BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.response import ErrorResponse, SuccessResponse
from mealie.schema.response.pagination import PaginationQuery
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary
from mealie.schema.user import ChangePassword, UserBase, UserOut
from mealie.schema.user.user import UserRatings, UserRatingSummary
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
@controller(admin_router)
class AdminUserController(BaseAdminController):
@property
def mixins(self) -> HttpRepo:
return HttpRepo[UserIn, UserOut, UserBase](self.repos.users, self.logger)
@admin_router.get("", response_model=UserPagination)
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
"""Returns all users from all groups"""
response = self.repos.users.page_all(
pagination=q,
override=UserOut,
)
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.model_dump())
return response
@admin_router.post("", response_model=UserOut, status_code=201)
def create_user(self, new_user: UserIn):
new_user.password = hash_password(new_user.password)
return self.mixins.create_one(new_user)
@admin_router.get("/{item_id}", response_model=UserOut)
def get_user(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@admin_router.delete("/{item_id}")
def delete_user(self, item_id: UUID4):
self.mixins.delete_one(item_id)
@controller(user_router)

View file

@ -76,7 +76,7 @@ def h2_user(session: Session, admin_token, api_client: TestClient, unique_user:
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=user_data, headers=admin_token)
response = api_client.post(api_routes.admin_users, json=user_data, headers=admin_token)
assert response.status_code == 201
# Log in as this user
@ -135,7 +135,7 @@ def g2_user(session: Session, admin_token, api_client: TestClient):
}
api_client.post(api_routes.admin_groups, json={"name": group}, headers=admin_token)
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
assert response.status_code == 201
@ -258,7 +258,7 @@ def user_tuple(session: Session, admin_token, api_client: TestClient) -> Generat
users_out = []
for usr in [create_data_1, create_data_2]:
response = api_client.post(api_routes.users, json=usr, headers=admin_token)
response = api_client.post(api_routes.admin_users, json=usr, headers=admin_token)
assert response.status_code == 201
# Log in as this user
@ -312,7 +312,7 @@ def user_token(admin_token, api_client: TestClient):
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
response = api_client.post(api_routes.admin_users, json=create_data, headers=admin_token)
assert response.status_code == 201

View file

@ -24,7 +24,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=admin_token)
assert response.status_code == 201
response = api_client.get(api_routes.users, headers=admin_token, params={"perPage": -1})
response = api_client.get(api_routes.admin_users, headers=admin_token, params={"perPage": -1})
assert response.status_code == 200
for user in response.json()["items"]:
for user_token in user["tokens"] or []:
@ -39,7 +39,7 @@ def test_api_token_private(api_client: TestClient, admin_token):
def test_use_token(api_client: TestClient, long_live_token):
response = api_client.get(api_routes.users, headers=long_live_token)
response = api_client.get(api_routes.admin_users, headers=long_live_token)
assert response.status_code == 200

View file

@ -88,9 +88,11 @@ def test_user_update(api_client: TestClient, unique_user: TestUser, admin_user:
def test_admin_updates(api_client: TestClient, admin_user: TestUser, unique_user: TestUser):
response = api_client.get(api_routes.users_item_id(unique_user.user_id), headers=admin_user.token)
response = api_client.get(api_routes.admin_users_item_id(unique_user.user_id), headers=admin_user.token)
assert response.status_code == 200
user = response.json()
response = api_client.get(api_routes.users_item_id(admin_user.user_id), headers=admin_user.token)
response = api_client.get(api_routes.admin_users_item_id(admin_user.user_id), headers=admin_user.token)
admin = response.json()
# admin updating themselves