feat: got sqlx working
This commit is contained in:
parent
6e99dc3d70
commit
8695685eb3
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@
|
||||
zig-out
|
||||
.zig-cache
|
||||
freebsd.txt
|
||||
/sparse-server/db.sqlite*
|
||||
|
||||
746
Cargo.lock
generated
746
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
flake.nix
31
flake.nix
@ -71,8 +71,9 @@
|
||||
dart-sass
|
||||
binaryen
|
||||
];
|
||||
freebsd = [ pkgsCross.x86_64-freebsd.buildPackages.clang ];
|
||||
windows = [
|
||||
freebsd = buildTools.linux
|
||||
++ [ pkgsCross.x86_64-freebsd.buildPackages.clang ];
|
||||
windows = buildTools.linux ++ [
|
||||
pkgsCross.mingwW64.stdenv.cc
|
||||
pkgsCross.mingwW64.windows.pthreads
|
||||
];
|
||||
@ -88,6 +89,9 @@
|
||||
# Cargo lint tools
|
||||
taplo
|
||||
cargo-deny
|
||||
|
||||
# Web server tools
|
||||
sqlx-cli
|
||||
];
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain (p:
|
||||
@ -143,7 +147,7 @@
|
||||
default = craneLib.devShell (buildEnvironment // {
|
||||
name = "sparse-default";
|
||||
|
||||
packages = buildTools.all ++ devShellTools
|
||||
packages = buildTools.linux ++ devShellTools
|
||||
++ [ setup-zig-freebsd setup-dev-environment ];
|
||||
|
||||
# Added to make development easier
|
||||
@ -157,6 +161,27 @@
|
||||
"../../target/x86_64-unknown-freebsd/debug/sparse-unix-beacon";
|
||||
SPARSE_BEACON_WINDOWS =
|
||||
"../../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 ];
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
26
sparse-server/.sqlx/query-86f197e514e8d55b95a71ab52b5901e939ee2c9e832ed1fae2661ad770d3ad60.json
generated
Normal file
26
sparse-server/.sqlx/query-86f197e514e8d55b95a71ab52b5901e939ee2c9e832ed1fae2661ad770d3ad60.json
generated
Normal 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"
|
||||
}
|
||||
@ -28,8 +28,14 @@ tokio-stream = { version = "0.1", optional = true }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
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"
|
||||
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]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@ -45,6 +51,11 @@ ssr = [
|
||||
"dep:tokio-stream",
|
||||
"dep:futures-util",
|
||||
"dep:tracing",
|
||||
"dep:sqlx",
|
||||
"dep:rpassword",
|
||||
"dep:pbkdf2",
|
||||
"dep:sha2",
|
||||
"dep:hex",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// generated by `sqlx migrate build-script`
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
include!("../build_common.rs");
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
|
||||
16
sparse-server/migrations/20250124053720_users.sql
Normal file
16
sparse-server/migrations/20250124053720_users.sql
Normal 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
|
||||
);
|
||||
@ -53,6 +53,7 @@ pub fn App() -> impl IntoView {
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage/>
|
||||
<Route path=StaticSegment("/users") view=crate::users::UserView/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
pub mod user;
|
||||
|
||||
#[derive(StructOpt, Debug, Clone)]
|
||||
#[structopt(name = "sparse-server")]
|
||||
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)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
@ -11,6 +24,34 @@ pub struct Options {
|
||||
#[derive(StructOpt, Debug, Clone)]
|
||||
#[structopt()]
|
||||
pub enum Command {
|
||||
/// Run the web and API server
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
75
sparse-server/src/cli/user.rs
Normal file
75
sparse-server/src/cli/user.rs
Normal 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)
|
||||
}
|
||||
@ -1,2 +1,15 @@
|
||||
mod beacons;
|
||||
mod users;
|
||||
#[cfg(feature = "ssr")]
|
||||
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>,
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
35
sparse-server/src/db/user.rs
Normal file
35
sparse-server/src/db/user.rs
Normal 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(())
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
pub mod app;
|
||||
|
||||
pub mod users;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
|
||||
@ -15,28 +15,82 @@ mod cli;
|
||||
mod webserver;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod db;
|
||||
pub mod users;
|
||||
|
||||
pub mod db;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[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 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 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() {
|
||||
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 => {
|
||||
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"))]
|
||||
|
||||
8
sparse-server/src/users.rs
Normal file
8
sparse-server/src/users.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn UserView() -> impl IntoView {
|
||||
view! {
|
||||
<p>"User view"</p>
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
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<()> {
|
||||
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();
|
||||
|
||||
pub async fn serve_web(options: crate::cli::Options, db: SqlitePool) -> anyhow::Result<ExitCode> {
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
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?;
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
@ -28,7 +28,9 @@ typedef struct Parameters {
|
||||
unsigned short privkey_size;
|
||||
unsigned short privkey_cert_size;
|
||||
unsigned short beacon_name_length;
|
||||
unsigned short domain_name_length;
|
||||
char pubkey_cert[1024];
|
||||
char beacon_identifier[64];
|
||||
char beacon_name[128];
|
||||
char domain_name[128];
|
||||
} Parameters_t;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user