diff --git a/packages/backend/src/config.js b/packages/backend/src/config.js
index 580a5ca7e..55b13fadc 100644
--- a/packages/backend/src/config.js
+++ b/packages/backend/src/config.js
@@ -97,6 +97,10 @@ if (config.server_id) {
config.contact_email = 'hey@' + config.domain;
+// TODO: default value will be changed to false in a future release;
+// details to follow in a future announcement.
+config.legacy_token_migrate = true;
+
module.exports = config;
// NEW_CONFIG_LOADING
diff --git a/packages/backend/src/middleware/auth.js b/packages/backend/src/middleware/auth.js
index a231d8b9b..0c4305578 100644
--- a/packages/backend/src/middleware/auth.js
+++ b/packages/backend/src/middleware/auth.js
@@ -17,35 +17,26 @@
* along with this program. If not, see .
*/
"use strict"
+const APIError = require('../api/APIError');
const {jwt_auth} = require('../helpers');
+const { UserActorType } = require('../services/auth/Actor');
const { DB_WRITE } = require('../services/database/consts');
const { Context } = require('../util/context');
+const auth2 = require('./auth2');
const auth = async (req, res, next)=>{
+ let auth2_ok = false;
try{
- let auth_res = await jwt_auth(req);
+ // Delegate to new middleware
+ await auth2(req, res, () => { auth2_ok = true; });
+ if ( ! auth2_ok ) return;
- // is account suspended?
- if(auth_res.user.suspended)
- return res.status(401).send({error: 'Account suspended'});
-
- // successful auth
- req.user = auth_res.user;
- req.token = auth_res.token;
-
- // let's add it to the context too
- try {
- const x = Context.get();
- x.set('user', req.user);
- } catch (e) {
- console.error(e);
+ // Everything using the old reference to the auth middleware
+ // should only allow session tokens
+ if ( ! (req.actor.type instanceof UserActorType) ) {
+ throw APIError.create('forbidden');
}
- // record as daily active users
- const db = req.services.get('database').get(DB_WRITE, 'auth');
- db.write('UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1', [req.user.id]);
-
- // go to next
next();
}
// auth failed
diff --git a/packages/backend/src/middleware/auth2.js b/packages/backend/src/middleware/auth2.js
index d9eaa2f33..bfc69a98c 100644
--- a/packages/backend/src/middleware/auth2.js
+++ b/packages/backend/src/middleware/auth2.js
@@ -18,8 +18,24 @@
*/
const APIError = require("../api/APIError");
const config = require("../config");
+const { UserActorType } = require("../services/auth/Actor");
+const { LegacyTokenError } = require("../services/auth/AuthService");
const { Context } = require("../util/context");
+// The "/whoami" endpoint is a special case where we want to allow
+// a legacy token to be used for authentication. The "/whoami"
+// endpoint will then return a new token for further requests.
+//
+const is_whoami = (req) => {
+ if ( ! config.legacy_token_migrate ) return;
+
+ if ( req.path !== '/whoami' ) return;
+
+ // const subdomain = req.subdomains[res.subdomains.length - 1];
+ // if ( subdomain !== 'api' ) return;
+ return true;
+}
+
// TODO: Allow auth middleware to be used without requiring
// authentication. This will allow us to use the auth middleware
// in endpoints that do not require authentication, but can
@@ -70,6 +86,26 @@ const auth2 = async (req, res, next) => {
e.write(res);
return;
}
+ if ( e instanceof LegacyTokenError && is_whoami(req) ) {
+ const new_info = await svc_auth.check_session(token, {
+ req,
+ from_upgrade: true,
+ })
+ context.set('actor', new_info.actor);
+ context.set('user', new_info.user);
+ req.new_token = new_info.token;
+ req.token = new_info.token;
+ req.user = new_info.user;
+ req.actor = new_info.actor;
+
+ res.cookie(config.cookie_name, new_info.token, {
+ sameSite: 'none',
+ secure: true,
+ httpOnly: true,
+ });
+ next();
+ return;
+ }
const re = APIError.create('token_auth_failed');
re.write(res);
return;
diff --git a/packages/backend/src/routers/auth/list-sessions.js b/packages/backend/src/routers/auth/list-sessions.js
new file mode 100644
index 000000000..a163c78c7
--- /dev/null
+++ b/packages/backend/src/routers/auth/list-sessions.js
@@ -0,0 +1,23 @@
+const eggspress = require("../../api/eggspress");
+const { UserActorType } = require("../../services/auth/Actor");
+const { Context } = require("../../util/context");
+
+module.exports = eggspress('/auth/list-sessions', {
+ subdomain: 'api',
+ auth2: true,
+ allowedMethods: ['GET'],
+}, async (req, res, next) => {
+ const x = Context.get();
+ const svc_auth = x.get('services').get('auth');
+
+ // Only users can list their own sessions
+ // apps, access tokens, etc should NEVER access this
+ const actor = x.get('actor');
+ if ( ! (actor.type instanceof UserActorType) ) {
+ throw APIError.create('forbidden');
+ }
+
+ const sessions = await svc_auth.list_sessions(actor);
+
+ res.json(sessions);
+});
diff --git a/packages/backend/src/routers/auth/revoke-session.js b/packages/backend/src/routers/auth/revoke-session.js
new file mode 100644
index 000000000..5604066c4
--- /dev/null
+++ b/packages/backend/src/routers/auth/revoke-session.js
@@ -0,0 +1,33 @@
+const APIError = require("../../api/APIError");
+const eggspress = require("../../api/eggspress");
+const { UserActorType } = require("../../services/auth/Actor");
+const { Context } = require("../../util/context");
+
+module.exports = eggspress('/auth/revoke-session', {
+ subdomain: 'api',
+ auth2: true,
+ allowedMethods: ['POST'],
+}, async (req, res, next) => {
+ const x = Context.get();
+ const svc_auth = x.get('services').get('auth');
+
+ // Only users can list their own sessions
+ // apps, access tokens, etc should NEVER access this
+ const actor = x.get('actor');
+ if ( ! (actor.type instanceof UserActorType) ) {
+ throw APIError.create('forbidden');
+ }
+
+ // Ensure valid UUID
+ if ( ! req.body.uuid || typeof req.body.uuid !== 'string' ) {
+ throw APIError.create('field_invalid', null, {
+ key: 'uuid',
+ expected: 'string'
+ });
+ }
+
+ const sessions = await svc_auth.revoke_session(
+ actor, req.body.uuid);
+
+ res.json({ sessions });
+});
diff --git a/packages/backend/src/routers/login.js b/packages/backend/src/routers/login.js
index 795497900..3a75c1990 100644
--- a/packages/backend/src/routers/login.js
+++ b/packages/backend/src/routers/login.js
@@ -89,7 +89,8 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
return res.status(400).send('Incorrect password.')
// check password
if(await bcrypt.compare(req.body.password, user.password)){
- const token = await jwt.sign({uuid: user.uuid}, config.jwt_secret)
+ const svc_auth = req.services.get('auth');
+ const token = await svc_auth.create_session_token(user, { req });
//set cookie
// res.cookie(config.cookie_name, token);
res.cookie(config.cookie_name, token, {
diff --git a/packages/backend/src/routers/signup.js b/packages/backend/src/routers/signup.js
index ca45d9d7a..91a9cd607 100644
--- a/packages/backend/src/routers/signup.js
+++ b/packages/backend/src/routers/signup.js
@@ -52,6 +52,7 @@ module.exports = eggspress(['/signup'], {
const validator = require('validator')
let uuid_user;
+ const svc_auth = Context.get('services').get('auth');
const svc_authAudit = Context.get('services').get('auth-audit');
svc_authAudit.record({
requester: Context.get('requester'),
@@ -67,9 +68,11 @@ module.exports = eggspress(['/signup'], {
// check if user is already logged in
if ( req.body.is_temp && req.cookies[config.cookie_name] ) {
- const token = req.cookies[config.cookie_name];
- const decoded = await jwt.verify(token, config.jwt_secret);
- const user = await get_user({ uuid: decoded.uuid });
+ const { user, token } = await svc_auth.check_session(
+ req.cookies[config.cookie_name]
+ );
+ // const decoded = await jwt.verify(token, config.jwt_secret);
+ // const user = await get_user({ uuid: decoded.uuid });
if ( user ) {
return res.send({
token: token,
@@ -233,17 +236,22 @@ module.exports = eggspress(['/signup'], {
db.write('UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1', [pseudo_user.id]);
invalidate_cached_user_by_id(pseudo_user.id);
}
- // create token for login
- const token = await jwt.sign({uuid: user_uuid}, config.jwt_secret);
// user id
// todo if pseudo user, assign directly no need to do another DB lookup
const user_id = (pseudo_user === undefined) ? insert_res.insertId : pseudo_user.id;
+
const [user] = await db.read(
'SELECT * FROM `user` WHERE `id` = ? LIMIT 1',
[user_id]
);
+ // create token for login
+ const token = await svc_auth.create_session_token(user, {
+ req,
+ });
+ // jwt.sign({uuid: user_uuid}, config.jwt_secret);
+
//-------------------------------------------------------------
// email confirmation
//-------------------------------------------------------------
diff --git a/packages/backend/src/routers/whoami.js b/packages/backend/src/routers/whoami.js
index e02661237..61ce36de3 100644
--- a/packages/backend/src/routers/whoami.js
+++ b/packages/backend/src/routers/whoami.js
@@ -54,6 +54,7 @@ const WHOAMI_GET = eggspress('/whoami', {
is_temp: (req.user.password === null && req.user.email === null),
taskbar_items: await get_taskbar_items(req.user),
referral_code: req.user.referral_code,
+ ...(req.new_token ? { token: req.token } : {})
};
if ( ! is_user ) {
@@ -65,6 +66,7 @@ const WHOAMI_GET = eggspress('/whoami', {
delete details.desktop_bg_color;
delete details.desktop_bg_fit;
delete details.taskbar_items;
+ delete details.token;
}
res.send(details);
@@ -76,8 +78,19 @@ const WHOAMI_GET = eggspress('/whoami', {
const WHOAMI_POST = new express.Router();
WHOAMI_POST.post('/whoami', auth, fs, express.json(), async (req, response, next)=>{
// check subdomain
- if(require('../helpers').subdomain(req) !== 'api')
- next();
+ if(require('../helpers').subdomain(req) !== 'api') {
+ return;
+ }
+
+ const actor = Context.get('actor');
+ if ( ! actor ) {
+ throw Error('actor not found in context');
+ }
+
+ const is_user = actor.type instanceof UserActorType;
+ if ( ! is_user ) {
+ throw Error('actor is not a user');
+ }
let desktop_items = [];
diff --git a/packages/backend/src/services/PuterAPIService.js b/packages/backend/src/services/PuterAPIService.js
index d8d33f824..3ccd29385 100644
--- a/packages/backend/src/services/PuterAPIService.js
+++ b/packages/backend/src/services/PuterAPIService.js
@@ -32,6 +32,8 @@ class PuterAPIService extends BaseService {
app.use(require('../routers/auth/grant-user-user'));
app.use(require('../routers/auth/revoke-user-user'));
app.use(require('../routers/auth/list-permissions'))
+ app.use(require('../routers/auth/list-sessions'))
+ app.use(require('../routers/auth/revoke-session'))
app.use(require('../routers/auth/check-app'))
app.use(require('../routers/auth/app-uid-from-origin'))
app.use(require('../routers/auth/create-access-token'))
diff --git a/packages/backend/src/services/auth/AuthService.js b/packages/backend/src/services/auth/AuthService.js
index 292186225..c1cdc9e5f 100644
--- a/packages/backend/src/services/auth/AuthService.js
+++ b/packages/backend/src/services/auth/AuthService.js
@@ -25,6 +25,8 @@ const { DB_WRITE } = require("../database/consts");
const APP_ORIGIN_UUID_NAMESPACE = '33de3768-8ee0-43e9-9e73-db192b97a5d8';
+const LegacyTokenError = class extends Error {};
+
class AuthService extends BaseService {
static MODULES = {
jwt: require('jsonwebtoken'),
@@ -34,6 +36,8 @@ class AuthService extends BaseService {
async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'auth');
+
+ this.sessions = {};
}
async authenticate_from_token (token) {
@@ -43,6 +47,7 @@ class AuthService extends BaseService {
);
if ( ! decoded.hasOwnProperty('type') ) {
+ throw new LegacyTokenError();
const user = await this.db.requireRead(
"SELECT * FROM `user` WHERE `uuid` = ? LIMIT 1",
[decoded.uuid],
@@ -66,6 +71,26 @@ class AuthService extends BaseService {
});
}
+ if ( decoded.type === 'session' ) {
+ const session = await this.get_session_(decoded.uuid);
+
+ if ( ! session ) {
+ throw APIError.create('token_auth_failed');
+ }
+
+ const user = await get_user({ uuid: decoded.user_uid });
+
+ const actor_type = new UserActorType({
+ user,
+ session: session.uuid,
+ });
+
+ return new Actor({
+ user_uid: decoded.user_uid,
+ type: actor_type,
+ });
+ }
+
if ( decoded.type === 'app-under-user' ) {
const user = await get_user({ uuid: decoded.user_uid });
if ( ! user ) {
@@ -149,6 +174,141 @@ class AuthService extends BaseService {
return token;
}
+ async create_session_ (user, meta = {}) {
+ this.log.info(`CREATING SESSION`);
+
+ if ( meta.req ) {
+ const req = meta.req;
+ delete meta.req;
+
+ const ip = this.global_config.fowarded
+ ? req.headers['x-forwarded-for'] ||
+ req.connection.remoteAddress
+ : req.connection.remoteAddress
+ ;
+
+ meta.ip = ip;
+
+ meta.server = this.global_config.server_id;
+
+ if ( req.headers['user-agent'] ) {
+ meta.user_agent = req.headers['user-agent'];
+ }
+
+ if ( req.headers['referer'] ) {
+ meta.referer = req.headers['referer'];
+ }
+
+ if ( req.headers['origin'] ) {
+ const origin = this._origin_from_url(req.headers['origin']);
+ if ( origin ) {
+ meta.origin = origin;
+ }
+ }
+
+ if ( req.headers['host'] ) {
+ const host = this._origin_from_url(req.headers['host']);
+ if ( host ) {
+ meta.host = host;
+ }
+ }
+ }
+
+ 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;
+ }
+
+ 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],
+ );
+
+ session.meta = JSON.parse(session.meta ?? {});
+
+ return session;
+ }
+
+ async create_session_token (user, meta) {
+ const session = await this.create_session_(user, meta);
+
+ const token = this.modules.jwt.sign({
+ type: 'session',
+ version: '0.0.0',
+ uuid: session.uuid,
+ meta: session.meta,
+ user_uid: user.uuid,
+ }, this.global_config.jwt_secret);
+
+ return { session, token };
+ }
+
+ async check_session (cur_token, meta) {
+ const decoded = this.modules.jwt.verify(
+ cur_token, this.global_config.jwt_secret
+ );
+
+ console.log('\x1B[36;1mDECODED SESSION', decoded);
+
+ if ( decoded.type && decoded.type !== 'session' ) {
+ return {};
+ }
+
+ const is_legacy = ! decoded.type;
+
+ const user = await get_user({ uuid:
+ is_legacy ? decoded.uuid : decoded.user_uid
+ });
+ if ( ! user ) {
+ return {};
+ }
+
+ if ( ! is_legacy ) {
+ // Ensure session exists
+ const session = await this.get_session_(decoded.uuid);
+ if ( ! session ) {
+ return {};
+ }
+
+ // Return the session
+ return { user, token: cur_token };
+ }
+
+ this.log.info(`UPGRADING SESSION`);
+
+ // Upgrade legacy token
+ // TODO: phase this out
+ const { session, token } = await this.create_session_token(user, meta);
+
+ const actor_type = new UserActorType({
+ user,
+ session,
+ });
+
+ const actor = new Actor({
+ user_uid: user.uuid,
+ type: actor_type,
+ });
+
+ return { actor, user, token };
+ }
+
async create_access_token (authorizer, permissions) {
const jwt_obj = {};
const authorizer_obj = {};
@@ -206,6 +366,32 @@ class AuthService extends BaseService {
return jwt;
}
+ async list_sessions (actor) {
+ // 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(
+ 'SELECT uuid, meta FROM `sessions` WHERE `user_id` = ?',
+ [actor.type.user.id],
+ );
+
+ sessions.forEach(session => {
+ if ( session.uuid === actor.type.session ) {
+ session.current = true;
+ }
+ session.meta = JSON.parse(session.meta ?? {});
+ });
+
+ return sessions;
+ }
+
+ async revoke_session (actor, uuid) {
+ delete this.sessions[uuid];
+ await this.db.write(
+ `DELETE FROM sessions WHERE uuid = ? AND user_id = ?`,
+ [uuid, actor.type.user.id]
+ );
+ }
+
async get_user_app_token_from_origin (origin) {
origin = this._origin_from_url(origin);
const app_uid = await this._app_uid_from_origin(origin);
@@ -264,4 +450,5 @@ class AuthService extends BaseService {
module.exports = {
AuthService,
+ LegacyTokenError,
};
diff --git a/packages/backend/src/services/database/SqliteDatabaseAccessService.js b/packages/backend/src/services/database/SqliteDatabaseAccessService.js
index 90049c9f7..c54d29efe 100644
--- a/packages/backend/src/services/database/SqliteDatabaseAccessService.js
+++ b/packages/backend/src/services/database/SqliteDatabaseAccessService.js
@@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
this.db = new Database(this.config.path);
// Database upgrade logic
- const TARGET_VERSION = 1;
+ const TARGET_VERSION = 2;
if ( do_setup ) {
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
@@ -50,6 +50,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
'0001_create-tables.sql',
'0002_add-default-apps.sql',
'0003_user-permissions.sql',
+ '0004_sessions.sql',
].map(p => path_.join(__dirname, 'sqlite_setup', p));
const fs = require('fs');
for ( const filename of sql_files ) {
@@ -70,6 +71,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
upgrade_files.push('0003_user-permissions.sql');
}
+ if ( user_version <= 1 ) {
+ upgrade_files.push('0004_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}`);
diff --git a/packages/backend/src/services/database/sqlite_setup/0004_sessions.sql b/packages/backend/src/services/database/sqlite_setup/0004_sessions.sql
new file mode 100644
index 000000000..a5a986acf
--- /dev/null
+++ b/packages/backend/src/services/database/sqlite_setup/0004_sessions.sql
@@ -0,0 +1,7 @@
+CREATE TABLE `sessions` (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "user_id" INTEGER NOT NULL,
+ "uuid" TEXT NOT NULL,
+ "meta" JSON DEFAULT NULL,
+ FOREIGN KEY("user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
diff --git a/src/UI/Settings/UIWindowSettings.js b/src/UI/Settings/UIWindowSettings.js
index ff07072e0..d7ecab014 100644
--- a/src/UI/Settings/UIWindowSettings.js
+++ b/src/UI/Settings/UIWindowSettings.js
@@ -26,6 +26,7 @@ import changeLanguage from "../../i18n/i18nChangeLanguage.js"
import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
import UITabAbout from './UITabAbout.js';
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
+import UIWindowManageSessions from '../UIWindowManageSessions.js';
async function UIWindowSettings(options){
return new Promise(async (resolve) => {
@@ -113,6 +114,14 @@ async function UIWindowSettings(options){
h += ``;
h += ``;
+ // session manager
+ h += `
`;
+ h += `
${i18n('sessions')}`;
+ h += `
`;
+ h += ``;
+ h += `
`;
+ h += `
`;
+
h += ``;
// Personalization
@@ -342,6 +351,10 @@ async function UIWindowSettings(options){
UIWindowThemeDialog();
})
+ $(el_window).find('.manage-sessions').on('click', function (e) {
+ UIWindowManageSessions();
+ })
+
$(el_window).on('click', '.settings-sidebar-item', function(){
const $this = $(this);
const settings = $this.attr('data-settings');
diff --git a/src/UI/UIWindowManageSessions.js b/src/UI/UIWindowManageSessions.js
new file mode 100644
index 000000000..561597fb5
--- /dev/null
+++ b/src/UI/UIWindowManageSessions.js
@@ -0,0 +1,148 @@
+import UIAlert from "./UIAlert.js";
+import UIWindow from "./UIWindow.js";
+
+const UIWindowManageSessions = async function UIWindowManageSessions () {
+ const services = globalThis.services;
+
+ const w = await UIWindow({
+ title: i18n('ui_manage_sessions'),
+ icon: null,
+ uid: null,
+ is_dir: false,
+ message: 'message',
+ // body_icon: options.body_icon,
+ // backdrop: options.backdrop ?? false,
+ is_droppable: false,
+ has_head: true,
+ selectable_body: false,
+ draggable_body: true,
+ allow_context_menu: false,
+ window_class: 'window-session-manager',
+ dominant: true,
+ body_content: '',
+ // width: 600,
+ // parent_uuid: options.parent_uuid,
+ // ...options.window_options,
+ });
+
+ const SessionWidget = ({ session }) => {
+ const el = document.createElement('div');
+ el.classList.add('session-widget');
+ el.dataset.uuid = session.uuid;
+ // '' +
+ // JSON.stringify(session, null, 2) +
+ // '
';
+
+ const el_uuid = document.createElement('div');
+ el_uuid.textContent = session.uuid;
+ el.appendChild(el_uuid);
+ el_uuid.classList.add('session-widget-uuid');
+
+ const el_meta = document.createElement('div');
+ el_meta.classList.add('session-widget-meta');
+ for ( const key in session.meta ) {
+ const el_entry = document.createElement('div');
+ el_entry.classList.add('session-widget-meta-entry');
+
+ const el_key = document.createElement('div');
+ el_key.textContent = key;
+ el_key.classList.add('session-widget-meta-key');
+ el_entry.appendChild(el_key);
+
+ const el_value = document.createElement('div');
+ el_value.textContent = session.meta[key];
+ el_value.classList.add('session-widget-meta-value');
+ el_entry.appendChild(el_value);
+
+ el_meta.appendChild(el_entry);
+ }
+ el.appendChild(el_meta);
+
+ const el_actions = document.createElement('div');
+ el_actions.classList.add('session-widget-actions');
+
+ const el_btn_revoke = document.createElement('button');
+ el_btn_revoke.textContent = i18n('ui_revoke');
+ el_btn_revoke.classList.add('button', 'button-danger');
+ el_btn_revoke.addEventListener('click', async () => {
+ try{
+ const alert_resp = await UIAlert({
+ message: i18n('confirm_session_revoke'),
+ buttons:[
+ {
+ label: i18n('yes'),
+ value: 'yes',
+ type: 'primary',
+ },
+ {
+ label: i18n('cancel')
+ },
+ ]
+ });
+
+ if ( alert_resp !== 'yes' ) {
+ return;
+ }
+
+ const resp = await fetch(`${api_origin}/auth/revoke-session`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ uuid: session.uuid,
+ }),
+ });
+ if ( resp.ok ) {
+ el.remove();
+ return;
+ }
+ UIAlert({ message: await resp.text() }).appendTo(w_body);
+ } catch ( e ) {
+ UIAlert({ message: e.toString() }).appendTo(w_body);
+ }
+ });
+ el_actions.appendChild(el_btn_revoke);
+ el.appendChild(el_actions);
+
+ return {
+ appendTo (parent) {
+ parent.appendChild(el);
+ return this;
+ }
+ };
+ };
+
+ const reload_sessions = async () => {
+ const resp = await fetch(`${api_origin}/auth/list-sessions`, {
+ method: 'GET',
+ });
+
+ const sessions = await resp.json();
+
+ for ( const el of w_body.querySelectorAll('.session-widget') ) {
+ if ( !sessions.find(s => s.uuid === el.dataset.uuid) ) {
+ el.remove();
+ }
+ }
+
+ for ( const session of sessions ) {
+ if ( w.querySelector(`.session-widget[data-uuid="${session.uuid}"]`) ) {
+ continue;
+ }
+ SessionWidget({ session }).appendTo(w_body);
+ }
+ };
+
+ const w_body = w.querySelector('.window-body');
+
+ w_body.classList.add('session-manager-list');
+
+ reload_sessions();
+ const interval = setInterval(reload_sessions, 8000);
+ w.on_close = () => {
+ clearInterval(interval);
+ }
+};
+
+export default UIWindowManageSessions;
diff --git a/src/css/style.css b/src/css/style.css
index 67dfeb708..f0a50fa90 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -3712,3 +3712,61 @@ label {
background: #04AA6D;
cursor: pointer;
}
+
+.session-manager-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 10px;
+ box-sizing: border-box;
+ height: 100% !important;
+}
+
+.session-widget {
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+ gap: 4px;
+}
+
+.session-widget-uuid {
+ font-size: 12px;
+ font-weight: 600;
+ color: #9c185b;
+}
+
+.session-widget-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-height: 100px;
+ overflow-y: scroll;
+}
+
+.session-widget-meta-entry {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.session-widget-meta-key {
+ font-size: 12px;
+ color: #666;
+ flex-basis: 40%;
+ flex-shrink: 0;
+}
+
+.session-widget-meta-value {
+ font-size: 12px;
+ color: #666;
+ flex-grow: 1;
+}
+
+.session-widget-actions {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ justify-content: flex-end;
+}
\ No newline at end of file
diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js
index 5c707dcb0..8e2c5c3a3 100644
--- a/src/i18n/translations/en.js
+++ b/src/i18n/translations/en.js
@@ -51,6 +51,7 @@ const en = {
confirm_new_password: "Confirm New Password",
confirm_delete_user: "Are you sure you want to delete your account? All your files and data will be permanently deleted. This action cannot be undone.",
confirm_delete_user_title: "Delete Account?",
+ confirm_session_revoke: "Are you sure you want to revoke this session?",
contact_us: "Contact Us",
contain: 'Contain',
continue: "Continue",
@@ -112,6 +113,7 @@ const en = {
log_in: "Log In",
log_into_another_account_anyway: 'Log into another account anyway',
log_out: 'Log Out',
+ manage_sessions: "Manage Sessions",
move: 'Move',
moving: "Moving",
my_websites: "My Websites",
@@ -179,6 +181,7 @@ const en = {
select: "Select",
selected: 'selected',
select_color: 'Select color…',
+ sessions: "Sessions",
send: "Send",
send_password_recovery_email: "Send Password Recovery Email",
session_saved: "Thank you for creating an account. This session has been saved.",
@@ -206,6 +209,8 @@ const en = {
type: 'Type',
type_confirm_to_delete_account: "Type 'confirm' to delete your account.",
ui_colors: "UI Colors",
+ ui_manage_sessions: "Session Manager",
+ ui_revoke: "Revoke",
undo: 'Undo',
unlimited: 'Unlimited',
unzip: "Unzip",
diff --git a/src/initgui.js b/src/initgui.js
index dfb1e3b04..1cc6ac4a3 100644
--- a/src/initgui.js
+++ b/src/initgui.js
@@ -366,7 +366,7 @@ window.initgui = async function(){
}
while(!is_verified)
}
- update_auth_data(window.auth_token, whoami);
+ update_auth_data(whoami.token || window.auth_token, whoami);
// -------------------------------------------------------------------------------------
// Load desktop, only if we're not embedded in a popup