session check
This commit is contained in:
+32
-4
@@ -1199,13 +1199,41 @@ pub async fn confirm_reset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_from_token(state: &SharedState, token: &str) -> Result<Option<User>, AuthError> {
|
pub async fn user_from_token(state: &SharedState, token: &str) -> Result<Option<User>, AuthError> {
|
||||||
let now = Utc::now().to_rfc3339();
|
let now = Utc::now();
|
||||||
sqlx::query_as::<_, User>(queries::get(state.db.kind(), queries::AUTH_USER_BY_SESSION))
|
let now_rfc3339 = now.to_rfc3339();
|
||||||
|
let user = sqlx::query_as::<_, User>(queries::get(state.db.kind(), queries::AUTH_USER_BY_SESSION))
|
||||||
.bind(token)
|
.bind(token)
|
||||||
.bind(now)
|
.bind(&now_rfc3339)
|
||||||
.fetch_optional(state.db.pool())
|
.fetch_optional(state.db.pool())
|
||||||
.await
|
.await
|
||||||
.map_err(AuthError::database)
|
.map_err(AuthError::database)?;
|
||||||
|
|
||||||
|
if let Some(user) = user {
|
||||||
|
let expires_at = (now + Duration::days(state.user_session_ttl_days)).to_rfc3339();
|
||||||
|
let refreshed = sqlx::query(queries::get(state.db.kind(), queries::AUTH_REFRESH_SESSION))
|
||||||
|
.bind(&expires_at)
|
||||||
|
.bind(token)
|
||||||
|
.bind(&now_rfc3339)
|
||||||
|
.execute(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::database)?;
|
||||||
|
if refreshed.rows_affected() == 1 {
|
||||||
|
debug!(user_id = user.id, expires_at = %expires_at, "authentication session extended");
|
||||||
|
Ok(Some(user))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sqlx::query(queries::get(
|
||||||
|
state.db.kind(),
|
||||||
|
queries::AUTH_DELETE_SESSION_BY_TOKEN,
|
||||||
|
))
|
||||||
|
.bind(token)
|
||||||
|
.execute(state.db.pool())
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::database)?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authorize_nickname(
|
pub async fn authorize_nickname(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub const AUTH_USER_BY_EXTERNAL_ID: &str = "SELECT id, nickname, email, password
|
|||||||
pub const AUTH_DELETE_USER: &str = "DELETE FROM users WHERE id = ?";
|
pub const AUTH_DELETE_USER: &str = "DELETE FROM users WHERE id = ?";
|
||||||
pub const AUTH_SESSION_EXPIRES_AT: &str = "SELECT expires_at FROM user_sessions WHERE token = ?";
|
pub const AUTH_SESSION_EXPIRES_AT: &str = "SELECT expires_at FROM user_sessions WHERE token = ?";
|
||||||
pub const AUTH_DELETE_SESSION_BY_TOKEN: &str = "DELETE FROM user_sessions WHERE token = ?";
|
pub const AUTH_DELETE_SESSION_BY_TOKEN: &str = "DELETE FROM user_sessions WHERE token = ?";
|
||||||
|
pub const AUTH_REFRESH_SESSION: &str = "UPDATE user_sessions SET expires_at = ? WHERE token = ? AND expires_at > ?";
|
||||||
pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: &str =
|
pub const AUTH_DELETE_CONFIRMATION_TOKENS_BY_USER: &str =
|
||||||
"DELETE FROM account_confirmation_tokens WHERE user_id = ?";
|
"DELETE FROM account_confirmation_tokens WHERE user_id = ?";
|
||||||
pub const AUTH_INSERT_CONFIRMATION_TOKEN: &str =
|
pub const AUTH_INSERT_CONFIRMATION_TOKEN: &str =
|
||||||
|
|||||||
@@ -6,6 +6,29 @@ function formatBytes(bytes) {
|
|||||||
return `${bytes} B`;
|
return `${bytes} B`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearExpiredSession() {
|
||||||
|
localStorage.removeItem("rustpad:auth-token");
|
||||||
|
sessionStorage.removeItem("rustpad:auth-token");
|
||||||
|
localStorage.removeItem("rustpad:nickname");
|
||||||
|
sessionStorage.removeItem("rustpad:nickname");
|
||||||
|
document.cookie = "rustpad_nickname=; Path=/; SameSite=Lax; Max-Age=0";
|
||||||
|
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearSessionIfInvalid() {
|
||||||
|
const token = localStorage.getItem("rustpad:auth-token") || sessionStorage.getItem("rustpad:auth-token");
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/me", {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
if (response.status === 401) clearExpiredSession();
|
||||||
|
} catch {
|
||||||
|
// A network failure does not prove that the session is invalid.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateUploadSize(body) {
|
function validateUploadSize(body) {
|
||||||
if (!(body instanceof FormData)) return;
|
if (!(body instanceof FormData)) return;
|
||||||
const maxBytes = Number(window.__RUSTPAD_CONFIG__?.uploadMaxSizeBytes || 0);
|
const maxBytes = Number(window.__RUSTPAD_CONFIG__?.uploadMaxSizeBytes || 0);
|
||||||
@@ -34,6 +57,7 @@ export async function api(path, options = {}) {
|
|||||||
const contentType = response.headers.get("content-type") || "";
|
const contentType = response.headers.get("content-type") || "";
|
||||||
const data = contentType.includes("application/json") ? await response.json().catch(() => ({})) : {};
|
const data = contentType.includes("application/json") ? await response.json().catch(() => ({})) : {};
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) await clearSessionIfInvalid();
|
||||||
const defaults = { 400: "Invalid request.", 401: "Authentication required.", 403: "Access denied.", 404: "The requested resource was not found.", 405: "This operation is not allowed.", 409: "The requested change conflicts with existing data.", 413: "The selected file exceeds the allowed upload limit.", 429: "Too many requests. Try again later.", 500: "Server error. Try again later.", 503: "Service temporarily unavailable." };
|
const defaults = { 400: "Invalid request.", 401: "Authentication required.", 403: "Access denied.", 404: "The requested resource was not found.", 405: "This operation is not allowed.", 409: "The requested change conflicts with existing data.", 413: "The selected file exceeds the allowed upload limit.", 429: "Too many requests. Try again later.", 500: "Server error. Try again later.", 503: "Service temporarily unavailable." };
|
||||||
const requestError = new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`);
|
const requestError = new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`);
|
||||||
requestError.status = response.status;
|
requestError.status = response.status;
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ if (identityDialog) {
|
|||||||
renderAccount(null);
|
renderAccount(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("rustpad:session-expired", () => renderAccount(null));
|
||||||
renderAccount(null);
|
renderAccount(null);
|
||||||
validateCurrentSession().then(renderAccount);
|
validateCurrentSession().then(renderAccount);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user