From 730cf9d176d30c19ef99d6946395236971adfe58 Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Sun, 13 Jul 2025 01:23:09 +0200 Subject: [PATCH] stuff --- idp/migrations/20250712182250_sessions.sql | 6 +- idp/src/main.rs | 84 +++++++++++++++++++--- idp/src/session.rs | 48 ++++++++++--- idp/templates/index.html | 3 + idp/templates/sessions.html | 27 +++++++ 5 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 idp/templates/sessions.html diff --git a/idp/migrations/20250712182250_sessions.sql b/idp/migrations/20250712182250_sessions.sql index e409689..4735ec3 100644 --- a/idp/migrations/20250712182250_sessions.sql +++ b/idp/migrations/20250712182250_sessions.sql @@ -1,7 +1,11 @@ CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT NOT NULL PRIMARY KEY, + session_public_id INTEGER NOT NULL PRIMARY KEY, + session_id TEXT NOT NULL, user_id INTEGER NOT NULL, created INTEGER NOT NULL, + user_agent TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) STRICT; + +CREATE INDEX sessions_session_public_id ON sessions(session_public_id); diff --git a/idp/src/main.rs b/idp/src/main.rs index 4e4edc7..45ad6d3 100644 --- a/idp/src/main.rs +++ b/idp/src/main.rs @@ -8,7 +8,7 @@ use askama::Template; use axum::{ Form, Router, extract::State, - http::StatusCode, + http::{HeaderMap, StatusCode}, response::{Html, IntoResponse, Redirect, Response}, routing::{get, post}, }; @@ -60,6 +60,8 @@ async fn main() -> Result<()> { .route("/signup", get(signup).post(signup_post)) .route("/login", get(login).post(login_post)) .route("/2fa", get(list_2fa)) + .route("/sessions", get(list_sessions)) + .route("/sessions/delete", post(delete_session)) .route("/2fa/delete", post(delete_2fa)) .route("/add-totp", get(add_totp).post(add_totp_post)) .route("/users", get(users)) @@ -153,6 +155,54 @@ async fn list_2fa(user: UserSession, State(db): State) -> Result, +) -> Result { + let Some(user) = user.0 else { + return Err(Redirect::to("/").into_response()); + }; + + let sessions = session::find_sessions_for_user(&db, user.user_id) + .await + .map_err(|err| { + error!(?err, "Error fetching sessions"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + })?; + + #[derive(askama::Template)] + #[template(path = "sessions.html")] + struct Template { + sessions: Vec, + } + + Ok(Html(Template { sessions }.render().unwrap())) +} + +#[derive(Deserialize)] +struct DeleteSessionForm { + session_public_id: i64, +} + +async fn delete_session( + user: UserSession, + State(db): State, + Form(form): Form, +) -> Result { + let Some(user) = user.0 else { + return Err(Redirect::to("/").into_response()); + }; + + session::delete_session(&db, user.user_id, form.session_public_id) + .await + .map_err(|err| { + error!(?err, "Failed to delete session"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + })?; + + Ok(Redirect::to("/sessions")) +} + #[derive(Deserialize)] struct Delete2faForm { device_id: i64, @@ -253,11 +303,17 @@ struct UsernamePasswordForm { password: String, } -async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result, Response> { - let session = session::create_session(&db, user_id).await.map_err(|err| { - error!(?err, "Failed to create session for user"); - StatusCode::INTERNAL_SERVER_ERROR.into_response() - })?; +async fn make_session_cookie_for_user( + db: &Db, + user_id: i64, + user_agent: &str, +) -> Result, Response> { + let session = session::create_session(&db, user_id, user_agent) + .await + .map_err(|err| { + error!(?err, "Failed to create session for user"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + })?; Ok(Cookie::build((SESSION_ID_COOKIE_NAME, session.0)) .secure(true) @@ -272,6 +328,7 @@ async fn make_session_cookie_for_user(db: &Db, user_id: i64) -> Result, jar: CookieJar, Form(signup): Form, @@ -294,12 +351,18 @@ async fn signup_post( .into_response()); }; - let session_id = make_session_cookie_for_user(&db, user.id).await?; + let user_agent = headers + .get(axum::http::header::USER_AGENT) + .and_then(|v| str::from_utf8(v.as_bytes()).ok()) + .unwrap_or("unknown"); + + let session_id = make_session_cookie_for_user(&db, user.id, user_agent).await?; Ok((jar.add(session_id), Redirect::to("/"))) } async fn login_post( + headers: HeaderMap, State(db): State, jar: CookieJar, Form(login): Form, @@ -315,7 +378,12 @@ async fn login_post( return Err(Html(LoginTemplate { error: true }.render().unwrap()).into_response()); }; - let session_id = make_session_cookie_for_user(&db, user.id).await?; + let user_agent = headers + .get(axum::http::header::USER_AGENT) + .and_then(|v| str::from_utf8(v.as_bytes()).ok()) + .unwrap_or("unknown"); + + let session_id = make_session_cookie_for_user(&db, user.id, user_agent).await?; Ok((jar.add(session_id), Redirect::to("/"))) } diff --git a/idp/src/session.rs b/idp/src/session.rs index f0f6ec2..b1e52e4 100644 --- a/idp/src/session.rs +++ b/idp/src/session.rs @@ -34,18 +34,48 @@ pub async fn find_session(db: &Db, session_id: &str) -> Result Result { +#[derive(Debug, sqlx::FromRow)] +pub struct SessionForList { + pub created: i64, + pub session_public_id: i64, + pub user_agent: String, +} + +pub async fn find_sessions_for_user(db: &Db, user_id: i64) -> Result> { + sqlx::query_as::<_, SessionForList>( + "select created, session_public_id, user_agent from sessions where user_id = ?", + ) + .bind(user_id) + .fetch_all(&db.pool) + .await + .wrap_err("failed to fetch sessions for user") +} + +pub async fn delete_session(db: &Db, user_id: i64, session_public_id: i64) -> Result<()> { + sqlx::query("delete from sessions where user_id = ? and session_public_id = ?") + .bind(user_id) + .bind(session_public_id) + .execute(&db.pool) + .await + .wrap_err("failed to delete session")?; + Ok(()) +} + +pub async fn create_session(db: &Db, user_id: i64, user_agent: &str) -> Result { let mut session_id = [0_u8; 32]; rand_core::OsRng.fill_bytes(&mut session_id); let session_id = format!("idpsess_{}", hex::encode(session_id)); - sqlx::query("insert into sessions (session_id, user_id, created) values (?, ?, ?)") - .bind(&session_id) - .bind(user_id) - .bind(jiff::Timestamp::now().as_millisecond()) - .execute(&db.pool) - .await - .wrap_err("inserting new session")?; + sqlx::query( + "insert into sessions (session_id, user_id, created, user_agent) values (?, ?, ?, ?)", + ) + .bind(&session_id) + .bind(user_id) + .bind(jiff::Timestamp::now().as_millisecond()) + .bind(user_agent) + .execute(&db.pool) + .await + .wrap_err("inserting new session")?; Ok(SessionId(session_id)) } @@ -63,7 +93,7 @@ impl FromRequestParts for UserSession { let jar = CookieJar::from_headers(&parts.headers); match jar.get(crate::SESSION_ID_COOKIE_NAME) { - None => Err(StatusCode::FORBIDDEN.into_response()), + None => Ok(UserSession(None)), Some(cookie) => { let sess = find_session(&db, cookie.value()).await; match sess { diff --git a/idp/templates/index.html b/idp/templates/index.html index 9ec7118..c66dcb5 100644 --- a/idp/templates/index.html +++ b/idp/templates/index.html @@ -22,6 +22,9 @@ List all users {% if let Some(username) = username %} + diff --git a/idp/templates/sessions.html b/idp/templates/sessions.html new file mode 100644 index 0000000..706f682 --- /dev/null +++ b/idp/templates/sessions.html @@ -0,0 +1,27 @@ + + + + + + Sessions - IDP + + + +

See all sessions your account

+ home + +
    + {% for session in sessions %} +
  • + {{session.user_agent}} (added + {{jiff::Timestamp::from_millisecond(*session.created).unwrap() + .strftime("%A, %d %B %Y at %I:%M %Q")}}) +
    + + +
    +
  • + {% endfor %} +
+ +