Let the admin see a list of users and the state of each one

This commit is contained in:
Savanni D'Gerinel 2025-01-26 19:58:59 -05:00
parent 90224a6841
commit dcd5514433
8 changed files with 53 additions and 30 deletions

View File

@ -140,6 +140,7 @@ impl Core {
.map(|user| UserOverview { .map(|user| UserOverview {
id: user.id, id: user.id,
name: user.name, name: user.name,
state: user.state,
is_admin: user.admin, is_admin: user.admin,
}) })
.collect()), .collect()),
@ -152,15 +153,10 @@ impl Core {
user_id: UserId, user_id: UserId,
) -> ResultExt<Option<UserOverview>, AppError, FatalError> { ) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
let users = return_error!(self.list_users().await); let users = return_error!(self.list_users().await);
let user = match users.into_iter().find(|user| user.id == user_id) { match users.into_iter().find(|user| user.id == user_id) {
Some(user) => user, Some(user) => ok(Some(user)),
None => return ok(None), None => return ok(None),
}; }
ok(Some(UserOverview {
id: user.id.clone(),
name: user.name,
is_admin: user.is_admin,
}))
} }
pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> { pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> {

View File

@ -52,14 +52,14 @@ impl From<crate::types::User> for User {
} }
} }
/* #[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[typeshare] #[typeshare]
pub struct UserOverview { pub struct UserOverview {
pub id: UserId, pub id: UserId,
pub name: String, pub name: String,
pub is_admin: bool, pub is_admin: bool,
pub state: AccountState,
pub games: Vec<crate::types::GameOverview>, pub games: Vec<crate::types::GameOverview>,
} }
@ -76,8 +76,8 @@ impl From<crate::types::UserOverview> for UserOverview {
id: input.id, id: input.id,
name: input.name, name: input.name,
is_admin: input.is_admin, is_admin: input.is_admin,
state: AccountState::from(input.state),
games: vec![], games: vec![],
} }
} }
} }
*/

View File

@ -10,6 +10,8 @@ use crate::{
types::{AppError, FatalError, User}, types::{AppError, FatalError, User},
}; };
use super::UserOverview;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare] #[typeshare]
pub struct AuthRequest { pub struct AuthRequest {
@ -106,21 +108,23 @@ pub async fn get_user(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
user_id: Option<UserId>, user_id: Option<UserId>,
) -> ResultExt<Option<crate::types::UserOverview>, AppError, FatalError> { ) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
auth_required(core.clone(), headers, |user| async move { auth_required(core.clone(), headers, |user| async move {
match user_id { match user_id {
Some(user_id) => core.user(user_id).await, Some(user_id) => core.user(user_id).await,
None => core.user(user.id).await, None => core.user(user.id).await,
} }
}).await .map(|maybe_user| maybe_user.map(|user| UserOverview::from(user)))
})
.await
} }
pub async fn get_users( pub async fn get_users(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
) -> ResultExt<Vec<crate::types::UserOverview>, AppError, FatalError> { ) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
auth_required(core.clone(), headers, |_user| async move { auth_required(core.clone(), headers, |_user| async move {
core.list_users().await core.list_users().await.map(|users| users.into_iter().map(|user| UserOverview::from(user)).collect::<Vec<UserOverview>>())
}) })
.await .await
} }

View File

@ -167,16 +167,15 @@ pub enum Message {
UpdateTabletop(Tabletop), UpdateTabletop(Tabletop),
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct UserOverview { pub struct UserOverview {
pub id: UserId, pub id: UserId,
pub name: String, pub name: String,
pub is_admin: bool, pub is_admin: bool,
pub state: AccountState,
} }
#[derive(Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[typeshare] #[typeshare]
pub struct GameOverview { pub struct GameOverview {

View File

@ -50,7 +50,7 @@ export class Client {
async users(sessionId: string) { async users(sessionId: string) {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = '/api/v1/users/'; url.pathname = '/api/v1/users';
return fetch(url, { return fetch(url, {
method: 'GET', method: 'GET',
headers: [['Authorization', `Bearer ${sessionId}`]] headers: [['Authorization', `Bearer ${sessionId}`]]

View File

@ -4,18 +4,23 @@ import './Profile.css';
interface ProfileProps { interface ProfileProps {
profile: UserOverview, profile: UserOverview,
users: UserOverview[],
games: GameOverview[], games: GameOverview[],
} }
export const ProfileElement = ({ profile, games }: ProfileProps) => { export const ProfileElement = ({ profile, users, games }: ProfileProps) => {
const adminNote = profile.isAdmin ? <div> <i>Note: this user is an admin</i> </div> : <></>; const adminNote = profile.isAdmin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
return (<div className="profile"> const userList = profile.isAdmin && <UserManagementElement users={users} />;
return (<div className="profile profile_columns">
<CardElement name={profile.name}> <CardElement name={profile.name}>
<div>Games: {games.map((game) => { <div>Games: {games.map((game) => {
return <span key={game.id}>{game.name} ({game.type})</span>; return <span key={game.id}>{game.name} ({game.type})</span>;
}) }</div> }) }</div>
{adminNote} {adminNote}
</CardElement> </CardElement>
{userList}
</div>) </div>)
} }

View File

@ -1,4 +1,4 @@
import { UserOverview } from "visions-types" import { AccountState, UserOverview } from "visions-types"
import { CardElement } from ".." import { CardElement } from ".."
interface UserManagementProps { interface UserManagementProps {
@ -7,10 +7,26 @@ interface UserManagementProps {
export const UserManagementElement = ({ users }: UserManagementProps) => { export const UserManagementElement = ({ users }: UserManagementProps) => {
return ( return (
<CardElement> <CardElement name="Users">
<ul> <table>
{users.map((user) => <li key={user.id}>{user.name}</li>)} <thead>
</ul> </thead>
<tbody>
{users.map((user) => <tr key={user.id}>
<td> {user.name} </td>
<td> {formatAccountState(user.state)} </td>
</tr>)}
</tbody>
</table>
<button>Create User</button>
</CardElement> </CardElement>
) )
} }
const formatAccountState = (state: AccountState): string => {
switch (state.type) {
case "Normal": return "";
case "PasswordReset": return "password reset";
case "Locked": return "locked";
}
}

View File

@ -17,13 +17,16 @@ export const MainView = ({ client }: MainProps) => {
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId) {
client.profile(sessionId, undefined).then((profile) => setProfile(profile)) client.profile(sessionId, undefined).then((profile) => setProfile(profile))
client.users(sessionId).then((users) => setUsers(users)) client.users(sessionId).then((users) => {
console.log(users);
setUsers(users);
})
} }
}, [sessionId, client]) }, [sessionId, client])
return ( return (
<div> <div>
{profile && <ProfileElement profile={profile} games={[]} />} {profile && <ProfileElement profile={profile} users={users} games={[]} />}
</div> </div>
) )
} }