diff --git a/backend/oasst_backend/api/v1/users.py b/backend/oasst_backend/api/v1/users.py index f09d58b5..565499a7 100644 --- a/backend/oasst_backend/api/v1/users.py +++ b/backend/oasst_backend/api/v1/users.py @@ -15,7 +15,7 @@ from starlette.status import HTTP_204_NO_CONTENT router = APIRouter() -@router.get("/users/{user_id}", response_model=protocol.User) +@router.get("/users/{user_id}", response_model=protocol.FrontEndUser) def get_user( user_id: UUID, api_client_id: UUID = None, diff --git a/website/src/components/UsersCell.tsx b/website/src/components/UsersCell.tsx index 63c923a7..f5545be1 100644 --- a/website/src/components/UsersCell.tsx +++ b/website/src/components/UsersCell.tsx @@ -15,6 +15,7 @@ import { import Link from "next/link"; import { useState } from "react"; import { get } from "src/lib/api"; +import type { User } from "src/types/Users"; import useSWR from "swr"; /** @@ -22,7 +23,7 @@ import useSWR from "swr"; */ const UsersCell = () => { const [pageIndex, setPageIndex] = useState(0); - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); // Fetch and save the users. // This follows useSWR's recommendation for simple pagination: @@ -53,21 +54,23 @@ const UsersCell = () => { Id - Email + Auth Id + Auth Method Name Role Update - {users.map((user, index) => ( - - {user.id} - {user.email} - {user.name} - {user.role} + {users.map(({ id, user_id, auth_method, display_name, role }) => ( + + {user_id} + {id} + {auth_method} + {display_name} + {role} - Manage + Manage ))} diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index b2ece97b..43ca6c49 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -1,4 +1,6 @@ import { JWT } from "next-auth/jwt"; +import type { Message } from "src/types/Conversation"; +import type { BackendUser } from "src/types/Users"; export class OasstError { message: string; @@ -43,6 +45,32 @@ export class OasstApiClient { return await resp.json(); } + private async put(path: string): Promise { + const resp = await fetch(`${this.oasstApiUrl}${path}`, { + method: "PUT", + headers: { + "X-API-Key": this.oasstApiKey, + }, + }); + + if (resp.status === 204) { + return null; + } + + if (resp.status >= 300) { + const errorText = await resp.text(); + let error: any; + try { + error = JSON.parse(errorText); + } catch (e) { + throw new OasstError(errorText, 0, resp.status); + } + throw new OasstError(error.message ?? error, error.error_code, resp.status); + } + + return await resp.json(); + } + private async get(path: string): Promise { const resp = await fetch(`${this.oasstApiUrl}${path}`, { method: "GET", @@ -121,6 +149,34 @@ export class OasstApiClient { }); } + /** + * Returns the `BackendUser` associated with `user_id` + */ + async fetch_user(user_id: string): Promise { + return this.get(`/api/v1/users/users/${user_id}`); + } + + /** + * Returns the `max_count` `BackendUser`s stored by the backend. + */ + async fetch_users(max_count: number): Promise { + return this.get(`/api/v1/frontend_users/?max_count=${max_count}`); + } + + /** + * Returns the `Message`s associated with `user_id` in the backend. + */ + async fetch_user_messages(user_id: string): Promise { + return this.get(`/api/v1/users/${user_id}/messages`); + } + + /** + * Updates the backend's knowledge about the `user_id`. + */ + async set_user_status(user_id: string, is_enabled: boolean, notes): Promise { + return this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}¬es=${notes}`); + } + /** * Returns the valid labels for messages. */ diff --git a/website/src/pages/admin/manage_user/[id].tsx b/website/src/pages/admin/manage_user/[id].tsx index 6386c155..90698f4a 100644 --- a/website/src/pages/admin/manage_user/[id].tsx +++ b/website/src/pages/admin/manage_user/[id].tsx @@ -7,6 +7,7 @@ import { useEffect } from "react"; import { getAdminLayout } from "src/components/Layout"; import { UserMessagesCell } from "src/components/UserMessagesCell"; import { post } from "src/lib/api"; +import { oasstApiClient } from "src/lib/oasst_api_client"; import prisma from "src/lib/prismadb"; import useSWRMutation from "swr/mutation"; @@ -68,24 +69,17 @@ const ManageUser = ({ user }) => { }} >
+ - + + {({ field }) => ( - Username + Display Name )} - - {({ field }) => ( - - Email - - - )} - - {({ field }) => ( @@ -98,13 +92,21 @@ const ManageUser = ({ user }) => { )} + + {({ field }) => ( + + Notes + + + )} + - + ); @@ -114,15 +116,17 @@ const ManageUser = ({ user }) => { * Fetch the user's data on the server side when rendering. */ export async function getServerSideProps({ query }) { - const user = await prisma.user.findUnique({ - where: { id: query.id }, + const backend_user = await oasstApiClient.fetch_user(query.id); + const local_user = await prisma.user.findUnique({ + where: { id: backend_user.id }, select: { - id: true, - name: true, - email: true, role: true, }, }); + const user = { + ...backend_user, + role: local_user?.role || "general", + }; return { props: { user, diff --git a/website/src/pages/api/admin/update_user.ts b/website/src/pages/api/admin/update_user.ts index 95ddff4b..341ec736 100644 --- a/website/src/pages/api/admin/update_user.ts +++ b/website/src/pages/api/admin/update_user.ts @@ -1,22 +1,28 @@ import { withRole } from "src/lib/auth"; +import { oasstApiClient } from "src/lib/oasst_api_client"; import prisma from "src/lib/prismadb"; /** * Update's the user's data in the database. Accessible only to admins. */ const handler = withRole("admin", async (req, res) => { - const { id, role } = req.body; + const { id, auth_method, user_id, notes, role } = req.body; - await prisma.user.update({ - where: { - id, - }, - data: { - role, - }, - }); + // If the user is authorized by the web, update their role. + if (auth_method === "local") { + await prisma.user.update({ + where: { + id, + }, + data: { + role, + }, + }); + } + // Tell the backend the user's enabled or not enabled status. + await oasstApiClient.set_user_status(user_id, role !== "banned", notes); - res.status(200).end(); + res.status(200).json({}); }); export default handler; diff --git a/website/src/pages/api/admin/user_messages.ts b/website/src/pages/api/admin/user_messages.ts index 254bf9c6..236afa2d 100644 --- a/website/src/pages/api/admin/user_messages.ts +++ b/website/src/pages/api/admin/user_messages.ts @@ -1,15 +1,13 @@ import { withRole } from "src/lib/auth"; +import { oasstApiClient } from "src/lib/oasst_api_client"; +import type { Message } from "src/types/Conversation"; +/** + * Returns the messages recorded by the backend for a user. + */ const handler = withRole("admin", async (req, res) => { const { user } = req.query; - - const messagesRes = await fetch(`${process.env.FASTAPI_URL}/api/v1/frontend_users/local/${user}/messages`, { - method: "GET", - headers: { - "X-API-Key": process.env.FASTAPI_KEY, - }, - }); - const messages = await messagesRes.json(); + const messages: Message[] = await oasstApiClient.fetch_user_messages(user as string); res.status(200).json(messages); }); diff --git a/website/src/pages/api/admin/users.ts b/website/src/pages/api/admin/users.ts index 7c71b667..04bcacf0 100644 --- a/website/src/pages/api/admin/users.ts +++ b/website/src/pages/api/admin/users.ts @@ -1,31 +1,44 @@ import { withRole } from "src/lib/auth"; +import { oasstApiClient } from "src/lib/oasst_api_client"; import prisma from "src/lib/prismadb"; -// The number of users to fetch in any request. -const PAGE_SIZE = 20; - /** * Returns a list of user results from the database when the requesting user is * a logged in admin. */ const handler = withRole("admin", async (req, res) => { - // Figure out the pagination index and skip that number of users. - // - // Note: with Prisma this isn't the most efficient but it's the only possible - // option with cuid based User IDs. - const { pageIndex } = req.query; - const skip = parseInt(pageIndex as string) * PAGE_SIZE || 0; + // TODO(#673): Update this to support pagination. - // Fetch 20 users. - const users = await prisma.user.findMany({ + // First, get all the users according to the backend. + const all_users = await oasstApiClient.fetch_users(20); + + // Next, get all the users stored in the web's auth datbase to fetch their role. + const local_user_ids = all_users.map(({ id }) => id); + const local_users = await prisma.user.findMany({ + where: { + id: { + in: local_user_ids, + }, + }, select: { id: true, role: true, - name: true, - email: true, }, - skip, - take: PAGE_SIZE, + }); + + // Combine the information by updating the set of full users with their role. + // Default any users without a role set locally as "general". + const local_user_map = local_users.reduce((result, user) => { + result.set(user.id, user.role); + return result; + }, new Map()); + + const users = all_users.map((user) => { + const role = local_user_map.get(user.id) || "general"; + return { + ...user, + role, + }; }); res.status(200).json(users); diff --git a/website/src/types/Users.ts b/website/src/types/Users.ts new file mode 100644 index 00000000..eeb1903a --- /dev/null +++ b/website/src/types/Users.ts @@ -0,0 +1,51 @@ +/** + * Reports the Backend's knowledge of a user. + */ +export interface BackendUser { + /** + * The user's unique ID according to the `auth_method`. + */ + id: string; + + /** + * The user's set name + */ + display_name: string; + + /** + * The authorization method. One of: + * - discord + * - local + */ + auth_method: string; + + /** + * The backend's UUID for this user. + */ + user_id: string; + + /** + * Arbitrary notes about the user. + */ + notes: string; + + /** + * True when the user is able to access the platform. False otherwise. + */ + enabled: boolean; + + /** + * True when the user is marked for deletion. False otherwise. + */ + deleted: boolean; +} + +/** + * An expanded User for the web. + */ +export interface User extends BackendUser { + /** + * The user's roles within the webapp. + */ + role: string; +}