feat: got sqlx working

This commit is contained in:
Andrew Rioux 2025-01-24 11:48:15 -05:00
parent 6e99dc3d70
commit 8695685eb3
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
19 changed files with 1071 additions and 49 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
zig-out zig-out
.zig-cache .zig-cache
freebsd.txt freebsd.txt
/sparse-server/db.sqlite*

746
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -71,8 +71,9 @@
dart-sass dart-sass
binaryen binaryen
]; ];
freebsd = [ pkgsCross.x86_64-freebsd.buildPackages.clang ]; freebsd = buildTools.linux
windows = [ ++ [ pkgsCross.x86_64-freebsd.buildPackages.clang ];
windows = buildTools.linux ++ [
pkgsCross.mingwW64.stdenv.cc pkgsCross.mingwW64.stdenv.cc
pkgsCross.mingwW64.windows.pthreads pkgsCross.mingwW64.windows.pthreads
]; ];
@ -88,6 +89,9 @@
# Cargo lint tools # Cargo lint tools
taplo taplo
cargo-deny cargo-deny
# Web server tools
sqlx-cli
]; ];
craneLib = (crane.mkLib pkgs).overrideToolchain (p: craneLib = (crane.mkLib pkgs).overrideToolchain (p:
@ -143,7 +147,7 @@
default = craneLib.devShell (buildEnvironment // { default = craneLib.devShell (buildEnvironment // {
name = "sparse-default"; name = "sparse-default";
packages = buildTools.all ++ devShellTools packages = buildTools.linux ++ devShellTools
++ [ setup-zig-freebsd setup-dev-environment ]; ++ [ setup-zig-freebsd setup-dev-environment ];
# Added to make development easier # Added to make development easier
@ -157,6 +161,27 @@
"../../target/x86_64-unknown-freebsd/debug/sparse-unix-beacon"; "../../target/x86_64-unknown-freebsd/debug/sparse-unix-beacon";
SPARSE_BEACON_WINDOWS = SPARSE_BEACON_WINDOWS =
"../../target/x86_64-pc-windows-gnu/debug/sparse-windows-beacon.exe"; "../../target/x86_64-pc-windows-gnu/debug/sparse-windows-beacon.exe";
shellHook = ''
export DATABASE_URL="sqlite://$(${pkgs.git}/bin/git rev-parse --show-toplevel)/sparse-server/db.sqlite"
'';
});
windows = craneLib.devShell (buildEnvironment // {
name = "sparse-default";
packages = buildTools.windows ++ devShellTools
++ [ setup-zig-freebsd setup-dev-environment ];
# No point adding above environment variables, since web server can't
# be built with windows tools available
});
freebsd = craneLib.devShell (buildEnvironment // {
name = "sparse-default";
packages = buildTools.freebsd ++ devShellTools
++ [ setup-zig-freebsd setup-dev-environment ];
}); });
}; };
}); });

View File

