Skip to content

Commit

Permalink
Allow admins to view and edit users
Browse files Browse the repository at this point in the history
- backend
  - add /admin/users/ endpoint to get a list of users
  - refactor admin endpoints into separate files for languages, milestones, users, questions
  - refactor tests to also create a temporary in-memory users database for each test
    - add pytest-asyncio to allow use of async fixtures
  - refactor tests to be fully independent of each other
    - new app & new temp dirs for each test
    - makes test suite slower to run but easier to maintain
  - remove DATA_FILES_PATH as we already have STATIC_FILES_PATH and PRIVATE_FILES_PATH
- admin-frontend
  - add users tab with table of users whose permissions can be edited
- resolves #164
  • Loading branch information
lkeegan committed Nov 14, 2024
1 parent 1496a3a commit e9563ea
Show file tree
Hide file tree
Showing 24 changed files with 1,100 additions and 856 deletions.
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ services:
- ${PRIVATE_FILES_PATH:-./private}:/app/private
environment:
- SECRET=${SECRET:-}
- DATA_FILES_PATH=/app/data
- STATIC_FILES_PATH=/app/static
- PRIVATE_FILES_PATH=/app/private
- DATABASE_PATH=/app/db
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/lib/client/services.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import { createClient, createConfig, type Options, formDataBodySerializer, urlSearchParamsBodySerializer } from '@hey-api/client-fetch';
import type { GetLanguagesError, GetLanguagesResponse, GetMilestonesError, GetMilestonesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, GetUserQuestionsError, GetUserQuestionsResponse, GetChildQuestionsError, GetChildQuestionsResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, UsersCurrentUserError, UsersCurrentUserResponse, UsersPatchCurrentUserData, UsersPatchCurrentUserError, UsersPatchCurrentUserResponse, UsersUserData, UsersUserError, UsersUserResponse, UsersPatchUserData, UsersPatchUserError, UsersPatchUserResponse, UsersDeleteUserData, UsersDeleteUserError, UsersDeleteUserResponse, GetChildrenError, GetChildrenResponse, UpdateChildData, UpdateChildError, UpdateChildResponse, CreateChildData, CreateChildError, CreateChildResponse, GetChildData, GetChildError, GetChildResponse, DeleteChildData, DeleteChildError, DeleteChildResponse, GetChildImageData, GetChildImageError, GetChildImageResponse, UploadChildImageData, UploadChildImageError, UploadChildImageResponse, DeleteChildImageData, DeleteChildImageError, DeleteChildImageResponse, GetCurrentMilestoneAnswerSessionData, GetCurrentMilestoneAnswerSessionError, GetCurrentMilestoneAnswerSessionResponse, UpdateMilestoneAnswerData, UpdateMilestoneAnswerError, UpdateMilestoneAnswerResponse, GetCurrentUserAnswersError, GetCurrentUserAnswersResponse, UpdateCurrentUserAnswersData, UpdateCurrentUserAnswersError, UpdateCurrentUserAnswersResponse, GetCurrentChildAnswersData, GetCurrentChildAnswersError, GetCurrentChildAnswersResponse, UpdateCurrentChildAnswersData, UpdateCurrentChildAnswersError, UpdateCurrentChildAnswersResponse, AuthCookieLoginData, AuthCookieLoginError, AuthCookieLoginResponse, AuthCookieLogoutError, AuthCookieLogoutResponse, RegisterRegisterData, RegisterRegisterError, RegisterRegisterResponse, ResetForgotPasswordData, ResetForgotPasswordError, ResetForgotPasswordResponse, ResetResetPasswordData, ResetResetPasswordError, ResetResetPasswordResponse, VerifyRequestTokenData, VerifyRequestTokenError, VerifyRequestTokenResponse, VerifyVerifyData, VerifyVerifyError, VerifyVerifyResponse, AuthError, AuthResponse } from './types.gen';
import type { GetLanguagesError, GetLanguagesResponse, GetMilestonesError, GetMilestonesResponse, GetMilestoneData, GetMilestoneError, GetMilestoneResponse, GetMilestoneGroupsData, GetMilestoneGroupsError, GetMilestoneGroupsResponse, GetUserQuestionsError, GetUserQuestionsResponse, GetChildQuestionsError, GetChildQuestionsResponse, CreateLanguageData, CreateLanguageError, CreateLanguageResponse, DeleteLanguageData, DeleteLanguageError, DeleteLanguageResponse, UpdateI18NData, UpdateI18NError, UpdateI18NResponse, GetMilestoneGroupsAdminError, GetMilestoneGroupsAdminResponse, CreateMilestoneGroupAdminError, CreateMilestoneGroupAdminResponse, UpdateMilestoneGroupAdminData, UpdateMilestoneGroupAdminError, UpdateMilestoneGroupAdminResponse, DeleteMilestoneGroupAdminData, DeleteMilestoneGroupAdminError, DeleteMilestoneGroupAdminResponse, UploadMilestoneGroupImageData, UploadMilestoneGroupImageError, UploadMilestoneGroupImageResponse, CreateMilestoneData, CreateMilestoneError, CreateMilestoneResponse, UpdateMilestoneData, UpdateMilestoneError, UpdateMilestoneResponse, DeleteMilestoneData, DeleteMilestoneError, DeleteMilestoneResponse, UploadMilestoneImageData, UploadMilestoneImageError, UploadMilestoneImageResponse, DeleteMilestoneImageData, DeleteMilestoneImageError, DeleteMilestoneImageResponse, GetMilestoneAgeScoresData, GetMilestoneAgeScoresError, GetMilestoneAgeScoresResponse, GetUserQuestionsAdminError, GetUserQuestionsAdminResponse, UpdateUserQuestionData, UpdateUserQuestionError, UpdateUserQuestionResponse, CreateUserQuestionError, CreateUserQuestionResponse, DeleteUserQuestionData, DeleteUserQuestionError, DeleteUserQuestionResponse, GetChildQuestionsAdminError, GetChildQuestionsAdminResponse, UpdateChildQuestionData, UpdateChildQuestionError, UpdateChildQuestionResponse, CreateChildQuestionError, CreateChildQuestionResponse, DeleteChildQuestionData, DeleteChildQuestionError, DeleteChildQuestionResponse, GetUsersError, GetUsersResponse, UsersCurrentUserError, UsersCurrentUserResponse, UsersPatchCurrentUserData, UsersPatchCurrentUserError, UsersPatchCurrentUserResponse, UsersUserData, UsersUserError, UsersUserResponse, UsersPatchUserData, UsersPatchUserError, UsersPatchUserResponse, UsersDeleteUserData, UsersDeleteUserError, UsersDeleteUserResponse, GetChildrenError, GetChildrenResponse, UpdateChildData, UpdateChildError, UpdateChildResponse, CreateChildData, CreateChildError, CreateChildResponse, GetChildData, GetChildError, GetChildResponse, DeleteChildData, DeleteChildError, DeleteChildResponse, GetChildImageData, GetChildImageError, GetChildImageResponse, UploadChildImageData, UploadChildImageError, UploadChildImageResponse, DeleteChildImageData, DeleteChildImageError, DeleteChildImageResponse, GetCurrentMilestoneAnswerSessionData, GetCurrentMilestoneAnswerSessionError, GetCurrentMilestoneAnswerSessionResponse, UpdateMilestoneAnswerData, UpdateMilestoneAnswerError, UpdateMilestoneAnswerResponse, GetCurrentUserAnswersError, GetCurrentUserAnswersResponse, UpdateCurrentUserAnswersData, UpdateCurrentUserAnswersError, UpdateCurrentUserAnswersResponse, GetCurrentChildAnswersData, GetCurrentChildAnswersError, GetCurrentChildAnswersResponse, UpdateCurrentChildAnswersData, UpdateCurrentChildAnswersError, UpdateCurrentChildAnswersResponse, AuthCookieLoginData, AuthCookieLoginError, AuthCookieLoginResponse, AuthCookieLogoutError, AuthCookieLogoutResponse, RegisterRegisterData, RegisterRegisterError, RegisterRegisterResponse, ResetForgotPasswordData, ResetForgotPasswordError, ResetForgotPasswordResponse, ResetResetPasswordData, ResetResetPasswordError, ResetResetPasswordResponse, VerifyRequestTokenData, VerifyRequestTokenError, VerifyRequestTokenResponse, VerifyVerifyData, VerifyVerifyError, VerifyVerifyResponse, AuthError, AuthResponse } from './types.gen';

