Create a user expiration time and make new users immediately expired
This is to give the ability to force the user to create a new password as soon as they log in.
This commit is contained in:
parent
f9e903da54
commit
d0ba8d921d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -5377,6 +5377,7 @@ dependencies = [
|
|||||||
"authdb",
|
"authdb",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
|
"chrono",
|
||||||
"cool_asserts",
|
"cool_asserts",
|
||||||
"futures",
|
"futures",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
@ -10,6 +10,7 @@ async-std = { version = "1.13.0" }
|
|||||||
async-trait = { version = "0.1.83" }
|
async-trait = { version = "0.1.83" }
|
||||||
authdb = { path = "../../authdb/" }
|
authdb = { path = "../../authdb/" }
|
||||||
axum = { version = "0.7.9", features = [ "macros" ] }
|
axum = { version = "0.7.9", features = [ "macros" ] }
|
||||||
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
futures = { version = "0.3.31" }
|
futures = { version = "0.3.31" }
|
||||||
include_dir = { version = "0.7.4" }
|
include_dir = { version = "0.7.4" }
|
||||||
lazy_static = { version = "1.5.0" }
|
lazy_static = { version = "1.5.0" }
|
||||||
|
@ -3,7 +3,8 @@ CREATE TABLE users(
|
|||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
admin BOOLEAN,
|
admin BOOLEAN,
|
||||||
enabled BOOLEAN
|
enabled BOOLEAN,
|
||||||
|
password_expires TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE sessions(
|
CREATE TABLE sessions(
|
||||||
@ -39,4 +40,4 @@ CREATE TABLE roles(
|
|||||||
FOREIGN KEY(game_id) REFERENCES games(uuid)
|
FOREIGN KEY(game_id) REFERENCES games(uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO users VALUES ('admin', 'admin', '', true, true);
|
INSERT INTO users VALUES ('admin', 'admin', '', true, true, datetime('now'));
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_std::sync::RwLock;
|
use async_std::sync::RwLock;
|
||||||
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::{self, AssetId, Assets},
|
asset_db::{self, AssetId, Assets},
|
||||||
database::{CharacterId, Database, GameId, SessionId, UserId}, types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserProfile},
|
database::{CharacterId, Database, GameId, SessionId, UserId},
|
||||||
|
types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
||||||
@ -25,6 +27,14 @@ pub struct Status {
|
|||||||
pub admin_enabled: bool,
|
pub admin_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum AuthResponse {
|
||||||
|
Success(SessionId),
|
||||||
|
Expired(SessionId),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct WebsocketClient {
|
struct WebsocketClient {
|
||||||
sender: Option<UnboundedSender<Message>>,
|
sender: Option<UnboundedSender<Message>>,
|
||||||
@ -117,28 +127,41 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
|
pub async fn list_users(&self) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||||
let users = self.0.write().await.db.users().await;
|
let users = self.0.write().await.db.users().await;
|
||||||
match users {
|
match users {
|
||||||
Ok(users) => ok(users.into_iter().map(User::from).collect()),
|
Ok(users) => ok(users
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| UserOverview {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
is_admin: user.admin,
|
||||||
|
games: vec![],
|
||||||
|
})
|
||||||
|
.collect()),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(&self, user_id: UserId) -> ResultExt<Option<UserProfile>, AppError, FatalError> {
|
pub async fn user(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
|
||||||
let users = return_error!(self.list_users().await);
|
let users = return_error!(self.list_users().await);
|
||||||
let games = return_error!(self.list_games().await);
|
let games = return_error!(self.list_games().await);
|
||||||
let user = match users.into_iter().find(|user| user.id == user_id) {
|
let user = match users.into_iter().find(|user| user.id == user_id) {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => return ok(None),
|
None => return ok(None),
|
||||||
};
|
};
|
||||||
let user_games = games.into_iter().filter(|g| g.gm == user.id).collect();
|
ok(Some(UserOverview {
|
||||||
ok(Some(UserProfile {
|
id: user.id.clone(),
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
name: user.name,
|
||||||
password: user.password,
|
is_admin: user.is_admin,
|
||||||
games: user_games,
|
games: games
|
||||||
is_admin: user.admin,
|
.into_iter()
|
||||||
|
.filter(|g| g.gm == user.id)
|
||||||
|
.map(|g| g.id)
|
||||||
|
.collect(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,13 +169,21 @@ impl Core {
|
|||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match return_error!(self.user_by_username(username).await) {
|
match return_error!(self.user_by_username(username).await) {
|
||||||
Some(_) => error(AppError::UsernameUnavailable),
|
Some(_) => error(AppError::UsernameUnavailable),
|
||||||
None => match state.db.save_user(None, username, "", false, true).await {
|
None => match state
|
||||||
|
.db
|
||||||
|
.save_user(None, username, "", false, true, Utc::now())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(user_id) => ok(user_id),
|
Ok(user_id) => ok(user_id),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn disable_user(&self, userid: UserId) -> ResultExt<(), AppError, FatalError> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
|
pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
|
||||||
let games = self.0.read().await.db.games().await;
|
let games = self.0.read().await.db.games().await;
|
||||||
match games {
|
match games {
|
||||||
@ -162,7 +193,12 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_game(&self, gm: &UserId, game_type: &str, game_name: &str) -> ResultExt<GameId, AppError, FatalError> {
|
pub async fn create_game(
|
||||||
|
&self,
|
||||||
|
gm: &UserId,
|
||||||
|
game_type: &str,
|
||||||
|
game_name: &str,
|
||||||
|
) -> ResultExt<GameId, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state.db.save_game(None, gm, game_type, game_name).await {
|
match state.db.save_game(None, gm, game_type, game_name).await {
|
||||||
Ok(game_id) => ok(game_id),
|
Ok(game_id) => ok(game_id),
|
||||||
@ -256,7 +292,7 @@ impl Core {
|
|||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state
|
match state
|
||||||
.db
|
.db
|
||||||
.save_user(uuid, username, password, admin, enabled)
|
.save_user(uuid, username, password, admin, enabled, Utc::now())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(uuid) => ok(uuid),
|
Ok(uuid) => ok(uuid),
|
||||||
@ -277,7 +313,14 @@ impl Core {
|
|||||||
};
|
};
|
||||||
match state
|
match state
|
||||||
.db
|
.db
|
||||||
.save_user(Some(uuid), &user.name, &password, user.admin, user.enabled)
|
.save_user(
|
||||||
|
Some(uuid),
|
||||||
|
&user.name,
|
||||||
|
&password,
|
||||||
|
user.admin,
|
||||||
|
user.enabled,
|
||||||
|
Utc::now(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => ok(()),
|
Ok(_) => ok(()),
|
||||||
@ -289,19 +332,35 @@ impl Core {
|
|||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> ResultExt<SessionId, AppError, FatalError> {
|
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||||
let state = self.0.write().await;
|
let now = Utc::now();
|
||||||
|
let state = self.0.read().await;
|
||||||
match state.db.user_by_username(username).await {
|
match state.db.user_by_username(username).await {
|
||||||
Ok(Some(row)) if (row.password == password) => {
|
Ok(Some(row))
|
||||||
let session_id = state.db.create_session(&row.id).await.unwrap();
|
if (row.password == password) && row.enabled && row.password_expires.0 <= now =>
|
||||||
ok(session_id)
|
{
|
||||||
|
match state.db.create_session(&row.id).await {
|
||||||
|
Ok(session_id) => ok(AuthResponse::Success(session_id)),
|
||||||
|
Err(err) => fatal(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(row))
|
||||||
|
if (row.password == password) && row.enabled && row.password_expires.0 > now =>
|
||||||
|
{
|
||||||
|
match state.db.create_session(&row.id).await {
|
||||||
|
Ok(session_id) => ok(AuthResponse::Expired(session_id)),
|
||||||
|
Err(err) => fatal(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(_) => error(AppError::AuthFailed),
|
Ok(_) => error(AppError::AuthFailed),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn session(&self, session_id: &SessionId) -> ResultExt<Option<User>, AppError, FatalError> {
|
pub async fn session(
|
||||||
|
&self,
|
||||||
|
session_id: &SessionId,
|
||||||
|
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state.db.session(session_id).await {
|
match state.db.session(session_id).await {
|
||||||
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
||||||
@ -311,6 +370,10 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_expiration_date() -> DateTime<Utc> {
|
||||||
|
Utc::now() + TimeDelta::days(365)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -351,10 +414,17 @@ mod test {
|
|||||||
]);
|
]);
|
||||||
let memory_db: Option<PathBuf> = None;
|
let memory_db: Option<PathBuf> = None;
|
||||||
let conn = DbConn::new(memory_db);
|
let conn = DbConn::new(memory_db);
|
||||||
conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
conn.save_user(
|
||||||
|
Some(UserId::from("admin")),
|
||||||
|
"admin",
|
||||||
|
"aoeu",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Utc::now(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
conn.save_user(None, "gm_1", "aoeu", false, true)
|
conn.save_user(None, "gm_1", "aoeu", false, true, Utc::now())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Core::new(assets, conn)
|
Core::new(assets, conn)
|
||||||
@ -424,7 +494,7 @@ mod test {
|
|||||||
async fn it_creates_a_sessionid_on_successful_auth() {
|
async fn it_creates_a_sessionid_on_successful_auth() {
|
||||||
let core = test_core().await;
|
let core = test_core().await;
|
||||||
match core.auth("admin", "aoeu").await {
|
match core.auth("admin", "aoeu").await {
|
||||||
ResultExt::Ok(session_id) => {
|
ResultExt::Ok(AuthResponse::Success(session_id)) => {
|
||||||
let st = core.0.read().await;
|
let st = core.0.read().await;
|
||||||
match st.db.session(&session_id).await {
|
match st.db.session(&session_id).await {
|
||||||
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
|
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
|
||||||
@ -432,6 +502,7 @@ mod test {
|
|||||||
Err(err) => panic!("{}", err),
|
Err(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResultExt::Ok(AuthResponse::Expired(_)) => panic!("user has expired"),
|
||||||
ResultExt::Err(err) => panic!("{}", err),
|
ResultExt::Err(err) => panic!("{}", err),
|
||||||
ResultExt::Fatal(err) => panic!("{}", err),
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use async_std::channel::Receiver;
|
use async_std::channel::Receiver;
|
||||||
|
use chrono::Utc;
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
@ -12,7 +13,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
types::GameId, CharacterId, CharsheetRow, DatabaseRequest, GameRow, SessionId, UserId, UserRow
|
types::{DateTime, GameId},
|
||||||
|
CharacterId, CharsheetRow, DatabaseRequest, GameRow, SessionId, UserId, UserRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||||
@ -48,7 +50,7 @@ impl DiskDb {
|
|||||||
pub fn user(&self, id: &UserId) -> Result<Option<UserRow>, FatalError> {
|
pub fn user(&self, id: &UserId) -> Result<Option<UserRow>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?")
|
.prepare("SELECT * FROM users WHERE uuid=?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items: Vec<UserRow> = stmt
|
let items: Vec<UserRow> = stmt
|
||||||
.query_map([id.as_str()], |row| {
|
.query_map([id.as_str()], |row| {
|
||||||
@ -58,6 +60,7 @@ impl DiskDb {
|
|||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
enabled: row.get(4).unwrap(),
|
||||||
|
password_expires: row.get(5).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -73,7 +76,7 @@ impl DiskDb {
|
|||||||
pub fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
|
pub fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?")
|
.prepare("SELECT * FROM users WHERE name=?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items: Vec<UserRow> = stmt
|
let items: Vec<UserRow> = stmt
|
||||||
.query_map([username], |row| {
|
.query_map([username], |row| {
|
||||||
@ -83,6 +86,7 @@ impl DiskDb {
|
|||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
enabled: row.get(4).unwrap(),
|
||||||
|
password_expires: row.get(5).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -102,24 +106,39 @@ impl DiskDb {
|
|||||||
password: &str,
|
password: &str,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
expiration: chrono::DateTime<Utc>,
|
||||||
) -> Result<UserId, FatalError> {
|
) -> Result<UserId, FatalError> {
|
||||||
match user_id {
|
match user_id {
|
||||||
None => {
|
None => {
|
||||||
let user_id = UserId::default();
|
let user_id = UserId::default();
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")
|
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?, ?)")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
stmt.execute((user_id.as_str(), name, password, admin, enabled))
|
stmt.execute((
|
||||||
|
user_id.as_str(),
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
admin,
|
||||||
|
enabled,
|
||||||
|
format!("{}", expiration.format("%Y-%m-%d %H:%M:%S")),
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(user_id)
|
Ok(user_id)
|
||||||
}
|
}
|
||||||
Some(user_id) => {
|
Some(user_id) => {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?")
|
.prepare("UPDATE users SET name=?, password=?, admin=?, enabled=?, password_expires=? WHERE uuid=?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
stmt.execute((name, password, admin, enabled, user_id.as_str()))
|
stmt.execute((
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
admin,
|
||||||
|
enabled,
|
||||||
|
format!("{}", expiration.format("%Y-%m-%d %H:%M:%S")),
|
||||||
|
user_id.as_str(),
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(user_id)
|
Ok(user_id)
|
||||||
}
|
}
|
||||||
@ -139,6 +158,7 @@ impl DiskDb {
|
|||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
enabled: row.get(4).unwrap(),
|
||||||
|
password_expires: row.get(5).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -210,6 +230,7 @@ impl DiskDb {
|
|||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
enabled: row.get(4).unwrap(),
|
||||||
|
password_expires: row.get(5).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -316,12 +337,10 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Request::Games => {
|
Request::Games => match db.games() {
|
||||||
match db.games() {
|
|
||||||
Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(),
|
Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(),
|
||||||
_ => unimplemented!("errors for Request::Games"),
|
_ => unimplemented!("errors for Request::Games"),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Request::Game(_game_id) => {
|
Request::Game(_game_id) => {
|
||||||
unimplemented!("Request::Game handler");
|
unimplemented!("Request::Game handler");
|
||||||
}
|
}
|
||||||
@ -350,13 +369,14 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||||||
err => panic!("{:?}", err),
|
err => panic!("{:?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Request::SaveUser(user_id, username, password, admin, enabled) => {
|
Request::SaveUser(user_id, username, password, admin, enabled, expiration) => {
|
||||||
let user_id = db.save_user(
|
let user_id = db.save_user(
|
||||||
user_id,
|
user_id,
|
||||||
username.as_ref(),
|
username.as_ref(),
|
||||||
password.as_ref(),
|
password.as_ref(),
|
||||||
admin,
|
admin,
|
||||||
enabled,
|
enabled,
|
||||||
|
expiration,
|
||||||
);
|
);
|
||||||
match user_id {
|
match user_id {
|
||||||
Ok(user_id) => {
|
Ok(user_id) => {
|
||||||
|
@ -5,6 +5,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use async_std::channel::{bounded, Sender};
|
use async_std::channel::{bounded, Sender};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use disk_db::{db_handler, DiskDb};
|
use disk_db::{db_handler, DiskDb};
|
||||||
pub use types::{CharacterId, CharsheetRow, GameId, GameRow, SessionId, UserId, UserRow};
|
pub use types::{CharacterId, CharsheetRow, GameId, GameRow, SessionId, UserId, UserRow};
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ enum Request {
|
|||||||
Games,
|
Games,
|
||||||
Game(GameId),
|
Game(GameId),
|
||||||
SaveGame(Option<GameId>, UserId, String, String),
|
SaveGame(Option<GameId>, UserId, String, String),
|
||||||
SaveUser(Option<UserId>, String, String, bool, bool),
|
SaveUser(Option<UserId>, String, String, bool, bool, DateTime<Utc>),
|
||||||
Session(SessionId),
|
Session(SessionId),
|
||||||
User(UserId),
|
User(UserId),
|
||||||
UserByUsername(String),
|
UserByUsername(String),
|
||||||
@ -58,6 +59,7 @@ pub trait Database: Send + Sync {
|
|||||||
password: &str,
|
password: &str,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
expiration: DateTime<Utc>,
|
||||||
) -> Result<UserId, FatalError>;
|
) -> Result<UserId, FatalError>;
|
||||||
|
|
||||||
async fn games(&self) -> Result<Vec<GameRow>, FatalError>;
|
async fn games(&self) -> Result<Vec<GameRow>, FatalError>;
|
||||||
@ -142,6 +144,7 @@ impl Database for DbConn {
|
|||||||
password: &str,
|
password: &str,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
expiration: DateTime<Utc>,
|
||||||
) -> Result<UserId, FatalError> {
|
) -> Result<UserId, FatalError> {
|
||||||
send_request!(self,
|
send_request!(self,
|
||||||
Request::SaveUser(
|
Request::SaveUser(
|
||||||
@ -150,6 +153,7 @@ impl Database for DbConn {
|
|||||||
password.to_owned(),
|
password.to_owned(),
|
||||||
admin,
|
admin,
|
||||||
enabled,
|
enabled,
|
||||||
|
expiration,
|
||||||
),
|
),
|
||||||
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
||||||
}
|
}
|
||||||
@ -201,7 +205,9 @@ mod test {
|
|||||||
let no_path: Option<PathBuf> = None;
|
let no_path: Option<PathBuf> = None;
|
||||||
let db = DiskDb::new(no_path).unwrap();
|
let db = DiskDb::new(no_path).unwrap();
|
||||||
|
|
||||||
db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true)
|
let now = Utc::now();
|
||||||
|
|
||||||
|
db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true, now)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let game_id = db.save_game(None, &UserId::from("admin"), "Candela", "Circle of the Winter Solstice").unwrap();
|
let game_id = db.save_game(None, &UserId::from("admin"), "Candela", "Circle of the Winter Solstice").unwrap();
|
||||||
(db, game_id)
|
(db, game_id)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use rusqlite::types::{FromSql, FromSqlResult, ValueRef};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -166,6 +167,7 @@ pub struct UserRow {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub password_expires: DateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -196,5 +198,20 @@ pub struct SessionRow {
|
|||||||
user_id: SessionId,
|
user_id: SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DateTime(pub chrono::DateTime<Utc>);
|
||||||
|
|
||||||
|
impl FromSql for DateTime {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
match value {
|
||||||
|
ValueRef::Text(text) => String::from_utf8(text.to_vec())
|
||||||
|
.map_err(|_err| FromSqlError::InvalidType)
|
||||||
|
.and_then(|s| {
|
||||||
|
NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S")
|
||||||
|
.map_err(|_err| FromSqlError::InvalidType)
|
||||||
|
})
|
||||||
|
.and_then(|dt| Ok(DateTime(dt.and_utc()))),
|
||||||
|
_ => Err(FromSqlError::InvalidType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::Core,
|
core::{AuthResponse, Core},
|
||||||
database::{SessionId, UserId},
|
database::{SessionId, UserId},
|
||||||
types::{AppError, FatalError, User, UserProfile},
|
types::{AppError, FatalError, User, UserOverview},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
@ -99,16 +99,20 @@ where
|
|||||||
pub async fn check_password(
|
pub async fn check_password(
|
||||||
core: Core,
|
core: Core,
|
||||||
req: Json<AuthRequest>,
|
req: Json<AuthRequest>,
|
||||||
) -> ResultExt<SessionId, AppError, FatalError> {
|
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||||
let Json(AuthRequest { username, password }) = req;
|
let Json(AuthRequest { username, password }) = req;
|
||||||
core.auth(&username, &password).await
|
unimplemented!()
|
||||||
|
/*
|
||||||
|
match core.auth(&username, &password).await {
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user(
|
pub async fn get_user(
|
||||||
core: Core,
|
core: Core,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
user_id: Option<UserId>,
|
user_id: Option<UserId>,
|
||||||
) -> ResultExt<Option<UserProfile>, 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,
|
||||||
@ -117,6 +121,15 @@ pub async fn get_user(
|
|||||||
}).await
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_users(
|
||||||
|
core: Core,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||||
|
auth_required(core.clone(), headers, |_user| async move {
|
||||||
|
core.list_users().await
|
||||||
|
}).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
core: Core,
|
core: Core,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@ -140,14 +153,3 @@ pub async fn set_password(
|
|||||||
}
|
}
|
||||||
}).await
|
}).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_admin_password(
|
|
||||||
core: Core,
|
|
||||||
req: String,
|
|
||||||
) -> ResultExt<(), AppError, FatalError> {
|
|
||||||
match return_error!(core.user(UserId::from("admin")).await) {
|
|
||||||
Some(admin) if admin.password.is_empty() => core.set_password(UserId::from("admin"), req).await,
|
|
||||||
Some(_) => error(AppError::PermissionDenied),
|
|
||||||
None => fatal(FatalError::DatabaseKeyMissing),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -13,9 +13,8 @@ use crate::{
|
|||||||
core::Core,
|
core::Core,
|
||||||
database::UserId,
|
database::UserId,
|
||||||
handlers::{
|
handlers::{
|
||||||
check_password, create_game, create_user, get_user, healthcheck, set_admin_password,
|
check_password, create_game, create_user, get_user, get_users, healthcheck, set_password,
|
||||||
set_password, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest,
|
wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
|
||||||
SetAdminPasswordRequest, SetPasswordRequest,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,23 +36,6 @@ pub fn routes(core: Core) -> Router {
|
|||||||
.allow_origin(Any),
|
.allow_origin(Any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/v1/admin_password",
|
|
||||||
put({
|
|
||||||
let core = core.clone();
|
|
||||||
move |req: Json<String>| {
|
|
||||||
let Json(req) = req;
|
|
||||||
println!("set admin password: {:?}", req);
|
|
||||||
wrap_handler(|| set_admin_password(core, req))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.layer(
|
|
||||||
CorsLayer::new()
|
|
||||||
.allow_methods([Method::PUT])
|
|
||||||
.allow_headers([CONTENT_TYPE])
|
|
||||||
.allow_origin(Any),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/auth",
|
"/api/v1/auth",
|
||||||
post({
|
post({
|
||||||
@ -88,6 +70,19 @@ pub fn routes(core: Core) -> Router {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/users",
|
||||||
|
get({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap| wrap_handler(|| get_users(core, headers))
|
||||||
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET])
|
||||||
|
.allow_headers([AUTHORIZATION])
|
||||||
|
.allow_origin(Any),
|
||||||
|
),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/user/password",
|
"/api/v1/user/password",
|
||||||
put({
|
put({
|
||||||
@ -132,13 +127,13 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::FsAssets,
|
asset_db::FsAssets,
|
||||||
core::Core,
|
core::{AuthResponse, Core},
|
||||||
database::{Database, DbConn, GameId, SessionId, UserId},
|
database::{Database, DbConn, GameId, SessionId, UserId},
|
||||||
handlers::CreateGameRequest,
|
handlers::CreateGameRequest,
|
||||||
types::UserProfile,
|
types::UserOverview,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn setup_without_admin() -> (Core, TestServer) {
|
fn initialize_test_server() -> (Core, TestServer) {
|
||||||
let memory_db: Option<PathBuf> = None;
|
let memory_db: Option<PathBuf> = None;
|
||||||
let conn = DbConn::new(memory_db);
|
let conn = DbConn::new(memory_db);
|
||||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||||
@ -147,20 +142,26 @@ mod test {
|
|||||||
(core, server)
|
(core, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_admin_enabled() -> (Core, TestServer) {
|
async fn setup_with_admin() -> (Core, TestServer) {
|
||||||
let memory_db: Option<PathBuf> = None;
|
let (core, server) = initialize_test_server();
|
||||||
let conn = DbConn::new(memory_db);
|
core.set_password(UserId::from("admin"), "aoeu".to_owned())
|
||||||
conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
.await;
|
||||||
.await
|
(core, server)
|
||||||
.unwrap();
|
}
|
||||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
|
||||||
let app = routes(core.clone());
|
async fn setup_with_disabled_user() -> (Core, TestServer) {
|
||||||
let server = TestServer::new(app).unwrap();
|
let (core, server) = setup_with_admin().await;
|
||||||
|
let uuid = match core.create_user("shephard").await {
|
||||||
|
ResultExt::Ok(uuid) => uuid,
|
||||||
|
ResultExt::Err(err) => panic!("{}", err),
|
||||||
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
|
};
|
||||||
|
core.disable_user(uuid).await;
|
||||||
(core, server)
|
(core, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_with_user() -> (Core, TestServer) {
|
async fn setup_with_user() -> (Core, TestServer) {
|
||||||
let (core, server) = setup_admin_enabled().await;
|
let (core, server) = setup_with_admin().await;
|
||||||
let response = server
|
let response = server
|
||||||
.post("/api/v1/auth")
|
.post("/api/v1/auth")
|
||||||
.json(&AuthRequest {
|
.json(&AuthRequest {
|
||||||
@ -195,18 +196,7 @@ mod test {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_returns_a_healthcheck() {
|
async fn it_returns_a_healthcheck() {
|
||||||
let (core, server) = setup_without_admin();
|
let (_core, server) = initialize_test_server();
|
||||||
|
|
||||||
let response = server.get("/api/v1/health").await;
|
|
||||||
response.assert_status_ok();
|
|
||||||
let b: crate::handlers::HealthCheck = response.json();
|
|
||||||
assert_eq!(b, crate::handlers::HealthCheck { ok: false });
|
|
||||||
|
|
||||||
assert_matches!(
|
|
||||||
core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
|
||||||
.await,
|
|
||||||
ResultExt::Ok(_)
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = server.get("/api/v1/health").await;
|
let response = server.get("/api/v1/health").await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
@ -214,9 +204,55 @@ mod test {
|
|||||||
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_new_user_has_an_expired_password() {
|
||||||
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.post("/api/v1/auth")
|
||||||
|
.add_header("Content-Type", "application/json")
|
||||||
|
.json(&AuthRequest{ username: "admin".to_owned(), password: "aoeu".to_owned() })
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
let session_id = response.json::<Option<AuthResponse>>().unwrap();
|
||||||
|
|
||||||
|
let session_id = match session_id {
|
||||||
|
AuthResponse::Success(session_id) => session_id,
|
||||||
|
AuthResponse::Expired(_) => panic!("admin user is already expired"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.put("/api/v1/user")
|
||||||
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
|
.json("savanni")
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.post("/api/v1/auth")
|
||||||
|
.add_header("Content-Type", "application/json")
|
||||||
|
.json(&AuthRequest{ username: "savanni".to_owned(), password: "".to_owned() })
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
let session = response.json::<Option<AuthResponse>>().unwrap();
|
||||||
|
assert_matches!(session, AuthResponse::Expired(_));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_refuses_to_authenticate_a_disabled_user() {
|
||||||
|
let (_core, server) = setup_with_disabled_user().await;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_forces_changing_expired_password() {
|
||||||
|
let (_core, server) = setup_with_user().await;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_authenticates_a_user() {
|
async fn it_authenticates_a_user() {
|
||||||
let (_core, server) = setup_admin_enabled().await;
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
let response = server
|
let response = server
|
||||||
.post("/api/v1/auth")
|
.post("/api/v1/auth")
|
||||||
@ -250,7 +286,7 @@ mod test {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_returns_user_profile() {
|
async fn it_returns_user_profile() {
|
||||||
let (_core, server) = setup_admin_enabled().await;
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
let response = server.get("/api/v1/user").await;
|
let response = server.get("/api/v1/user").await;
|
||||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
@ -271,19 +307,12 @@ mod test {
|
|||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.id, UserId::from("admin"));
|
assert_eq!(profile.id, UserId::from("admin"));
|
||||||
assert_eq!(profile.name, "admin");
|
assert_eq!(profile.name, "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn an_admin_can_create_a_user() {
|
|
||||||
// All of the contents of this test are basically required for any test on individual
|
|
||||||
// users, so I moved it all into the setup code.
|
|
||||||
let (_core, _server) = setup_with_user().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_user_can_get_any_user_profile() {
|
async fn a_user_can_get_any_user_profile() {
|
||||||
let (core, server) = setup_with_user().await;
|
let (core, server) = setup_with_user().await;
|
||||||
@ -310,7 +339,7 @@ mod test {
|
|||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.name, "savanni");
|
assert_eq!(profile.name, "savanni");
|
||||||
|
|
||||||
@ -319,7 +348,7 @@ mod test {
|
|||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.name, "admin");
|
assert_eq!(profile.name, "admin");
|
||||||
}
|
}
|
||||||
@ -342,7 +371,7 @@ mod test {
|
|||||||
.get("/api/v1/user")
|
.get("/api/v1/user")
|
||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
let profile = response.json::<Option<UserProfile>>().unwrap();
|
let profile = response.json::<Option<UserOverview>>().unwrap();
|
||||||
assert_eq!(profile.name, "savanni");
|
assert_eq!(profile.name, "savanni");
|
||||||
|
|
||||||
let response = server
|
let response = server
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -79,6 +80,7 @@ pub struct User {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub expiration: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UserRow> for User {
|
impl From<UserRow> for User {
|
||||||
@ -89,6 +91,7 @@ impl From<UserRow> for User {
|
|||||||
password: row.password.to_owned(),
|
password: row.password.to_owned(),
|
||||||
admin: row.admin,
|
admin: row.admin,
|
||||||
enabled: row.enabled,
|
enabled: row.enabled,
|
||||||
|
expiration: row.password_expires.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,12 +138,11 @@ pub enum Message {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct UserProfile {
|
pub struct UserOverview {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub password: String,
|
|
||||||
pub games: Vec<GameOverview>,
|
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
|
pub games: Vec<GameId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
|
@ -48,10 +48,13 @@ export class Client {
|
|||||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async users() {
|
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).then((response) => response.json());
|
return fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: [['Authorization', `Bearer ${sessionId}`]]
|
||||||
|
}).then((response) => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
async charsheet(id: string) {
|
async charsheet(id: string) {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
.profile {
|
||||||
|
margin: var(--margin-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile_columns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile_columns > div {
|
||||||
|
width: 45%;
|
||||||
|
}
|
@ -1,30 +1,28 @@
|
|||||||
import { UserProfile } from 'visions-types';
|
import { UserProfile } from 'visions-types';
|
||||||
import { CardElement } from '../Card/Card';
|
import { CardElement, GameOverviewElement, UserManagementElement } from '..';
|
||||||
import { GameOverviewElement } from '../GameOverview/GameOverview';
|
import './Profile.css';
|
||||||
|
|
||||||
export const ProfileElement = ({ name, games, is_admin }: UserProfile) => {
|
interface ProfileProps {
|
||||||
const adminNote = is_admin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
|
profile: UserProfile,
|
||||||
|
users: UserProfile[],
|
||||||
|
}
|
||||||
|
|
||||||
return (<div>
|
export const ProfileElement = ({ profile, users }: ProfileProps) => {
|
||||||
<CardElement name={name}>
|
const adminNote = profile.is_admin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
|
||||||
<div>Games: {games.map((game) => {
|
|
||||||
|
return (<div className="profile">
|
||||||
|
<CardElement name={profile.name}>
|
||||||
|
<div>Games: {profile.games.map((game) => {
|
||||||
return <span key={game.id}>{game.game_name} ({game.game_type})</span>;
|
return <span key={game.id}>{game.game_name} ({game.game_type})</span>;
|
||||||
}) }</div>
|
}) }</div>
|
||||||
{adminNote}
|
{adminNote}
|
||||||
</CardElement>
|
</CardElement>
|
||||||
|
|
||||||
<div>
|
<div className="profile_columns">
|
||||||
<CardElement>
|
<UserManagementElement users={users} />
|
||||||
<ul>
|
|
||||||
<li> Savanni </li>
|
|
||||||
<li> Shephard </li>
|
|
||||||
<li> Vakarian </li>
|
|
||||||
<li> vas Normandy </li>
|
|
||||||
</ul>
|
|
||||||
</CardElement>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{games.map((game) => <GameOverviewElement {...game} />)}
|
{profile.games.map((game) => <GameOverviewElement {...game} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
16
visions/ui/src/components/UserManagement/UserManagement.tsx
Normal file
16
visions/ui/src/components/UserManagement/UserManagement.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { UserProfile } from "visions-types"
|
||||||
|
import { CardElement } from ".."
|
||||||
|
|
||||||
|
interface UserManagementProps {
|
||||||
|
users: UserProfile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserManagementElement = ({ users }: UserManagementProps ) => {
|
||||||
|
return (
|
||||||
|
<CardElement>
|
||||||
|
<ul>
|
||||||
|
{users.map((user) => <li key={user.id}>{user.name}</li>)}
|
||||||
|
</ul>
|
||||||
|
</CardElement>
|
||||||
|
)
|
||||||
|
}
|
@ -4,5 +4,6 @@ import { ProfileElement } from './Profile/Profile'
|
|||||||
import { SimpleGuage } from './Guages/SimpleGuage'
|
import { SimpleGuage } from './Guages/SimpleGuage'
|
||||||
import { ThumbnailElement } from './Thumbnail/Thumbnail'
|
import { ThumbnailElement } from './Thumbnail/Thumbnail'
|
||||||
import { TabletopElement } from './Tabletop/Tabletop'
|
import { TabletopElement } from './Tabletop/Tabletop'
|
||||||
|
import { UserManagementElement } from './UserManagement/UserManagement'
|
||||||
|
|
||||||
export { CardElement, ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }
|
export { CardElement, GameOverviewElement, UserManagementElement, ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }
|
||||||
|
@ -32,7 +32,7 @@ export const Admin = ({ client }: AdminProps) => {
|
|||||||
const [users, setUsers] = useState<Array<User>>([]);
|
const [users, setUsers] = useState<Array<User>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.users().then((u) => {
|
client.users("aoeu").then((u) => {
|
||||||
console.log(u);
|
console.log(u);
|
||||||
setUsers(u);
|
setUsers(u);
|
||||||
});
|
});
|
||||||
|
@ -11,18 +11,19 @@ interface MainProps {
|
|||||||
export const MainView = ({ client }: MainProps) => {
|
export const MainView = ({ client }: MainProps) => {
|
||||||
const [state, _manager] = useContext(StateContext)
|
const [state, _manager] = useContext(StateContext)
|
||||||
const [profile, setProfile] = useState<UserProfile | undefined>(undefined)
|
const [profile, setProfile] = useState<UserProfile | undefined>(undefined)
|
||||||
|
const [users, setUsers] = useState<UserProfile[]>([])
|
||||||
|
|
||||||
const sessionId = getSessionId(state)
|
const sessionId = getSessionId(state)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}, [sessionId, client])
|
}, [sessionId, client])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Session ID: {sessionId}</div>
|
{profile && <ProfileElement profile={profile} users={[]} />}
|
||||||
{profile && <ProfileElement {...profile} />}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
8
visions/visions-types/package-lock.json
generated
8
visions/visions-types/package-lock.json
generated
@ -9,13 +9,13 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -9,6 +9,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user