mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-26 16:07:13 +00:00
Improve sessions
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user