simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit e9293bae2480eb0a009d4342300d92a8990db357
parent 654245fbe3ef2deb0fe87b9a54646726a7cdfec2
Author: Silas Brack <silasbrack@gmail.com>
Date:   Sat, 13 Jun 2026 14:53:23 +0200

feat: replace JWTs with opaque session tokens

JWTs gave us statelessness we never used (secret rotated on every
restart anyway). Opaque sessions are simpler and support real
logout/revocation.

- New session table (token, user_id, created_at)
- Login creates a random 32-byte hex token, stores in DB
- Auth check is a primary key lookup (~4µs)
- Logout DELETEs the session row — actually invalidates it
- Removed jsonwebtoken dep (and its ring crypto transitive tree)
- Removed jwt_secret from Config
- src/auth.rs now only has password hashing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
MCargo.lock | 184++-----------------------------------------------------------------------------
MCargo.toml | 1-
Amigrations/0004_sessions.sql | 12++++++++++++
Msrc/auth.rs | 38--------------------------------------
Msrc/config.rs | 6------
Msrc/database.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/handlers/auth.rs | 20++++++++++++--------
Msrc/handlers/comment.rs | 7+++----
Msrc/handlers/feed.rs | 9++++-----
Msrc/handlers/search.rs | 3+--
Msrc/handlers/story.rs | 13++++++-------
Msrc/handlers/tag.rs | 3+--
Msrc/handlers/user.rs | 3+--
Msrc/handlers/vote.rs | 5++---
Msrc/migrations.rs | 1+
Msrc/state.rs | 9+++++++++
16 files changed, 99 insertions(+), 259 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -167,12 +167,6 @@ dependencies = [ ] [[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -392,10 +386,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -546,21 +538,6 @@ dependencies = [ ] [[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -630,7 +607,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -644,31 +621,12 @@ dependencies = [ ] [[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -695,16 +653,6 @@ dependencies = [ ] [[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - -[[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -786,20 +734,6 @@ dependencies = [ ] [[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] name = "rusqlite" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -906,7 +840,6 @@ dependencies = [ "axum", "axum-extra", "chrono", - "jsonwebtoken", "rand", "rusqlite", "serde", @@ -915,18 +848,6 @@ dependencies = [ ] [[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror", - "time", -] - -[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -945,7 +866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -972,26 +893,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "time" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1033,7 +934,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1139,12 +1040,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1268,15 +1163,6 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -1285,70 +1171,6 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -13,5 +13,4 @@ serde = { version = "1", features = ["derive"] } tokio-stream = { version = "0.1", features = ["sync"] } rusqlite = "0.31" argon2 = "0.5" -jsonwebtoken = "9" rand = "0.8" diff --git a/migrations/0004_sessions.sql b/migrations/0004_sessions.sql @@ -0,0 +1,12 @@ +BEGIN IMMEDIATE; + +CREATE TABLE session ( + token TEXT PRIMARY KEY NOT NULL, + user_id INTEGER NOT NULL REFERENCES user(id), + created_at TEXT NOT NULL DEFAULT (datetime('now')) +) STRICT; + +CREATE INDEX idx_session_user_id ON session(user_id); + +UPDATE migration_version SET version = 4; +COMMIT; diff --git a/src/auth.rs b/src/auth.rs @@ -2,17 +2,9 @@ use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use serde::{Deserialize, Serialize}; use crate::error::AppError; -#[derive(Debug, Serialize, Deserialize)] -struct Claims { - sub: String, // user_id as string - exp: usize, -} - pub fn hash_password(password: &str) -> Result<String, AppError> { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); @@ -29,33 +21,3 @@ pub fn verify_password(password: &str, hash: &str) -> Result<bool, AppError> { .verify_password(password.as_bytes(), &parsed_hash) .is_ok()) } - -pub fn create_token(user_id: i64, secret: &str) -> Result<String, AppError> { - let expiration = chrono::Utc::now() - .checked_add_signed(chrono::Duration::hours(24)) - .ok_or_else(|| AppError::Database("Failed to calculate token expiry".to_string()))? - .timestamp() as usize; - - let claims = Claims { - sub: user_id.to_string(), - exp: expiration, - }; - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .map_err(|e| AppError::Database(format!("Token creation error: {}", e))) -} - -pub fn verify_token(token: &str, secret: &str) -> Option<i64> { - let token_data = decode::<Claims>( - token, - &DecodingKey::from_secret(secret.as_bytes()), - &Validation::default(), - ) - .ok()?; - - token_data.claims.sub.parse().ok() -} diff --git a/src/config.rs b/src/config.rs @@ -2,7 +2,6 @@ use std::env; pub struct Config { pub db_path: String, - pub jwt_secret: String, pub debug: bool, pub addr: String, pub smtp_addr: String, @@ -18,11 +17,6 @@ impl Config { pub fn from_env() -> Self { Self { db_path: env("DB_PATH"), - jwt_secret: { - use rand::Rng; - let bytes: [u8; 32] = rand::thread_rng().gen(); - bytes.iter().map(|b| format!("{:02x}", b)).collect() - }, debug: env("DEBUG") == "true", addr: env("ADDR"), smtp_addr: env("SMTP_ADDR"), diff --git a/src/database.rs b/src/database.rs @@ -789,6 +789,50 @@ impl Database { } // ====================================================================== + // Session queries + // ====================================================================== + + pub fn create_session(&self, user_id: i64) -> Result<String, AppError> { + use rand::Rng; + let bytes: [u8; 32] = rand::thread_rng().gen(); + let token: String = bytes.iter().map(|b| format!("{:02x}", b)).collect(); + + let conn = self.write_conn()?; + conn.execute( + "INSERT INTO session (token, user_id) VALUES (?1, ?2)", + params![token, user_id], + ).map_err(|e| AppError::Database(e.to_string()))?; + + Ok(token) + } + + pub fn get_session_user_id(&self, token: &str) -> Result<Option<i64>, AppError> { + let conn = self.read_conn()?; + let mut stmt = conn.prepare_cached("SELECT user_id FROM session WHERE token = ?1") + .map_err(|e| AppError::Database(e.to_string()))?; + + match stmt.query_row(params![token], |row| row.get::<_, i64>(0)) { + Ok(id) => Ok(Some(id)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(AppError::Database(e.to_string())), + } + } + + pub fn delete_session(&self, token: &str) -> Result<(), AppError> { + let conn = self.write_conn()?; + conn.execute("DELETE FROM session WHERE token = ?1", params![token]) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + + pub fn delete_user_sessions(&self, user_id: i64) -> Result<(), AppError> { + let conn = self.write_conn()?; + conn.execute("DELETE FROM session WHERE user_id = ?1", params![user_id]) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + + // ====================================================================== // Enrichment queries // ====================================================================== diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs @@ -12,12 +12,11 @@ use crate::error::AppError; use crate::state::AppState; use crate::templates::{ApplicationTemplate, LoginTemplate, RegisterTemplate, ResendVerificationTemplate}; -const AUTH_COOKIE: &str = "auth_token"; +const AUTH_COOKIE: &str = "session"; -/// Extract user_id from the auth cookie JWT -pub fn get_user_id_from_cookies(jar: &CookieJar, jwt_secret: &str) -> Option<i64> { - jar.get(AUTH_COOKIE) - .and_then(|c| auth::verify_token(c.value(), jwt_secret)) +/// Extract the session token from cookies. The caller must look it up in the DB. +pub fn get_session_token(jar: &CookieJar) -> Option<String> { + jar.get(AUTH_COOKIE).map(|c| c.value().to_string()) } fn set_auth_cookie(jar: CookieJar, token: &str) -> CookieJar { @@ -129,7 +128,6 @@ pub async fn login( ) -> Result<(CookieJar, Redirect), Html<String>> { let email = form.email.clone(); let password = form.password.clone(); - let jwt_secret = state.config.jwt_secret.clone(); let result = state.db_query(move |db| { let user = db.get_user_by_email(&email)? @@ -146,7 +144,7 @@ pub async fn login( )); } - let token = auth::create_token(user.id, &jwt_secret)?; + let token = db.create_session(user.id)?; Ok(token) }).await; @@ -343,7 +341,13 @@ pub async fn resend_verification( Ok(Html(app.render()?)) } -pub async fn logout(jar: CookieJar) -> (CookieJar, Redirect) { +pub async fn logout( + State(state): State<AppState>, + jar: CookieJar, +) -> (CookieJar, Redirect) { + if let Some(token) = get_session_token(&jar) { + let _ = state.db_query(move |db| db.delete_session(&token)).await; + } let jar = clear_auth_cookie(jar); (jar, Redirect::to("/")) } diff --git a/src/handlers/comment.rs b/src/handlers/comment.rs @@ -8,7 +8,6 @@ use axum_extra::extract::cookie::CookieJar; use serde::Deserialize; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::state::AppState; use crate::templates::{ApplicationTemplate, ReplyFormTemplate}; @@ -24,7 +23,7 @@ pub async fn create_comment( Path(story_id): Path<i64>, Form(form): Form<CommentForm>, ) -> Result<Redirect, Html<String>> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret).ok_or_else(|| { + let user_id = state.get_user_id(&jar).await.ok_or_else(|| { Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; @@ -51,7 +50,7 @@ pub async fn show_reply_form( jar: CookieJar, Path(comment_id): Path<i64>, ) -> Result<Html<String>, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; if user_id.is_none() { return Ok(Html( @@ -102,7 +101,7 @@ pub async fn submit_reply( Path(parent_id): Path<i64>, Form(form): Form<ReplyForm>, ) -> Result<Redirect, Html<String>> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret).ok_or_else(|| { + let user_id = state.get_user_id(&jar).await.ok_or_else(|| { Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; diff --git a/src/handlers/feed.rs b/src/handlers/feed.rs @@ -9,7 +9,6 @@ use serde::Deserialize; use crate::database::{Category, StoryWithMeta}; use crate::database::{self, TARGET_TYPE_STORY}; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::state::AppState; use crate::templates::{ApplicationTemplate, FeedTemplate}; @@ -42,7 +41,7 @@ async fn fetch_feed( sort: FeedSort, ) -> Result<(Vec<StoryWithMeta>, Vec<Category>, bool), AppError> { let offset = query.page * PAGE_LIMIT; - let user_id = get_user_id_from_cookies(jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; let tag_id = query.tag; let page = query.page; @@ -114,7 +113,7 @@ pub async fn show_feed( Query(query): Query<FeedQuery>, ) -> Result<Html<String>, AppError> { let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::Hot).await?; - let is_logged_in = get_user_id_from_cookies(&jar, &state.config.jwt_secret).is_some(); + let is_logged_in = state.get_user_id(&jar).await.is_some(); let feed = FeedTemplate { stories, @@ -138,7 +137,7 @@ pub async fn show_new( Query(query): Query<FeedQuery>, ) -> Result<Html<String>, AppError> { let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::New).await?; - let is_logged_in = get_user_id_from_cookies(&jar, &state.config.jwt_secret).is_some(); + let is_logged_in = state.get_user_id(&jar).await.is_some(); let feed = FeedTemplate { stories, @@ -162,7 +161,7 @@ pub async fn show_top( Query(query): Query<FeedQuery>, ) -> Result<Html<String>, AppError> { let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::Top).await?; - let is_logged_in = get_user_id_from_cookies(&jar, &state.config.jwt_secret).is_some(); + let is_logged_in = state.get_user_id(&jar).await.is_some(); let feed = FeedTemplate { stories, diff --git a/src/handlers/search.rs b/src/handlers/search.rs @@ -8,7 +8,6 @@ use axum_extra::extract::cookie::CookieJar; use serde::Deserialize; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::sse; use crate::state::AppState; use crate::templates::{ApplicationTemplate, SearchResultsTemplate, SearchTemplate}; @@ -17,7 +16,7 @@ pub async fn open_search( State(state): State<AppState>, jar: CookieJar, ) -> Result<Html<String>, AppError> { - let is_logged_in = get_user_id_from_cookies(&jar, &state.config.jwt_secret).is_some(); + let is_logged_in = state.get_user_id(&jar).await.is_some(); let template = SearchTemplate { is_logged_in }; let app = ApplicationTemplate { content: template.render()?, diff --git a/src/handlers/story.rs b/src/handlers/story.rs @@ -12,7 +12,6 @@ use serde::Deserialize; use crate::database::{CommentWithMeta, StoryWithMeta}; use crate::database::{self, TARGET_TYPE_COMMENT, TARGET_TYPE_STORY}; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::state::AppState; use crate::templates::{ApplicationTemplate, CommentsTemplate, EditTemplate, StoryTemplate, SubmitTemplate}; @@ -71,7 +70,7 @@ pub async fn show_story( jar: CookieJar, Path(id): Path<i64>, ) -> Result<Html<String>, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; let (story, tags, author_username, story_voted) = state.db_query(move |db| { let story = db.get_story(id)? @@ -128,7 +127,7 @@ pub async fn show_submit( State(state): State<AppState>, jar: CookieJar, ) -> Result<Html<String>, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; if user_id.is_none() { return Ok(Html( r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string(), @@ -170,7 +169,7 @@ pub async fn submit_story( jar: CookieJar, Form(form): Form<SubmitForm>, ) -> Result<Redirect, Html<String>> { - let uid = get_user_id_from_cookies(&jar, &state.config.jwt_secret).ok_or_else(|| { + let uid = state.get_user_id(&jar).await.ok_or_else(|| { Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; @@ -258,7 +257,7 @@ pub async fn show_edit( jar: CookieJar, Path(id): Path<i64>, ) -> Result<Html<String>, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret) + let user_id = state.get_user_id(&jar).await .ok_or_else(|| AppError::Unauthorized("Login required".to_string()))?; let (story, all_tags, story_tags) = state.db_query(move |db| { @@ -312,7 +311,7 @@ pub async fn edit_story( Path(id): Path<i64>, Form(form): Form<EditForm>, ) -> Result<Redirect, Html<String>> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret).ok_or_else(|| { + let user_id = state.get_user_id(&jar).await.ok_or_else(|| { Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; @@ -393,7 +392,7 @@ pub async fn comments_stream( jar: CookieJar, Path(story_id): Path<i64>, ) -> Response { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; let is_logged_in = user_id.is_some(); let (tx, rx) = tokio::sync::mpsc::channel::<Result<Event, Infallible>>(10); let mut broadcast_rx = state.comment_tx.subscribe(); diff --git a/src/handlers/tag.rs b/src/handlers/tag.rs @@ -9,7 +9,6 @@ use serde::Deserialize; use crate::database::{Category, StoryWithMeta}; use crate::database::{self, TARGET_TYPE_STORY}; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::state::AppState; use crate::templates::{ApplicationTemplate, TagTemplate}; @@ -27,7 +26,7 @@ pub async fn show_tag( Path(tag_name): Path<String>, Query(query): Query<TagQuery>, ) -> Result<Html<String>, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let user_id = state.get_user_id(&jar).await; let offset = query.page * PAGE_LIMIT; let page = query.page; let tag_name_clone = tag_name.clone(); diff --git a/src/handlers/user.rs b/src/handlers/user.rs @@ -9,7 +9,6 @@ use serde::Deserialize; use crate::database::{CommentWithMeta, StoryWithMeta, UserProfile}; use crate::database::{self, TARGET_TYPE_STORY}; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::state::AppState; use crate::templates::{ApplicationTemplate, UserTemplate}; @@ -29,7 +28,7 @@ pub async fn show_user( Path(username): Path<String>, Query(query): Query<UserQuery>, ) -> Result<Html<String>, AppError> { - let current_user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret); + let current_user_id = state.get_user_id(&jar).await; let tab = query.tab.clone(); let username_clone = username.clone(); diff --git a/src/handlers/vote.rs b/src/handlers/vote.rs @@ -7,7 +7,6 @@ use axum_extra::extract::cookie::CookieJar; use crate::database::{TARGET_TYPE_COMMENT, TARGET_TYPE_STORY}; use crate::error::AppError; -use crate::handlers::auth::get_user_id_from_cookies; use crate::sse; use crate::state::AppState; use crate::templates::VoteButtonTemplate; @@ -35,7 +34,7 @@ pub async fn vote_story( jar: CookieJar, Path(story_id): Path<i64>, ) -> Result<Response, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret) + let user_id = state.get_user_id(&jar).await .ok_or_else(|| AppError::Unauthorized("Login required to vote".to_string()))?; let (voted, new_score) = state.db_query(move |db| { @@ -53,7 +52,7 @@ pub async fn vote_comment( jar: CookieJar, Path(comment_id): Path<i64>, ) -> Result<Response, AppError> { - let user_id = get_user_id_from_cookies(&jar, &state.config.jwt_secret) + let user_id = state.get_user_id(&jar).await .ok_or_else(|| AppError::Unauthorized("Login required to vote".to_string()))?; let (voted, new_score) = state.db_query(move |db| { diff --git a/src/migrations.rs b/src/migrations.rs @@ -4,6 +4,7 @@ const MIGRATIONS: &[&str] = &[ include_str!("../migrations/0001_initial_schema.sql"), include_str!("../migrations/0002_email_verification.sql"), include_str!("../migrations/0003_strict_tables.sql"), + include_str!("../migrations/0004_sessions.sql"), ]; pub fn run(conn: &Connection) -> Result<(), String> { diff --git a/src/state.rs b/src/state.rs @@ -21,6 +21,15 @@ impl AppState { } } + /// Look up session token in DB, return user_id if valid. + pub async fn get_user_id(&self, jar: &axum_extra::extract::cookie::CookieJar) -> Option<i64> { + let token = crate::handlers::auth::get_session_token(jar)?; + self.db_query(move |db| db.get_session_user_id(&token)) + .await + .ok() + .flatten() + } + pub async fn db_query<F, T>(&self, f: F) -> Result<T, AppError> where F: FnOnce(&Database) -> Result<T, AppError> + Send + 'static,