implemented automated user session login and session refresh

This commit is contained in:
Patrick 2025-10-04 13:05:03 +02:00
parent 976cd3edb9
commit 16b5aba457
5 changed files with 149 additions and 3 deletions

77
src/hooks.server.ts Normal file
View File

@ -0,0 +1,77 @@
import type { Handle } from "@sveltejs/kit"
import { error, redirect, json, fail } from "@sveltejs/kit"
import { Error401Cause } from "$lib/errors"
import Config from "$lib/server/config"
import UserMgmt from "$lib/server/usermgmt"
function action_fail<T = undefined>(status: number, data: T) {
return json(fail(status, data))
}
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("session")
let session = await UserMgmt.session_login(token ?? "")
if (session && event.route.id !== "/api/refresh") {
if (session.expires.getTime() < Date.now() + Config.session_refresh_grace) {
const response = await event.fetch("/api/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: token
})
})
if (response.ok) {
const new_session = await response.json()
if (new_session && new_session.token !== null && new_session.expires !== null) {
event.cookies.set("session", new_session.token, {
expires: new Date(new_session.expires),
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/"
})
// For mobile
/*event.setHeaders({
"X-Authorization-Refresh": new_session.token
})*/
session = await UserMgmt.session_login(new_session.token)
}
}
}
}
event.locals.user = session?.user
if (!event.route.id) {
return error(404, { message: "Diese Seite existiert nicht" })
}
if (event.route.id.startsWith("/api")) {
if (event.route.id.startsWith("/api/users") && !event.locals.user) {
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Please log in" })
}
} else {
if (!event.locals.user && !event.route.id.startsWith("/login")) {
if (event.request.method === "POST") {
if (event.request.headers.get("x-sveltekit-action")) {
return action_fail(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
}
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
}
return redirect(307, "/login")
}
}
return await resolve(event)
}

View File

@ -14,6 +14,10 @@ export class DuplicateError extends Error {
}
}
export const enum Error401Cause {
NotLoggedIn
}
export const enum RegisterResponseCause {
Server = 1,
MalformedRequest,
@ -32,3 +36,10 @@ export const enum LoginResponseCause {
NotFound,
Timeout
}
export const enum RefreshResponseCause {
Server = 1,
MalformedRequest,
InvalidToken,
InvalidSession
}

View File

@ -38,6 +38,7 @@ class Config {
readonly is_production: boolean = process.env.NODE_ENV == "production"
private _session_timeout: number = 15 * 60 * 1000
private _session_refresh_grace: number = 5 * 60 * 1000 // time until expiration
get log_dir(): string {
return this._log_dir
@ -47,6 +48,7 @@ class Config {
}
get session_timeout(): number { return this._session_timeout }
get session_refresh_grace(): number { return this._session_refresh_grace }
constructor() {
this._log_dir = resolve_env_to_path(process.env.APP_LOG_DIR, "./data/logs")

View File

@ -52,6 +52,10 @@ class Cache {
}
}
invalidate_session(session: SessionData) {
this._session_map.delete(session.token)
}
get_session(token: string): SessionData|null {
const session_info = this._session_map.get(token)
if (!session_info) return null
@ -118,14 +122,39 @@ class UserMgmt {
return null
}
const token = Crypto.randomBytes(32).toBase64()
const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout))
const session_info = this._generate_session_for_user(user)
return session_info
}
async session_login(token: string): Promise<SessionData|null> {
return this._cache.get_session(token)
const session = this._cache.get_session(token)
if (!session) {
return null
}
if (session?.expires.getTime() < Date.now()) {
return null
}
return session
}
async refresh_session(token: string): Promise<SessionData|null> {
const session = await this.session_login(token)
if (!session) {
return null
}
const new_session = this._generate_session_for_user(session.user)
this._cache.invalidate_session(session)
return new_session
}
private _generate_session_for_user(user: User): SessionData {
const token = Crypto.randomBytes(32).toBase64()
const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout))
return session_info
}
}

View File

@ -0,0 +1,27 @@
import type { RequestHandler } from "./$types"
import { error, json } from "@sveltejs/kit"
import UserMgmt from "$lib/server/usermgmt"
import { RefreshResponseCause } from "$lib/errors"
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
const token = data["token"]
if (!token || typeof token !== "string") {
return error(400, { cause: RefreshResponseCause.MalformedRequest, message: "token must be provided as string." })
}
const new_session = await UserMgmt.refresh_session(token)
if (!new_session) {
return error(401, { cause: RefreshResponseCause.InvalidSession, message: "No session for token" })
}
return json({
token: new_session.token,
expires: new_session.expires
})
}