Check username and password

This commit is contained in:
Savanni D'Gerinel 2024-12-22 09:17:00 -05:00
parent 2a616ef6c9
commit e62ff9aa7a
10 changed files with 204 additions and 17 deletions

59
Cargo.lock generated
View File

@ -932,6 +932,19 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -1882,6 +1895,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.30"
@ -2049,6 +2068,17 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is-terminal"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
dependencies = [
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -2824,6 +2854,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@ -3813,6 +3853,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.64"
@ -4294,6 +4343,7 @@ dependencies = [
"lazy_static",
"mime",
"mime_guess",
"pretty_env_logger",
"result-extended",
"rusqlite",
"rusqlite_migration",
@ -4476,6 +4526,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@ -15,6 +15,7 @@ include_dir = { version = "0.7.4" }
lazy_static = { version = "1.5.0" }
mime = { version = "0.3.17" }
mime_guess = { version = "2.0.5" }
pretty_env_logger = "0.5.0"
result-extended = { path = "../../result-extended" }
rusqlite = { version = "0.32.1" }
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }

View File

@ -220,6 +220,15 @@ impl Core {
Err(err) => fatal(err),
}
}
pub async fn auth(&self, username: String, password: String) -> ResultExt<UserId, AppError, FatalError> {
let state = self.0.write().await;
match state.db.user_by_username(username).await {
Ok(Some(row)) if (row.password == password) => ok(row.id),
Ok(_) => error(AppError::AuthFailed),
Err(err) => fatal(err),
}
}
}
#[cfg(test)]

View File

@ -26,6 +26,7 @@ enum Request {
Charsheet(CharacterId),
Games,
User(UserId),
UserByUsername(String),
Users,
SaveUser(Option<UserId>, String, String, bool, bool),
}
@ -180,6 +181,8 @@ pub struct CharsheetRow {
pub trait Database: Send + Sync {
async fn user(&mut self, _: UserId) -> Result<Option<UserRow>, FatalError>;
async fn user_by_username(&self, _: String) -> Result<Option<UserRow>, FatalError>;
async fn save_user(
&mut self,
user_id: Option<UserId>,
@ -290,6 +293,31 @@ impl DiskDb {
}
}
fn user_by_username(&self, username: String) -> Result<Option<UserRow>, FatalError> {
let mut stmt = self
.conn
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?")
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
let items: Vec<UserRow> = stmt
.query_map([username.as_str()], |row| {
Ok(UserRow {
id: row.get(0).unwrap(),
name: row.get(1).unwrap(),
password: row.get(2).unwrap(),
admin: row.get(3).unwrap(),
enabled: row.get(4).unwrap(),
})
})
.unwrap()
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
.unwrap();
match &items[..] {
[] => Ok(None),
[item] => Ok(Some(item.clone())),
_ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())),
}
}
fn users(&self) -> Result<Vec<UserRow>, FatalError> {
let mut stmt = self
.conn
@ -448,6 +476,13 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
err => panic!("{:?}", err),
}
}
Request::UserByUsername(username) => {
let user = db.user_by_username(username);
match user {
Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(),
err => panic!("{:?}", err),
}
}
Request::SaveUser(user_id, username, password, admin, enabled) => {
let user_id = db.save_user(user_id, username.as_ref(), password.as_ref(), admin, enabled);
match user_id {
@ -514,6 +549,26 @@ impl Database for DbConn {
}
}
async fn user_by_username(&self, username: String) -> Result<Option<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest {
tx,
req: Request::UserByUsername(username),
};
match self.conn.send(request).await {
Ok(()) => (),
Err(_) => return Err(FatalError::DatabaseConnectionLost),
};
match rx.recv().await {
Ok(DatabaseResponse::User(user)) => Ok(user),
Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => Err(FatalError::DatabaseConnectionLost),
}
}
async fn save_user(
&mut self,
user_id: Option<UserId>,

View File

@ -3,6 +3,7 @@ use std::future::Future;
use futures::{SinkExt, StreamExt};
use result_extended::{error, ok, return_error, ResultExt};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
use crate::{
@ -255,3 +256,24 @@ pub async fn handle_set_admin_password(core: Core, password: String) -> impl Rep
})
.await
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare]
pub struct AuthRequest {
username: String,
password: String,
}
pub async fn handle_auth(core: Core, auth_request: AuthRequest) -> impl Reply {
handler(async move {
let userid = return_error!(core.auth(auth_request.username, auth_request.password).await);
ok(Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "*")
.header("Access-Control-Allow-Headers", "content-type")
.header("Content-Type", "application/json")
.body(serde_json::to_vec(&userid).unwrap())
.unwrap())
}).await
}

