From 90224a6841a7b469542c69d78bdc057e9ec39633 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 20 Jan 2025 22:19:16 -0500 Subject: [PATCH] Be able to authenticate and get back Success, PasswordReset, and Locked --- visions/server/src/core.rs | 7 -- visions/server/src/handlers/mod.rs | 9 +- visions/server/src/handlers/types.rs | 83 +++++++++++++++++++ .../server/src/handlers/user_management.rs | 9 +- visions/server/src/routes.rs | 46 ---------- visions/server/src/types.rs | 5 +- visions/ui/src/App.tsx | 4 +- visions/ui/src/client.ts | 21 +++-- .../components/GameOverview/GameOverview.tsx | 4 +- visions/ui/src/components/Profile/Profile.tsx | 22 ++--- .../UserManagement/UserManagement.tsx | 4 +- .../providers/StateProvider/StateProvider.tsx | 50 +++++------ visions/ui/src/views/Admin/Admin.tsx | 19 ++++- .../views/Authentication/Authentication.tsx | 34 ++++---- visions/ui/src/views/Main/Main.tsx | 8 +- 15 files changed, 184 insertions(+), 141 deletions(-) create mode 100644 visions/server/src/handlers/types.rs diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index d131afc..24e863c 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -141,7 +141,6 @@ impl Core { id: user.id, name: user.name, is_admin: user.admin, - games: vec![], }) .collect()), Err(err) => fatal(err), @@ -153,7 +152,6 @@ impl Core { user_id: UserId, ) -> ResultExt, AppError, FatalError> { let users = return_error!(self.list_users().await); - let games = return_error!(self.list_games().await); let user = match users.into_iter().find(|user| user.id == user_id) { Some(user) => user, None => return ok(None), @@ -162,11 +160,6 @@ impl Core { id: user.id.clone(), name: user.name, is_admin: user.is_admin, - games: games - .into_iter() - .filter(|g| g.gm == user.id) - .map(|g| g.id) - .collect(), })) } diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index b90c483..3e917bf 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -1,23 +1,20 @@ mod game_management; mod user_management; +mod types; + use axum::{http::StatusCode, Json}; use futures::Future; pub use game_management::*; pub use user_management::*; +pub use types::*; use result_extended::ResultExt; -use serde::{Deserialize, Serialize}; use crate::{ core::Core, types::{AppError, FatalError}, }; -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct HealthCheck { - pub ok: bool, -} - pub async fn wrap_handler(f: F) -> (StatusCode, Json>) where F: FnOnce() -> Fut, diff --git a/visions/server/src/handlers/types.rs b/visions/server/src/handlers/types.rs new file mode 100644 index 0000000..e485167 --- /dev/null +++ b/visions/server/src/handlers/types.rs @@ -0,0 +1,83 @@ +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::database::UserId; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct HealthCheck { + pub ok: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", content = "content")] +#[typeshare] +pub enum AccountState { + Normal, + PasswordReset(String), + Locked, +} + +impl From for AccountState { + fn from(s: crate::types::AccountState) -> Self { + match s { + crate::types::AccountState::Normal => Self::Normal, + crate::types::AccountState::PasswordReset(r) => { + Self::PasswordReset(format!("{}", r.format("%Y-%m-%d %H:%M:%S"))) + } + crate::types::AccountState::Locked => Self::Locked, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct User { + pub id: UserId, + pub name: String, + pub password: String, + pub admin: bool, + pub state: AccountState, +} + +impl From for User { + fn from(u: crate::types::User) -> Self { + Self { + id: u.id, + name: u.name, + password: u.password, + admin: u.admin, + state: AccountState::from(u.state), + } + } +} + +/* +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct UserOverview { + pub id: UserId, + pub name: String, + pub is_admin: bool, + pub games: Vec, +} + +impl UserOverview { + pub fn new(user: crate::types::UserOverview, games: Vec) -> Self { + let s = Self::from(user); + Self{ games, ..s } + } +} + +impl From for UserOverview { + fn from(input: crate::types::UserOverview) -> Self { + Self { + id: input.id, + name: input.name, + is_admin: input.is_admin, + games: vec![], + } + } +} +*/ diff --git a/visions/server/src/handlers/user_management.rs b/visions/server/src/handlers/user_management.rs index b5cfb9c..abd1006 100644 --- a/visions/server/src/handlers/user_management.rs +++ b/visions/server/src/handlers/user_management.rs @@ -7,7 +7,7 @@ use typeshare::typeshare; use crate::{ core::{AuthResponse, Core}, database::{SessionId, UserId}, - types::{AppError, FatalError, User, UserOverview}, + types::{AppError, FatalError, User}, }; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -106,20 +106,19 @@ pub async fn get_user( core: Core, headers: HeaderMap, user_id: Option, -) -> ResultExt, AppError, FatalError> { +) -> ResultExt, AppError, FatalError> { auth_required(core.clone(), headers, |user| async move { match user_id { Some(user_id) => core.user(user_id).await, None => core.user(user.id).await, } - }) - .await + }).await } pub async fn get_users( core: Core, headers: HeaderMap, -) -> ResultExt, AppError, FatalError> { +) -> ResultExt, AppError, FatalError> { auth_required(core.clone(), headers, |_user| async move { core.list_users().await }) diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index 37f4b08..10146c8 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -7,9 +7,7 @@ use axum::{ routing::{get, post, put}, Json, Router, }; -use serde::{Deserialize, Serialize}; use tower_http::cors::{Any, CorsLayer}; -use typeshare::typeshare; use crate::{ core::Core, @@ -20,50 +18,6 @@ use crate::{ }, }; -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "content")] -#[typeshare] -pub enum AccountState { - Normal, - PasswordReset(String), - Locked, -} - -impl From for AccountState { - fn from(s: crate::types::AccountState) -> Self { - match s { - crate::types::AccountState::Normal => Self::Normal, - crate::types::AccountState::PasswordReset(r) => { - Self::PasswordReset(format!("{}", r.format("%Y-%m-%d %H:%M:%S"))) - } - crate::types::AccountState::Locked => Self::Locked, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -#[typeshare] -pub struct User { - pub id: UserId, - pub name: String, - pub password: String, - pub admin: bool, - pub state: AccountState, -} - -impl From for User { - fn from(u: crate::types::User) -> Self { - Self { - id: u.id, - name: u.name, - password: u.password, - admin: u.admin, - state: AccountState::from(u.state), - } - } -} - pub fn routes(core: Core) -> Router { Router::new() .route( diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index c0a052d..6c99e31 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -167,16 +167,17 @@ pub enum Message { UpdateTabletop(Tabletop), } -#[derive(Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] #[typeshare] pub struct UserOverview { pub id: UserId, pub name: String, pub is_admin: bool, - pub games: Vec, } #[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] #[typeshare] pub struct GameOverview { pub id: GameId, diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index b6dcf0e..1f792ba 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -32,8 +32,8 @@ interface AuthedViewProps { const AuthedView = ({ client, children }: PropsWithChildren) => { const [state, manager] = useContext(StateContext) return ( - { - manager.setAdminPassword(password) + { + manager.setPassword(password1, password2) }} onAuth={(username, password) => manager.auth(username, password)}> {children} diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index 81c1a12..397c077 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -1,4 +1,4 @@ -import { SessionId, UserId, UserProfile } from "visions-types"; +import { AuthResponse, SessionId, UserId, UserOverview } from "visions-types"; export type PlayingField = { backgroundImage: string; @@ -63,14 +63,18 @@ export class Client { return fetch(url).then((response) => response.json()); } - async setAdminPassword(password: string) { + async setPassword(password_1: string, password_2: string) { const url = new URL(this.base); - url.pathname = `/api/v1/admin_password`; - console.log("setting the admin password to: ", password); - return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) }); + url.pathname = `/api/v1/user/password`; + + return fetch(url, { + method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ + password_1, password_2, + }) + }); } - async auth(username: string, password: string): Promise { + async auth(username: string, password: string): Promise { const url = new URL(this.base); url.pathname = `/api/v1/auth` const response = await fetch(url, { @@ -78,11 +82,10 @@ export class Client { headers: [['Content-Type', 'application/json']], body: JSON.stringify({ 'username': username, 'password': password }) }); - const session_id: SessionId = await response.json(); - return session_id; + return await response.json(); } - async profile(sessionId: SessionId, userId: UserId | undefined): Promise { + async profile(sessionId: SessionId, userId: UserId | undefined): Promise { const url = new URL(this.base); if (userId) { url.pathname = `/api/v1/user${userId}` diff --git a/visions/ui/src/components/GameOverview/GameOverview.tsx b/visions/ui/src/components/GameOverview/GameOverview.tsx index 976f2aa..a5e4190 100644 --- a/visions/ui/src/components/GameOverview/GameOverview.tsx +++ b/visions/ui/src/components/GameOverview/GameOverview.tsx @@ -2,8 +2,8 @@ import { GameOverview } from "visions-types" import { CardElement } from '../Card/Card'; -export const GameOverviewElement = ({ game_type, game_name, gm, players }: GameOverview) => { - return ( +export const GameOverviewElement = ({ name, gm, players }: GameOverview) => { + return (

GM {gm}

    {players.map((player) => player)} diff --git a/visions/ui/src/components/Profile/Profile.tsx b/visions/ui/src/components/Profile/Profile.tsx index 162b701..b531bfe 100644 --- a/visions/ui/src/components/Profile/Profile.tsx +++ b/visions/ui/src/components/Profile/Profile.tsx @@ -1,29 +1,21 @@ -import { UserProfile } from 'visions-types'; +import { GameOverview, UserOverview } from 'visions-types'; import { CardElement, GameOverviewElement, UserManagementElement } from '..'; import './Profile.css'; interface ProfileProps { - profile: UserProfile, - users: UserProfile[], + profile: UserOverview, + games: GameOverview[], } -export const ProfileElement = ({ profile, users }: ProfileProps) => { - const adminNote = profile.is_admin ?
    Note: this user is an admin
    : <>; +export const ProfileElement = ({ profile, games }: ProfileProps) => { + const adminNote = profile.isAdmin ?
    Note: this user is an admin
    : <>; return (
    -
    Games: {profile.games.map((game) => { - return {game.game_name} ({game.game_type}); +
    Games: {games.map((game) => { + return {game.name} ({game.type}); }) }
    {adminNote} - -
    - - -
    - {profile.games.map((game) => )} -
    -
    ) } diff --git a/visions/ui/src/components/UserManagement/UserManagement.tsx b/visions/ui/src/components/UserManagement/UserManagement.tsx index 14ef213..2b3d7b7 100644 --- a/visions/ui/src/components/UserManagement/UserManagement.tsx +++ b/visions/ui/src/components/UserManagement/UserManagement.tsx @@ -1,8 +1,8 @@ -import { UserProfile } from "visions-types" +import { UserOverview } from "visions-types" import { CardElement } from ".." interface UserManagementProps { - users: UserProfile[] + users: UserOverview[] } export const UserManagementElement = ({ users }: UserManagementProps ) => { diff --git a/visions/ui/src/providers/StateProvider/StateProvider.tsx b/visions/ui/src/providers/StateProvider/StateProvider.tsx index 82669dc..0abfead 100644 --- a/visions/ui/src/providers/StateProvider/StateProvider.tsx +++ b/visions/ui/src/providers/StateProvider/StateProvider.tsx @@ -3,7 +3,7 @@ import { SessionId, Status, Tabletop } from "visions-types"; import { Client } from "../../client"; import { assertNever } from "../../utils"; -type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", sessionId: string }; +type AuthState = { type: "Unauthed" } | { type: "Authed", sessionId: string } | { type: "PasswordReset", sessionId: string }; export enum LoadingState { Loading, @@ -21,7 +21,7 @@ type Action = { type: "SetAuthState", content: AuthState }; const initialState = (): AppState => { let state: AppState = { state: LoadingState.Ready, - auth: { type: "NoAdmin" }, + auth: { type: "Unauthed" }, tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }, } @@ -36,6 +36,7 @@ const initialState = (): AppState => { const stateReducer = (state: AppState, action: Action): AppState => { switch (action.type) { case "SetAuthState": { + console.log("setReducer: ", action); return { ...state, auth: action.content } } /* @@ -51,9 +52,9 @@ export const authState = (state: AppState): AuthState => state.auth export const getSessionId = (state: AppState): SessionId | undefined => { switch (state.auth.type) { - case "NoAdmin": return undefined case "Unauthed": return undefined case "Authed": return state.auth.sessionId + case "PasswordReset": return state.auth.sessionId default: { assertNever(state.auth) return undefined @@ -70,35 +71,38 @@ class StateManager { this.dispatch = dispatch; } - async status() { + async setPassword(password1: string, password2: string) { if (!this.client || !this.dispatch) return; - const { admin_enabled } = await this.client.health(); - if (!admin_enabled) { - this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } }); - } - - return admin_enabled; - } - - async setAdminPassword(password: string) { - if (!this.client || !this.dispatch) return; - - await this.client.setAdminPassword(password); - let admin_enabled = await this.status(); - if (admin_enabled) { - this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } }); - } + await this.client.setPassword(password1, password2); } async auth(username: string, password: string) { if (!this.client || !this.dispatch) return; - let sessionId = await this.client.auth(username, password); + let authResponse = await this.client.auth(username, password); + switch (authResponse.type) { + case "Success": { + window.localStorage.setItem("sessionId", authResponse.content); + this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId: authResponse.content } }); + break; + } + case "PasswordReset": { + window.localStorage.setItem("sessionId", authResponse.content); + this.dispatch({ type: "SetAuthState", content: { type: "PasswordReset", sessionId: authResponse.content } }); + break; + } + case "Locked": { + this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } }); + break; + } + } + /* if (sessionId) { window.localStorage.setItem("sessionId", sessionId); this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId } }); } + */ } } @@ -111,10 +115,6 @@ export const StateProvider = ({ client, children }: PropsWithChildren { - stateManager.current.status(); - }, [stateManager]); - return {children} ; diff --git a/visions/ui/src/views/Admin/Admin.tsx b/visions/ui/src/views/Admin/Admin.tsx index c588c16..721c17f 100644 --- a/visions/ui/src/views/Admin/Admin.tsx +++ b/visions/ui/src/views/Admin/Admin.tsx @@ -7,10 +7,27 @@ interface UserRowProps { } const UserRow = ({ user }: UserRowProps) => { + let accountState = "Normal"; + + switch (user.state.type) { + case "Normal": { + accountState = "Normal"; + break; + } + case "PasswordReset": { + accountState = `PasswordReset until ${user.state.content}`; + break; + } + case "Locked": { + accountState = "Locked"; + break; + } + } + return ( {user.name} {user.admin && "admin"} - {user.enabled && "enabled"} + {accountState} ); } diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index baf7d27..5d4478a 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -4,43 +4,36 @@ import { assertNever } from '../../utils'; import './Authentication.css'; interface AuthenticationProps { - onAdminPassword: (password: string) => void; + onSetPassword: (password1: string, password2: string) => void; onAuth: (username: string, password: string) => void; } -export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithChildren) => { +export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChildren) => { // No admin password set: prompt for the admin password // Password set, nobody logged in: prompt for login // User logged in: show the children let [userField, setUserField] = useState(""); - let [pwField, setPwField] = useState(""); + let [pwField1, setPwField1] = useState(""); + let [pwField2, setPwField2] = useState(""); let [state, _] = useContext(StateContext); + console.log("Authentication component", state.state); + switch (state.state) { case LoadingState.Loading: { return
    Loading
    } case LoadingState.Ready: { switch (state.auth.type) { - case "NoAdmin": { - return
    -
    -

    Welcome to your new Visions VTT Instance

    -

    Set your admin password:

    - setPwField(evt.target.value)} /> - onAdminPassword(pwField)} /> -
    -
    ; - } case "Unauthed": { return

    Welcome to Visions VTT

    setUserField(evt.target.value)} /> - setPwField(evt.target.value)} /> - onAuth(userField, pwField)} /> + setPwField1(evt.target.value)} /> + onAuth(userField, pwField1)} />
    ; @@ -48,6 +41,17 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC case "Authed": { return
    {children}
    ; } + case "PasswordReset": { + return
    +
    +

    Password Reset

    +

    Your password currently requires a reset.

    + setPwField1(evt.target.value)} /> + setPwField2(evt.target.value)} /> + onSetPassword(pwField1, pwField2)} /> +
    +
    ; + } default: { assertNever(state.auth); return
    ; diff --git a/visions/ui/src/views/Main/Main.tsx b/visions/ui/src/views/Main/Main.tsx index 14f1126..4c7c921 100644 --- a/visions/ui/src/views/Main/Main.tsx +++ b/visions/ui/src/views/Main/Main.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from 'react'; -import { UserProfile } from 'visions-types'; +import { UserOverview } from 'visions-types'; import { Client } from '../../client'; import { ProfileElement } from '../../components'; import { getSessionId, StateContext, StateProvider } from '../../providers/StateProvider/StateProvider'; @@ -10,8 +10,8 @@ interface MainProps { export const MainView = ({ client }: MainProps) => { const [state, _manager] = useContext(StateContext) - const [profile, setProfile] = useState(undefined) - const [users, setUsers] = useState([]) + const [profile, setProfile] = useState(undefined) + const [users, setUsers] = useState([]) const sessionId = getSessionId(state) useEffect(() => { @@ -23,7 +23,7 @@ export const MainView = ({ client }: MainProps) => { return (
    - {profile && } + {profile && }
    ) }