@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT user_id, user_name FROM users;",
"describe": {
"columns": [
{
"name": "user_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "user_name",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "86f197e514e8d55b95a71ab52b5901e939ee2c9e832ed1fae2661ad770d3ad60"
}

View File

@ -28,8 +28,14 @@ tokio-stream = { version = "0.1", optional = true }
futures-util = { version = "0.3", optional = true } futures-util = { version = "0.3", optional = true }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true }
web-sys = { version = "0.3", features = ["WebSocket"] } web-sys = { version = "0.3", features = ["WebSocket"] }
leptos-use = { version = "0.15", default_features = false, features = ["use_websocket"] } leptos-use = { version = "0.15", default-features = false, features = ["use_websocket"] }
codee = "0.2" codee = "0.2"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
chrono = "0.4"
rpassword = { version = "7.3", optional = true }
pbkdf2 = { version = "0.12", features = ["simple", "sha2"], optional = true }
sha2 = { version = "0.10", optional = true }
hex = { version = "0.4", optional = true }
[features] [features]
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
@ -45,6 +51,11 @@ ssr = [
"dep:tokio-stream", "dep:tokio-stream",
"dep:futures-util", "dep:futures-util",
"dep:tracing", "dep:tracing",
"dep:sqlx",
"dep:rpassword",
"dep:pbkdf2",
"dep:sha2",
"dep:hex",
"leptos/ssr", "leptos/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",

View File

@ -1,3 +1,6 @@
// generated by `sqlx migrate build-script`
fn main() { fn main() {
// trigger recompilation when a new migration is added
include!("../build_common.rs"); include!("../build_common.rs");
println!("cargo:rerun-if-changed=migrations");
} }

View File

@ -0,0 +1,16 @@
-- Add migration script here
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_name varchar(255) NOT NULL,
password_salt char(64) NOT NULL,
password_hash char(64) NOT NULL
);
CREATE TABLE sessions (
session_id char(64) NOT NULL,
user_id int NOT NULL,
expires int NOT NULL,
PRIMARY KEY (session_id),
FOREIGN KEY (user_id) REFERENCES users
);

View File

@ -53,6 +53,7 @@ pub fn App() -> impl IntoView {
<main> <main>
<Routes fallback=|| "Page not found.".into_view()> <Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/> <Route path=StaticSegment("") view=HomePage/>
<Route path=StaticSegment("/users") view=crate::users::UserView/>
</Routes> </Routes>
</main> </main>
</Router> </Router>

View File

@ -1,9 +1,22 @@
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
pub mod user;
#[derive(StructOpt, Debug, Clone)] #[derive(StructOpt, Debug, Clone)]
#[structopt(name = "sparse-server")] #[structopt(name = "sparse-server")]
pub struct Options { pub struct Options {
/// The location of the database to use. If not present,
/// sparse will read the DATABASE_URL environment variable
#[structopt(short = "d")]
pub db_location: Option<PathBuf>,
/// During the startup of the program, sparse will produce an error
/// if the database doesn't exist. Use this flag to instead create a new
/// database
#[structopt(short = "i")]
pub init_ok: bool,
#[structopt(subcommand)] #[structopt(subcommand)]
pub command: Option<Command>, pub command: Option<Command>,
} }
@ -11,6 +24,34 @@ pub struct Options {
#[derive(StructOpt, Debug, Clone)] #[derive(StructOpt, Debug, Clone)]
#[structopt()] #[structopt()]
pub enum Command { pub enum Command {
/// Run the web and API server
Serve {}, Serve {},
Init {},
/// Extract the public key and print it to standard out
ExtractPubKey {},
/// Perform user management
User {
#[structopt(subcommand)]
command: UserCommand,
},
}
#[derive(StructOpt, Debug, Clone)]
#[structopt()]
pub enum UserCommand {
/// List available users to sign in as
List {},
/// Create a new user
Create {
#[structopt(short = "n")]
user_name: String,
},
/// Reset the password for a user who forgot their password
ResetPassword {
#[structopt(short = "I")]
user_id: i16,
},
} }

View File

@ -0,0 +1,75 @@
use std::process::ExitCode;
use futures_util::StreamExt;
use sqlx::{Database, query, sqlite::SqlitePool};
use crate::cli::UserCommand as UC;
pub async fn handle_user_command(user_command: UC, db: SqlitePool) -> anyhow::Result<ExitCode> {
match user_command {
UC::List {} => list_users(db).await,
UC::Create { user_name } => create_user(db, user_name).await,
UC::ResetPassword { user_id } => reset_password(&db, user_id).await
}
}
async fn list_users(db: SqlitePool) -> anyhow::Result<ExitCode> {
println!("\n\nListing users:");
let mut results = query!("SELECT user_id, user_name FROM users;").fetch(&db);
while let Some(Ok(user)) = results.next().await {
println!("\t{:?}: {}", user.user_id, user.user_name);
}
Ok(ExitCode::SUCCESS)
}
async fn create_user(db: SqlitePool, name: String) -> anyhow::Result<ExitCode> {
let mut tx = db.begin().await?;
let previous_user_check = sqlx::query_scalar!(
"SELECT COUNT(*) FROM users WHERE user_name = ?",
name
)
.fetch_one(&mut *tx)
.await?;
if previous_user_check > 0 {
eprintln!("Error! User already exists!");
return Ok(ExitCode::FAILURE);
}
let new_id = query!(
r#"INSERT INTO users (user_name, password_salt, password_hash) VALUES (?, "", "")"#,
name
)
.execute(&mut *tx)
.await?
.last_insert_rowid();
reset_password(&mut *tx, new_id as i16).await?;
tx.commit().await?;
println!("User successfully created!");
Ok(ExitCode::SUCCESS)
}
async fn reset_password<'a, E>(db: E, id: i16) -> anyhow::Result<ExitCode>
where
E: sqlx::Executor<'a, Database = sqlx::Sqlite>
{
let password1 = rpassword::prompt_password("Enter new password: ")?;
let password2 = rpassword::prompt_password("Enter password again: ")?;
if password1 != password2 {
Err(anyhow::anyhow!("Passwords do not match!"))?
}
crate::db::user::reset_password(db, id, password1).await?;
println!("Password set successfully!");
Ok(ExitCode::SUCCESS)
}

View File

@ -1,2 +1,15 @@
mod beacons; #[cfg(feature = "ssr")]
mod users; pub mod user;
pub struct User {
pub user_id: i16,
pub user_name: String,
pub password_salt: String,
pub password_hash: String,
}
pub struct Sessions {
pub session_id: String,
pub user_id: i16,
pub expires: chrono::DateTime<chrono::offset::Local>,
}

View File

@ -1 +0,0 @@

View File

@ -0,0 +1,35 @@
use sqlx::{sqlite::SqlitePool, Database};
use pbkdf2::{pbkdf2_hmac_array, password_hash::{rand_core::OsRng, SaltString}};
use sha2::Sha256;
const PASSWORD_ITERATIONS: u32 = 100_000;
pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> anyhow::Result<()>
where
E: sqlx::Executor<'a, Database = sqlx::Sqlite>
{
let salt = SaltString::generate(&mut OsRng);
let key = pbkdf2_hmac_array::<Sha256, 20>(
password.as_bytes(),
salt.as_str().as_bytes(),
PASSWORD_ITERATIONS
);
let salt_string = hex::encode(salt.as_str().as_bytes());
let password_string = hex::encode(&key[..]);
tracing::debug!("New password hash: {password_string}");
tracing::debug!("New password salt: {salt_string}");
sqlx::query!(
"UPDATE users SET password_hash = ?, password_salt = ? WHERE user_id = ?",
password_string,
salt_string,
id
)
.execute(pool)
.await?;
Ok(())
}

View File

@ -1 +0,0 @@

View File

@ -1,5 +1,7 @@
pub mod app; pub mod app;
pub mod users;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() { pub fn hydrate() {

View File

@ -15,28 +15,82 @@ mod cli;
mod webserver; mod webserver;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod db; pub mod users;
pub mod db;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<std::process::ExitCode> {
use std::{path::PathBuf, process::ExitCode, str::FromStr};
use structopt::StructOpt; use structopt::StructOpt;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use sqlx::sqlite::{SqlitePool, SqliteConnectOptions};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let options = cli::Options::from_args(); let options = cli::Options::from_args();
let db_location = options.db_location.clone()
.or(std::env::var("DATABASE_URL")
.map(|p| p.replace("sqlite://", ""))
.map(PathBuf::from)
.ok())
.unwrap_or(PathBuf::from("./db.sqlite"));
tracing::info!("Using database {}", db_location.display());
let db_exists = std::fs::metadata(&db_location);
let run_init = if let Err(e) = db_exists {
if !options.init_ok {
tracing::error!("Database doesn't exist, and initialization not allowed!");
tracing::error!("{:?}", e);
return Ok(ExitCode::FAILURE)
}
tracing::info!("Database doesn't exist, readying initialization");
true
} else {
false
};
let pool = SqlitePool::connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))?
.create_if_missing(options.init_ok)
).await?;
tracing::info!("Running database migrations...");
sqlx::migrate!()
.run(&pool)
.await?;
tracing::info!("Done running database migrations!");
match options.command.clone() { match options.command.clone() {
Some(cli::Command::Serve { }) => { Some(cli::Command::Serve { }) => {
webserver::serve_web(options).await?; tracing::info!("Performing requested action, acting as web server");
webserver::serve_web(options, pool).await
} }
Some(cli::Command::Init { }) => { Some(cli::Command::ExtractPubKey { }) => {
Ok(ExitCode::SUCCESS)
}
Some(cli::Command::User { command }) => {
cli::user::handle_user_command(command, pool).await
} }
None => { None => {
webserver::serve_web(options).await?; tracing::info!("Performing default action of acting as web server");
webserver::serve_web(options, pool).await
} }
} }
Ok(())
} }
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]