export const client = createClient(createConfig());

Expand Down Expand Up @@ -295,6 +295,16 @@ export const deleteChildQuestion = <ThrowOnError extends boolean = false>(option
});
};

/**
* Get Users
*/
export const getUsers = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => {
return (options?.client ?? client).get<GetUsersResponse, GetUsersError, ThrowOnError>({
...options,
url: '/admin/users/'
});
};

/**
* Users:Current User
*/
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,10 @@ export type DeleteChildQuestionResponse = (unknown);

export type DeleteChildQuestionError = (HTTPValidationError);

export type GetUsersResponse = (Array<UserRead>);

export type GetUsersError = unknown;

export type UsersCurrentUserResponse = (UserRead);

export type UsersCurrentUserError = (unknown);
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/lib/components/Admin/Users.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<svelte:options runes={true} />

<script lang="ts">
import SaveButton from "$lib/components/Admin/SaveButton.svelte";
import {
Card,
Checkbox,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
} from "flowbite-svelte";
import { getUsers, usersPatchUser } from "$lib/client/services.gen";
import type { UserRead, UserUpdate } from "$lib/client/types.gen";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let users = $state([] as Array<UserRead>);
let saveDisabled = $state({} as Record<string, boolean>);
async function refreshUsers() {
const { data, error } = await getUsers();
if (error || !data) {
console.log(error);
} else {
saveDisabled = {};
users = data;
for (const user of users) {
saveDisabled[user.id] = true;
}
}
}
async function updateUser(user: UserRead) {
const { data, error } = await usersPatchUser({
body: {
is_active: user.is_active,
is_verified: user.is_verified,
is_researcher: user.is_researcher,
is_superuser: user.is_superuser,
},
path: {
id: `${user.id}`,
},
});
if (error || !data) {
console.log(error);
} else {
await refreshUsers();
}
}
onMount(async () => {
await refreshUsers();
});
</script>

