Implement automatic session extending
Fixes #30
Diff
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(-)
@@ -403,6 +403,26 @@
})
.await?
}
pub async fn extend(
self: Arc<Self>,
conn: ConnectionPool,
given_expires_at: chrono::NaiveDateTime,
) -> Result<bool> {
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)]
@@ -19,6 +19,11 @@
picture_url?: string;
}
interface ExtendResponse {
expires: number;
error?: string;
}
export interface AuthContext {
login: (username: string, password: string) => Promise<void>;
oauthLogin: (provider: string) => Promise<void>;
@@ -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,
@@ -1,0 +1,49 @@
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<Arc<UserSession>>,
extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<ExtendResponse>, 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<chrono::Utc>,
}
#[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);
@@ -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 {