128 lines
2.8 KiB
TypeScript
128 lines
2.8 KiB
TypeScript
import type { User } from "$lib/server/database";
|
|
|
|
import Crypto from "node:crypto";
|
|
|
|
interface UserSession {
|
|
user: User;
|
|
last_active: Date;
|
|
}
|
|
|
|
export interface TokenInfo {
|
|
user_id: number;
|
|
creation_time: Date;
|
|
expiry_time: Date;
|
|
}
|
|
|
|
class LoginError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
class UserLoginError extends LoginError {
|
|
user_id: number;
|
|
|
|
constructor(user_id: number, message: string) {
|
|
super(message);
|
|
this.user_id = user_id;
|
|
}
|
|
}
|
|
|
|
|
|
const TOKEN_LENGTH = 256;
|
|
|
|
const active_users = new Map<number, UserSession>();
|
|
const active_session_tokens = new Map<string, TokenInfo>();
|
|
|
|
|
|
function issue_access_token_for_user(user: User, expiry_date: Date): string {
|
|
|
|
let user_session = active_users.get(user.id);
|
|
if (user_session === undefined) {
|
|
user_session = {
|
|
user: user,
|
|
last_active: new Date()
|
|
} as UserSession;
|
|
|
|
active_users.set(user.id, user_session);
|
|
}
|
|
|
|
/* | Token (256) | Expiry date as getTime | */
|
|
const token_info: TokenInfo = {
|
|
user_id: user.id,
|
|
creation_time: new Date(),
|
|
expiry_time: expiry_date
|
|
}
|
|
const session_token = Buffer.concat(
|
|
[ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3),
|
|
Buffer.from(token_info.creation_time.getTime().toFixed(0)) ]
|
|
).toString("base64");
|
|
|
|
if (active_session_tokens.has(session_token)) {
|
|
throw new UserLoginError(user.id, "Tried issuing a duplicate session token.");
|
|
}
|
|
|
|
active_session_tokens.set(session_token, token_info);
|
|
|
|
return session_token;
|
|
}
|
|
|
|
function get_user_by_access_token(token: string): User | null {
|
|
|
|
const token_info = active_session_tokens.get(token);
|
|
|
|
if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) {
|
|
return null;
|
|
}
|
|
|
|
const user = active_users.get(token_info.user_id);
|
|
|
|
if (user === undefined) {
|
|
return null;
|
|
}
|
|
|
|
user.last_active = new Date();
|
|
|
|
return user.user;
|
|
}
|
|
|
|
function logout_user_session(token: string): boolean {
|
|
const token_info = active_session_tokens.get(token);
|
|
|
|
if (!token_info) {
|
|
console.log("this shouldn't happen.");
|
|
return false;
|
|
}
|
|
|
|
token_info.expiry_time = new Date(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
async function __clean_session_store() {
|
|
console.log("cleaning sessions");
|
|
|
|
const active_users_session = new Set<number>();
|
|
active_session_tokens.forEach((token_info, token, token_map) => {
|
|
if (token_info.expiry_time.getTime() < Date.now()) {
|
|
token_map.delete(token);
|
|
} else {
|
|
active_users_session.add(token_info.user_id);
|
|
}
|
|
});
|
|
|
|
active_users.forEach((user_info, user_id, user_map) => {
|
|
if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) {
|
|
user_map.delete(user_id)
|
|
}
|
|
});
|
|
}
|
|
|
|
export default class SessionStore {
|
|
static issue_access_token_for_user = issue_access_token_for_user;
|
|
static get_user_by_access_token = get_user_by_access_token;
|
|
static logout_user_session = logout_user_session;
|
|
}
|
|
|
|
setInterval(__clean_session_store, 15*60*1000);
|