add: schema, add user but no middleware

This commit is contained in:
2025-05-11 20:16:42 -07:00
parent a68ffc4bb7
commit 66d8144a03
8 changed files with 198 additions and 10 deletions

View File

@@ -17,6 +17,13 @@ tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
wasm-bindgen = { version = "=0.2.100", optional = true }
serde = { version = "1.0.219", features = ["derive"] }
tokio-postgres = { version = "0.7.13", optional = true }
clap = { version = "4.5.37", features = ["derive"], optional = true }
anyhow = { version = "1.0.98", optional = true }
log = { version = "0.4.27", optional = true }
env_logger = { version = "0.11.8", optional = true }
rand = { version = "0.9.1", optional = true}
[features]
hydrate = [
"leptos/hydrate",
@@ -27,10 +34,22 @@ ssr = [
"dep:axum",
"dep:tokio",
"dep:leptos_axum",
"dep:tokio-postgres",
"dep:clap",
"dep:anyhow",
"dep:log",
"dep:env_logger",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
env_logger = ["dep:env_logger"]
log = ["dep:log"]
anyhow = ["dep:anyhow"]
clap = ["dep:clap"]
tokio-postgres = ["dep:tokio-postgres"]
rand = ["dep:rand"]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]

29
schema.sql Normal file
View File

@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS Users (
-- for foreign key relations
user_id SERIAL PRIMARY KEY,
-- Public key posted next to display name; never changes
pub VARCHAR(255),
-- Private key used by user to login
priv VARCHAR(4096),
display_name VARCHAR(255)
);
CREATE TABLE IF NOT EXISTS UserRooms (
user_id INTEGER,
room_id INTEGER
);
CREATE TABLE IF NOT EXISTS Rooms (
room_id SERIAL PRIMARY KEY,
display_name VARCHAR(255)
);
CREATE UNIQUE INDEX IF NOT EXISTS display_name_idx ON Rooms (display_name);
CREATE TABLE IF NOT EXISTS UserVote (
user_id INTEGER,
imdb_title VARCHAR(255),
rank INTEGER
);

View File

@@ -4,9 +4,12 @@ use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
};
use user::User;
use movies::Movies;
mod movies;
mod user;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -89,6 +92,7 @@ fn HomePage() -> impl IntoView {
view! {
<h1>"Welcome to Wiseau movie picker"</h1>
<User></User>
<Movies></Movies>
<button on:click=onclick>"Click Me: "
<Transition fallback=move || view! { <p>"Loading..."</p> }>

87
src/app/user.rs Normal file
View File

@@ -0,0 +1,87 @@
use leptos::prelude::*;
#[server]
pub async fn set_user(display_name: String, secret: String) -> Result<(), ServerFnError> {
use crate::common::Context;
use axum::http::{HeaderName, HeaderValue};
use leptos_axum::ResponseOptions;
use log::info;
use rand::{distr::Alphanumeric, Rng};
let mut secret = secret;
if display_name.len() == 0 && secret.len() == 0 {
return Err(ServerFnError::MissingArg(
"need either secret or display_name".into(),
));
}
info!("set_user called");
let data = use_context::<Context>().unwrap();
let mut client = data.client.lock().await;
let txn = client.transaction().await?;
// If the secret exists, update the database
if secret.len() > 0 {
// Validate the secret exists
txn.query_one("SELECT user_id FROM Users WHERE priv = $1", &[&secret])
.await?;
// Update display name if needed
if display_name.len() > 0 {
info!("Updating user with name {}", &display_name);
txn.execute(
"UPDATE Users SET display_name = $1 WHERE secret = $2;",
&[&display_name, &secret],
)
.await?;
}
} else if secret.len() == 0 {
// Create a new secret
info!("Creating user with name {}", &display_name);
secret = rand::rng()
.sample_iter(&Alphanumeric)
.take(4096)
.map(char::from)
.collect();
let public: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
txn.execute(
"INSERT INTO Users (display_name, priv, pub) VALUES ($1, $2, $3);",
&[&display_name, &secret, &public],
)
.await?;
}
txn.commit().await?;
info!("Setting headers");
// Set user auth token
let response = expect_context::<ResponseOptions>();
info!("Appending header");
response.insert_header(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&format! {"Basic {}", secret})?,
);
info!("Returning");
Ok(())
}
/// Renders the home page of your application.
#[component]
pub fn User() -> impl IntoView {
let set_user = ServerMultiAction::<SetUser>::new();
let create_user_view = view! {
<MultiActionForm action=set_user>
<div>
<p>"Display name to use; this will change your display name if you set a secret"</p>
<label>"Display name" <input type="text" name="display_name"/></label>
</div>
<div>
<p>"Leave blank to create a new user; enter the secret key to login to an existing user"</p>
<label>"Secret" <input type="text" name="secret"/></label>
</div>
<div>
<input type="submit" value="Login"/>
</div>
</MultiActionForm>
};
create_user_view
}

View File

@@ -1,5 +1,8 @@
use anyhow::{anyhow, Result};
use axum::extract::Request;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_postgres::Client;
use crate::model::Movie;
@@ -7,17 +10,25 @@ use crate::model::Movie;
pub struct Context {
pub counter: Arc<Mutex<usize>>,
pub movies: Arc<Mutex<Vec<Movie>>>,
pub client: Arc<Mutex<Client>>,
}
impl Context {
pub fn new() -> Self {
let movies = vec![
Movie::new("Hackers"),
Movie::new("Princess Bridge"),
];
pub fn new(client: Client) -> Self {
let movies = vec![Movie::new("Hackers"), Movie::new("Princess Bridge")];
Self {
counter: Arc::new(Mutex::new(0)),
movies: Arc::new(Mutex::new(movies)),
client: Arc::new(Mutex::new(client)),
}
}
}
}
pub async fn user_id(ctx: &Context, request: Request) -> Result<usize> {
let client = ctx.client.lock().await;
let secret = request.headers().get("authorization").ok_or(anyhow!("auth header not found"))?;
let res = client
.query_one("SELECT user_id FROM Users WHERE secret = $1", &[&secret.to_str()?])
.await?;
Ok(res.get::<_, i64>(0) as usize)
}

View File

@@ -2,6 +2,8 @@ pub mod app;
pub mod model;
#[cfg(feature = "ssr")]
pub mod common;
#[cfg(feature = "ssr")]
pub mod pool;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]

View File

@@ -1,21 +1,54 @@
use wiseau::common;
//#[cfg(feature = "ssr")]
/// Simple program to greet a person
#[cfg(feature = "ssr")]
#[derive(clap::Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Postgres connection string
#[arg(short, long)]
postgres: String,
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
use axum::Router;
use clap::Parser;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use log::info;
use tokio_postgres::NoTls;
use wiseau::app::*;
env_logger::init();
let args = Args::parse();
// Connect to the database
let (client, connection) = tokio_postgres::connect(&args.postgres, NoTls).await?;
// Spin off the database worker
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
// Sanity check the database connection
let rows = client.query("SELECT $1::TEXT", &[&"hello world"]).await?;
let value: &str = rows[0].get(0);
assert_eq!(value, "hello world");
// Setup leptos
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let context = common::Context::new();
let context = common::Context::new(client);
let app = Router::new()
.leptos_routes_with_context(
@@ -32,9 +65,11 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
info!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
Ok(())
}

1
src/pool.rs Normal file
View File

@@ -0,0 +1 @@
// Write a psql connection pool