View File

@ -8,10 +8,14 @@ use asset_db::{AssetId, FsAssets};
use authdb::AuthError;
use database::DbConn;
use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest
handle_auth, handle_available_images, handle_connect_websocket, handle_file,
handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status,
handle_set_admin_password, handle_set_background_image, handle_unregister_client,
RegisterRequest,
};
use warp::{
// header,
filters::{method, path},
http::{Response, StatusCode},
reply::Reply,
Filter,
@ -99,17 +103,16 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
#[tokio::main]
pub async fn main() {
pretty_env_logger::init();
let conn = DbConn::new(Some("/home/savanni/game.db"));
let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
let log = warp::log("visions::api");
let route_server_status = warp::path!("api" / "v1" / "status")
.and(warp::get())
.then({
let core = core.clone();
move || handle_server_status(core.clone())
});
let route_server_status = warp::path!("api" / "v1" / "status").and(warp::get()).then({
let core = core.clone();
move || handle_server_status(core.clone())
});
let route_image = warp::path!("api" / "v1" / "image" / String)
.and(warp::get())
@ -164,16 +167,13 @@ pub async fn main() {
.then({
let core = core.clone();
move |body| handle_set_background_image(core.clone(), body)
})
.with(log);
let route_get_users = warp::path!("api" / "v1" / "users")
.and(warp::get())
.then({
let core = core.clone();
move || handle_get_users(core.clone())
});
let route_get_users = warp::path!("api" / "v1" / "users").and(warp::get()).then({
let core = core.clone();
move || handle_get_users(core.clone())
});
let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String)
.and(warp::get())
.then({
@ -202,6 +202,26 @@ pub async fn main() {
move |body| handle_set_admin_password(core.clone(), body)
});
let route_check_password_options = warp::path!("api" / "v1" / "auth")
.and(warp::options())
.map({
move || {
Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "PUT")
.header("Access-Control-Allow-Headers", "content-type")
.body("")
.unwrap()
}
});
let route_check_password = warp::path!("api" / "v1" / "auth")
.and(warp::put())
.and(warp::body::json())
.then({
let core = core.clone();
move |body| handle_auth(core.clone(), body)
});
let filter = route_server_status
.or(route_register_client)
.or(route_unregister_client)
@ -214,6 +234,9 @@ pub async fn main() {
.or(route_get_charsheet)
.or(route_set_admin_password_options)
.or(route_set_admin_password)
.or(route_check_password_options)
.or(route_check_password)
.with(warp::log("visions"))
.recover(handle_rejection);
let server = warp::serve(filter);

View File

@ -36,6 +36,9 @@ pub enum AppError {
#[error("the requested operation is not allowed")]
PermissionDenied,
#[error("the requested username/password combination was not found")]
AuthFailed,
#[error("invalid json {0}")]
JsonError(serde_json::Error),

View File

@ -36,7 +36,7 @@ const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) =>
return (
<Authentication onAdminPassword={(password) => {
manager.setAdminPassword(password);
}} onAuth={(username, password) => console.log(username, password)}>
}} onAuth={(username, password) => manager.auth(username, password)}>
{children}
</Authentication>
);

View File

@ -64,6 +64,12 @@ export class Client {
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
}
async auth(username: string, password: string) {
const url = new URL(this.base);
url.pathname = `api/v1/auth`
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ 'username': username, 'password': password }) });
}
async status() {
const url = new URL(this.base);
url.pathname = `/api/v1/status`;

View File

@ -49,6 +49,15 @@ class StateManager {
await this.client.setAdminPassword(password);
await this.status();
}
async auth(username: string, password: string) {
if (!this.client || !this.dispatch) return;
let resp = await this.client.auth(username, password);
let userid = await resp.json();
console.log("userid retrieved", userid);
this.dispatch({ type: "SetAuthState", content: { type: "Authed", userid } });
}
}
export const StateContext = createContext<[AppState, StateManager]>([initialState(), new StateManager(undefined, undefined)]);