From 37f6aa90a473710e57ef65c4e8f03ad445b3c03e Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 08 Nov 2021 00:37:13 +0000 Subject: [PATCH] Implement automatic session extending Fixes #30 --- chartered-db/src/users.rs | 20 ++++++++++++++++++++ chartered-frontend/src/useAuth.tsx | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- chartered-web/src/endpoints/web_api/auth/extend.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/auth/mod.rs | 5 ++++- 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 63530e7..a8a0710 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -403,6 +403,26 @@ }) .await? } + + pub async fn extend( + self: Arc, + conn: ConnectionPool, + given_expires_at: chrono::NaiveDateTime, + ) -> Result { + use crate::schema::user_sessions::dsl::expires_at; + + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let res = diesel::update(user_sessions::table) + .filter(user_sessions::id.eq(self.id)) + .set(expires_at.eq(given_expires_at)) + .execute(&conn)?; + + Ok(res > 0) + }) + .await? + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)] diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx index 85ae7eb..f576a74 100644 --- a/chartered-frontend/src/useAuth.tsx +++ a/chartered-frontend/src/useAuth.tsx @@ -19,6 +19,11 @@ picture_url?: string; } +interface ExtendResponse { + expires: number; + error?: string; +} + export interface AuthContext { login: (username: string, password: string) => Promise; oauthLogin: (provider: string) => Promise; @@ -73,18 +78,6 @@ authStorage.pictureUrl, ]; }); - - useEffect(() => { - localStorage.setItem( - "charteredAuthentication", - JSON.stringify({ - userUuid: auth?.[0], - authKey: auth?.[1], - expires: auth?.[2], - pictureUrl: auth?.[3], - }) - ); - }, [auth]); const handleLoginResponse = (response: LoginResponse) => { if (response.error) { @@ -158,6 +151,36 @@ console.error("Failed to fully log user out of session", e); } finally { setAuth(null); + } + }; + + const extendSession = async () => { + if (auth === null || auth?.[2] < new Date()) { + return; + } + + try { + const res = await fetch( + `${BASE_URL}/a/${getAuthKey()}/web/v1/auth/extend`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "User-Agent": window.navigator.userAgent, + }, + } + ); + const response: ExtendResponse = await res.json(); + + if (response.error) { + throw new Error(response.error); + } + + const newAuth = [...auth]; + newAuth[2] = new Date(response.expires); + setAuth(newAuth); + } catch (e) { + console.error("Failed to extend user session", e); } }; @@ -176,6 +199,30 @@ return null; } }; + + useEffect(() => { + localStorage.setItem( + "charteredAuthentication", + JSON.stringify({ + userUuid: auth?.[0], + authKey: auth?.[1], + expires: auth?.[2], + pictureUrl: auth?.[3], + }) + ); + + const extendInterval = 60000; + if (auth?.[2] && auth?.[2].getTime() - new Date().getTime() <= 0) { + extendSession(); + } + + const extendSessionIntervalId = setInterval( + () => extendSession(), + extendInterval + ); + + return () => clearInterval(extendSessionIntervalId); + }, [auth]); return { login, diff --git a/chartered-web/src/endpoints/web_api/auth/extend.rs b/chartered-web/src/endpoints/web_api/auth/extend.rs new file mode 100644 index 0000000..2a58e50 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/auth/extend.rs @@ -1,0 +1,49 @@ +//! Extends a user's session. Called on a loop from the web UI to keep +//! the user from logging out during periods of idleness from the API's +//! perspective. + +use axum::{extract, Json}; +use chartered_db::users::UserSession; +use chartered_db::ConnectionPool; +use serde::Serialize; +use std::sync::Arc; +use thiserror::Error; + +pub async fn handle( + extract::Extension(session): extract::Extension>, + extract::Extension(db): extract::Extension, +) -> Result, Error> { + let expires = chrono::Utc::now() + chrono::Duration::hours(1); + + if session.extend(db, expires.naive_utc()).await? { + Ok(Json(ExtendResponse { expires })) + } else { + Err(Error::InvalidSession) + } +} + +#[derive(Debug, Serialize)] +pub struct ExtendResponse { + expires: chrono::DateTime, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), + #[error("Invalid session")] + InvalidSession, +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(e) => e.status_code(), + Self::InvalidSession => StatusCode::BAD_REQUEST, + } + } +} + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs index e869b63..4f0f6cc 100644 --- a/chartered-web/src/endpoints/web_api/auth/mod.rs +++ a/chartered-web/src/endpoints/web_api/auth/mod.rs @@ -11,12 +11,15 @@ use serde::Serialize; +pub mod extend; pub mod logout; pub mod openid; pub mod password; pub fn authenticated_routes() -> Router { - Router::new().route("/logout", get(logout::handle)) + Router::new() + .route("/logout", get(logout::handle)) + .route("/extend", get(extend::handle)) } pub fn unauthenticated_routes() -> Router { -- rgit 0.1.3