Improve sessions

This commit is contained in:
KernelDeimos
2024-04-16 18:57:02 -04:00
parent 653898b463
commit 19c49db538
6 changed files with 187 additions and 34 deletions
+3
View File
@@ -189,6 +189,9 @@ const install = async ({ services, app }) => {
const { PuterVersionService } = require('./services/PuterVersionService');
services.registerService('puter-version', PuterVersionService);
const { SessionService } = require('./services/SessionService');
services.registerService('session', SessionService);
}
const install_legacy = async ({ services }) => {
+10
View File
@@ -31,6 +31,16 @@ router.post('/logout', auth, express.json(), async (req, res, next)=>{
next();
// delete cookie
res.clearCookie(config.cookie_name);
// delete session
(async () => {
if ( ! req.token ) return;
try {
const svc_auth = req.services.get('auth');
await svc_auth.remove_session_by_token(req.token);
} catch (e) {
console.log(e);
}
})()
//---------------------------------------------------------
// DANGER ZONE: delete temp user and all its data
//---------------------------------------------------------
@@ -0,0 +1,135 @@
const { asyncSafeSetInterval } = require("../util/promise");
const { MINUTE, SECOND } = require("../util/time");
const BaseService = require("./BaseService");
const { DB_WRITE } = require("./database/consts");
/**
* This service is responsible for updating session activity
* timestamps and maintaining the number of active sessions.
*/
class SessionService extends BaseService {
static MODULES = {
// uuidv5: require('uuid').v5,
uuidv4: require('uuid').v4,
}
_construct () {
this.sessions = {};
}
async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'session');
(async () => {
// TODO: change to 5 minutes or configured value
asyncSafeSetInterval(async () => {
await this._update_sessions();
}, 1000);
// }, 2 * MINUTE);
})();
}
async create_session (user, meta) {
const unix_ts = Math.floor(Date.now() / 1000);
meta = {
// clone
...(meta || {}),
};
meta.created = new Date().toISOString();
meta.created_unix = unix_ts;
const uuid = this.modules.uuidv4();
await this.db.write(
'INSERT INTO `sessions` ' +
'(`uuid`, `user_id`, `meta`, `last_activity`, `created_at`) ' +
'VALUES (?, ?, ?, ?, ?)',
[uuid, user.id, JSON.stringify(meta), unix_ts, unix_ts],
);
const session = {
last_touch: Date.now(),
last_store: Date.now(),
uuid,
user_uid: user.uuid,
meta,
};
this.sessions[uuid] = session;
return session;
}
async get_session_ (uuid) {
let session = this.sessions[uuid];
if ( session ) return session;
;[session] = await this.db.read(
"SELECT * FROM `sessions` WHERE `uuid` = ? LIMIT 1",
[uuid],
);
if ( ! session ) return;
session.last_store = Date.now();
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
})();
this.sessions[uuid] = session;
return session;
}
async get_session (uuid) {
const session = await this.get_session_(uuid);
if ( session ) {
session.last_touch = Date.now();
session.meta.last_activity = (new Date()).toISOString();
}
return this.remove_internal_values_(session);
}
remove_internal_values_ (session) {
const copy = {
...session,
};
delete copy.last_touch;
delete copy.last_store;
delete copy.user_id;
return copy;
}
get_user_sessions (user) {
const sessions = [];
for ( const session of Object.values(this.sessions) ) {
if ( session.user_id === user.id ) {
sessions.push(session);
}
}
return sessions.map(this.remove_internal_values_.bind(this));
}
remove_session (uuid) {
delete this.sessions[uuid];
return this.db.write(
'DELETE FROM `sessions` WHERE `uuid` = ?',
[uuid],
);
}
async _update_sessions () {
this.log.tick('UPDATING SESSIONS');
const now = Date.now();
const keys = Object.keys(this.sessions);
for ( const key of keys ) {
const session = this.sessions[key];
// if ( now - session.last_store > 5 * MINUTE ) {
if ( now - session.last_store > 20 * SECOND ) {
this.log.debug('storing session meta: ' + session.uuid);
const unix_ts = Math.floor(now / 1000);
await this.db.write(
'UPDATE `sessions` ' +
'SET `meta` = ?, `last_activity` = ? ' +
'WHERE `uuid` = ?',
[JSON.stringify(session.meta), unix_ts, session.uuid],
);
session.last_store = now;
}
}
}
}
module.exports = { SessionService };
@@ -36,6 +36,7 @@ class AuthService extends BaseService {
async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'auth');
this.svc_session = await this.services.get('session');
this.sessions = {};
}
@@ -214,40 +215,12 @@ class AuthService extends BaseService {
}
}
meta.created = new Date().toISOString();
meta.created_unix = Math.floor(Date.now() / 1000);
const uuid = this.modules.uuidv4();
await this.db.write(
'INSERT INTO `sessions` ' +
'(`uuid`, `user_id`, `meta`) ' +
'VALUES (?, ?, ?)',
[uuid, user.id, JSON.stringify(meta)],
);
const session = { uuid, user_uid: user.uuid, meta };
this.sessions[uuid] = session;
return session;
return await this.svc_session.create_session(user, meta);
}
async get_session_ (uuid) {
this.log.info(`USING SESSION`);
if ( this.sessions[uuid] ) {
return this.sessions[uuid];
}
const [session] = await this.db.read(
"SELECT * FROM `sessions` WHERE `uuid` = ? LIMIT 1",
[uuid],
);
if ( ! session ) return;
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
})();
return session;
return await this.svc_session.get_session(uuid);
}
async create_session_token (user, meta) {
@@ -314,6 +287,18 @@ class AuthService extends BaseService {
return { actor, user, token };
}
async remove_session_by_token (token) {
const decoded = this.modules.jwt.verify(
token, this.global_config.jwt_secret
);
if ( decoded.type !== 'session' ) {
return;
}
await this.svc_session.remove_session(decoded.uuid);
}
async create_access_token (authorizer, permissions) {
const jwt_obj = {};
const authorizer_obj = {};
@@ -372,14 +357,26 @@ class AuthService extends BaseService {
}
async list_sessions (actor) {
const seen = new Set();
const sessions = [];
const cache_sessions = this.svc_session.get_user_sessions(actor.type.user);
for ( const session of cache_sessions ) {
seen.add(session.uuid);
sessions.push(session);
}
// We won't take the cached sessions here because it's
// possible the user has sessions on other servers
const sessions = await this.db.read(
const db_sessions = await this.db.read(
'SELECT uuid, meta FROM `sessions` WHERE `user_id` = ?',
[actor.type.user.id],
);
sessions.forEach(session => {
for ( const session of db_sessions ) {
if ( seen.has(session.uuid) ) {
continue;
}
session.meta = this.db.case({
mysql: () => session.meta,
otherwise: () => JSON.parse(session.meta ?? "{}")
@@ -387,7 +384,8 @@ class AuthService extends BaseService {
if ( session.uuid === actor.type.session ) {
session.current = true;
}
});
sessions.push(session);
};
return sessions;
}
@@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
this.db = new Database(this.config.path);
// Database upgrade logic
const TARGET_VERSION = 4;
const TARGET_VERSION = 5;
if ( do_setup ) {
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
@@ -53,6 +53,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
'0004_sessions.sql',
'0005_background-apps.sql',
'0006_update-apps.sql',
'0007_sessions.sql',
].map(p => path_.join(__dirname, 'sqlite_setup', p));
const fs = require('fs');
for ( const filename of sql_files ) {
@@ -85,6 +86,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
upgrade_files.push('0006_update-apps.sql');
}
if ( user_version <= 4 ) {
upgrade_files.push('0007_sessions.sql');
}
if ( upgrade_files.length > 0 ) {
this.log.noticeme(`Database out of date: ${this.config.path}`);
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
@@ -0,0 +1,2 @@
ALTER TABLE `sessions` ADD COLUMN "created_at" INTEGER DEFAULT 0;
ALTER TABLE `sessions` ADD COLUMN "last_activity" INTEGER DEFAULT 0;