<Card size="xl" class="m-5 w-full">
<h3 class="mb-3 text-xl font-medium text-gray-900 dark:text-white">
{$_("admin.users")}
</h3>
<Table>
<TableHead>
<TableHeadCell>Email</TableHeadCell>
<TableHeadCell>Active</TableHeadCell>
<TableHeadCell>Verified</TableHeadCell>
<TableHeadCell>Researcher</TableHeadCell>
<TableHeadCell>Admin</TableHeadCell>
<TableHeadCell>Actions</TableHeadCell>
</TableHead>
<TableBody>
{#each users as user (user.id)}
<TableBodyRow>
<TableBodyCell>
{user.email}
</TableBodyCell>
<TableBodyCell>
<Checkbox bind:checked={user.is_active} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<Checkbox bind:checked={user.is_verified} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<Checkbox bind:checked={user.is_researcher} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<Checkbox bind:checked={user.is_superuser} onchange={() => {saveDisabled[user.id]=false}} />
</TableBodyCell>
<TableBodyCell>
<SaveButton disabled={saveDisabled[user.id]} onclick={() => {updateUser(user)}} />
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</Table>
</Card>
9 changes: 9 additions & 0 deletions frontend/src/lib/components/AdminPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import MilestoneExpectedAges from "$lib/components/Admin/MilestoneExpectedAges.s
import MilestoneGroups from "$lib/components/Admin/MilestoneGroups.svelte";
import Questions from "$lib/components/Admin/Questions.svelte";
import Translations from "$lib/components/Admin/Translations.svelte";
import Users from "$lib/components/Admin/Users.svelte";
import { TabItem, Tabs } from "flowbite-svelte";
import {
BadgeCheckOutline,
ClipboardListOutline,
LanguageOutline,
ScaleBalancedOutline,
UsersOutline,
} from "flowbite-svelte-icons";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
Expand All @@ -38,6 +40,13 @@ onMount(async () => {
</div>
<MilestoneExpectedAges/>
</TabItem>
<TabItem>
<div slot="title" class="flex items-center gap-2">
<UsersOutline size="md"/>
{$_("admin.users")}
</div>
<Users/>
</TabItem>
<TabItem>
<div slot="title" class="flex items-center gap-2">
<ClipboardListOutline size="md"/>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"milestones": "Meilensteine",
"milestone-groups": "Meilensteingruppen",
"translations": "Übersetzungen",
"users": "Benutzer",
"user-questions": "Fragen über Beobachter",
"child-questions": "Fragen über Kind",
"question": "Frage",
Expand Down
2 changes: 1 addition & 1 deletion mondey_backend/openapi.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion mondey_backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies = [
dynamic = ["version"]

[project.optional-dependencies]
tests = ["pytest", "pytest-randomly", "pytest-cov", "pytest-mock"]
tests = ["pytest", "pytest-randomly", "pytest-cov", "pytest-mock", "pytest-asyncio"]

[project.scripts]
mondey-backend = "mondey_backend.main:main"
Expand All @@ -34,6 +34,7 @@ path = "src/mondey_backend/__init__.py"

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_default_fixture_loop_scope = "function"

[tool.ruff.lint]
select = [
Expand Down
4 changes: 4 additions & 0 deletions mondey_backend/src/mondey_backend/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import Session

from .databases.milestones import engine as milestones_engine
from .databases.users import get_async_session
from .models.users import User
from .users import fastapi_users

Expand All @@ -22,6 +24,8 @@ def get_session():

CurrentActiveUserDep = Annotated[User, Depends(current_active_user)]

UserAsyncSessionDep = Annotated[AsyncSession, Depends(get_async_session)]


def current_active_researcher(
user: Annotated[User, Depends(current_active_user)],
Expand Down
Loading

0 comments on commit e9563ea

Please sign in to comment.