mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Merge pull request #266 from HeyPuter/eric/session-updates
session management
This commit is contained in:
commit
8135e076c2
@ -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
|
||||
|
@ -17,35 +17,26 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"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
|
||||
|
@ -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;
|
||||
|
23
packages/backend/src/routers/auth/list-sessions.js
Normal file
23
packages/backend/src/routers/auth/list-sessions.js
Normal file
@ -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);
|
||||
});
|
33
packages/backend/src/routers/auth/revoke-session.js
Normal file
33
packages/backend/src/routers/auth/revoke-session.js
Normal file
@ -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 });
|
||||
});
|
@ -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, {
|
||||
|
@ -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
|
||||
//-------------------------------------------------------------
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -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'))
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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}`);
|
||||
|
@ -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
|
||||
);
|
@ -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 += `</div>`;
|
||||
h += `</div>`;
|
||||
|
||||
// session manager
|
||||
h += `<div class="settings-card">`;
|
||||
h += `<strong>${i18n('sessions')}</strong>`;
|
||||
h += `<div style="flex-grow:1;">`;
|
||||
h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
|
||||
h += `</div>`;
|
||||
h += `</div>`;
|
||||
|
||||
h += `</div>`;
|
||||
|
||||
// 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');
|
||||
|
148
src/UI/UIWindowManageSessions.js
Normal file
148
src/UI/UIWindowManageSessions.js
Normal file
@ -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;
|
||||
// '<pre>' +
|
||||
// JSON.stringify(session, null, 2) +
|
||||
// '</pre>';
|
||||
|
||||
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;
|
@ -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;
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user