View File

@ -0,0 +1,8 @@
use leptos::prelude::*;
#[component]
pub fn UserView() -> impl IntoView {
view! {
<p>"User view"</p>
}
}

View File

@ -1,3 +1,13 @@
use std::process::ExitCode;
use sqlx::sqlite::SqlitePool;
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use sparse_server::app::*;
pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response { pub async fn websocket(ws: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response {
ws.on_upgrade(handle_websocket) ws.on_upgrade(handle_websocket)
} }
@ -32,22 +42,7 @@ async fn handle_websocket(mut socket: axum::extract::ws::WebSocket) {
} }
} }
pub async fn serve_web(options: crate::cli::Options) -> anyhow::Result<()> { pub async fn serve_web(options: crate::cli::Options, db: SqlitePool) -> anyhow::Result<ExitCode> {
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use sparse_server::app::*;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let conf = get_configuration(None).unwrap(); let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr; let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options; let leptos_options = conf.leptos_options;
@ -69,5 +64,5 @@ pub async fn serve_web(options: crate::cli::Options) -> anyhow::Result<()> {
let listener = tokio::net::TcpListener::bind(&addr).await?; let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service()).await?; axum::serve(listener, app.into_make_service()).await?;
Ok(()) Ok(ExitCode::SUCCESS)
} }

View File

@ -28,7 +28,9 @@ typedef struct Parameters {
unsigned short privkey_size; unsigned short privkey_size;
unsigned short privkey_cert_size; unsigned short privkey_cert_size;
unsigned short beacon_name_length; unsigned short beacon_name_length;
unsigned short domain_name_length;
char pubkey_cert[1024]; char pubkey_cert[1024];
char beacon_identifier[64]; char beacon_identifier[64];
char beacon_name[128]; char beacon_name[128];
char domain_name[128];
} Parameters_t; } Parameters_t;