From 66d8144a03fe2b4ce6a53000271f8ba61ede68ee Mon Sep 17 00:00:00 2001
From: Charles
Date: Sun, 11 May 2025 20:16:42 -0700
Subject: [PATCH] add: schema, add user but no middleware
---
Cargo.toml | 19 +++++++++++
schema.sql | 29 +++++++++++++++++
src/app.rs | 4 +++
src/app/user.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++
src/common.rs | 23 +++++++++----
src/lib.rs | 2 ++
src/main.rs | 43 +++++++++++++++++++++---
src/pool.rs | 1 +
8 files changed, 198 insertions(+), 10 deletions(-)
create mode 100644 schema.sql
create mode 100644 src/app/user.rs
create mode 100644 src/pool.rs
diff --git a/Cargo.toml b/Cargo.toml
index 3326466..6767ef0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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]
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..517fc49
--- /dev/null
+++ b/schema.sql
@@ -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
+);
\ No newline at end of file
diff --git a/src/app.rs b/src/app.rs
index 849adc9..cbb5131 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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! {
"Welcome to Wiseau movie picker"
+
}>
diff --git a/src/app/user.rs b/src/app/user.rs
new file mode 100644
index 0000000..b3d2683
--- /dev/null
+++ b/src/app/user.rs
@@ -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::().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::();
+ 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::::new();
+ let create_user_view = view! {
+
+
+
"Display name to use; this will change your display name if you set a secret"
+
+
+
+
"Leave blank to create a new user; enter the secret key to login to an existing user"
+
+
+
+
+
+
+ };
+ create_user_view
+}
diff --git a/src/common.rs b/src/common.rs
index a3a6de7..09ad6c7 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -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>,
pub movies: Arc>>,
+ pub client: Arc>,
}
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)),
}
}
-}
\ No newline at end of file
+}
+
+pub async fn user_id(ctx: &Context, request: Request) -> Result {
+ 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)
+}
diff --git a/src/lib.rs b/src/lib.rs
index f581f38..6ea3913 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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]
diff --git a/src/main.rs b/src/main.rs
index 9f4dae2..17dd568 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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(())
}
diff --git a/src/pool.rs b/src/pool.rs
new file mode 100644
index 0000000..50fd716
--- /dev/null
+++ b/src/pool.rs
@@ -0,0 +1 @@
+// Write a psql connection pool
\ No newline at end of file