Merge pull request #701 from LAION-AI/673-enhanced-admin-routing

673 enhanced admin management
This commit is contained in:
Keith Stevens
2023-01-15 08:34:09 +09:00
committed by GitHub
8 changed files with 191 additions and 60 deletions
+1 -1
View File
@@ -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,
+12 -9
View File
@@ -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<User[]>([]);
// Fetch and save the users.
// This follows useSWR's recommendation for simple pagination:
@@ -53,21 +54,23 @@ const UsersCell = () => {
<Thead>
<Tr>
<Th>Id</Th>
<Th>Email</Th>
<Th>Auth Id</Th>
<Th>Auth Method</Th>
<Th>Name</Th>
<Th>Role</Th>
<Th>Update</Th>
</Tr>
</Thead>
<Tbody>
{users.map((user, index) => (
<Tr key={index}>
<Td>{user.id}</Td>
<Td>{user.email}</Td>
<Td>{user.name}</Td>
<Td>{user.role}</Td>
{users.map(({ id, user_id, auth_method, display_name, role }) => (
<Tr key={user_id}>
<Td>{user_id}</Td>
<Td>{id}</Td>
<Td>{auth_method}</Td>
<Td>{display_name}</Td>
<Td>{role}</Td>
<Td>
<Link href={`/admin/manage_user/${user.id}`}>Manage</Link>
<Link href={`/admin/manage_user/${user_id}`}>Manage</Link>
</Td>
</Tr>
))}
+56
View File
@@ -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<any> {
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<any> {
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<BackendUser> {
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<BackendUser[]> {
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<Message[]> {
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<void> {
return this.put(`/api/v1/users/users/${user_id}?enabled=${is_enabled}&notes=${notes}`);
}
/**
* Returns the valid labels for messages.
*/
+21 -17
View File
@@ -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 }) => {
}}
>
<Form>
<Field name="user_id" type="hidden" />
<Field name="id" type="hidden" />
<Field name="name">
<Field name="auth_method" type="hidden" />
<Field name="display_name">
{({ field }) => (
<FormControl>
<FormLabel>Username</FormLabel>
<FormLabel>Display Name</FormLabel>
<Input {...field} isDisabled />
</FormControl>
)}
</Field>
<Field name="email">
{({ field }) => (
<FormControl>
<FormLabel>Email</FormLabel>
<Input {...field} isDisabled />
</FormControl>
)}
</Field>
<Field name="role">
{({ field }) => (
<FormControl>
@@ -98,13 +92,21 @@ const ManageUser = ({ user }) => {
</FormControl>
)}
</Field>
<Field name="notes">
{({ field }) => (
<FormControl>
<FormLabel>Notes</FormLabel>
<Input {...field} />
</FormControl>
)}
</Field>
<Button mt={4} type="submit">
Update
</Button>
</Form>
</Formik>
</Container>
<UserMessagesCell path={`/api/admin/user_messages?user=${user.id}`} />
<UserMessagesCell path={`/api/admin/user_messages?user=${user.user_id}`} />
</Stack>
</>
);
@@ -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,
+16 -10
View File
@@ -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;
+6 -8
View File
@@ -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);
});
+28 -15
View File
@@ -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);
+51
View File
@@ -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;
}