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:
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,