From 09bf422686572398acdd496e5f2c0e1ba4c3133e Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 11 Apr 2024 00:29:39 -0400 Subject: [PATCH] Add session manager ui --- packages/backend/src/routers/login.js | 2 +- packages/backend/src/routers/signup.js | 4 +- .../backend/src/services/auth/AuthService.js | 45 +++++- src/UI/Settings/UIWindowSettings.js | 13 ++ src/UI/UIWindowManageSessions.js | 148 ++++++++++++++++++ src/css/style.css | 58 +++++++ src/i18n/translations/en.js | 4 + 7 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 src/UI/UIWindowManageSessions.js diff --git a/packages/backend/src/routers/login.js b/packages/backend/src/routers/login.js index 1f5c6c0ca..3a75c1990 100644 --- a/packages/backend/src/routers/login.js +++ b/packages/backend/src/routers/login.js @@ -90,7 +90,7 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res // check password if(await bcrypt.compare(req.body.password, user.password)){ const svc_auth = req.services.get('auth'); - const token = await svc_auth.create_session_token(user); + 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 012fdfb21..91a9cd607 100644 --- a/packages/backend/src/routers/signup.js +++ b/packages/backend/src/routers/signup.js @@ -247,7 +247,9 @@ module.exports = eggspress(['/signup'], { ); // create token for login - const token = await svc_auth.create_session_token(user); + const token = await svc_auth.create_session_token(user, { + req, + }); // jwt.sign({uuid: user_uuid}, config.jwt_secret); //------------------------------------------------------------- diff --git a/packages/backend/src/services/auth/AuthService.js b/packages/backend/src/services/auth/AuthService.js index 0f5380d7d..c00345421 100644 --- a/packages/backend/src/services/auth/AuthService.js +++ b/packages/backend/src/services/auth/AuthService.js @@ -174,6 +174,44 @@ class AuthService extends BaseService { 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; + } + } + } + const uuid = this.modules.uuidv4(); await this.db.write( 'INSERT INTO `sessions` ' + @@ -197,6 +235,8 @@ class AuthService extends BaseService { [uuid], ); + session.meta = JSON.parse(session.meta ?? {}); + return session; } @@ -214,7 +254,7 @@ class AuthService extends BaseService { return token; } - async check_session (cur_token) { + async check_session (cur_token, meta) { const decoded = this.modules.jwt.verify( cur_token, this.global_config.jwt_secret ); @@ -245,7 +285,7 @@ class AuthService extends BaseService { // Upgrade legacy token // TODO: phase this out - const { token } = await this.create_session_token(user); + const { token } = await this.create_session_token(user, meta); return { user, token }; } @@ -318,6 +358,7 @@ class AuthService extends BaseService { if ( session.uuid === actor.type.session ) { session.current = true; } + session.meta = JSON.parse(session.meta ?? {}); }); return sessions; diff --git a/src/UI/Settings/UIWindowSettings.js b/src/UI/Settings/UIWindowSettings.js index 6a8560d43..518b84078 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) => { @@ -111,6 +112,14 @@ async function UIWindowSettings(options){ h += ``; h += ``; + // session manager + h += `
`; + h += `${i18n('sessions')}`; + h += `
`; + h += ``; + h += `
`; + h += `
`; + h += ``; // Personalization @@ -324,6 +333,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 b1b349ea9..299f7f243 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -3703,3 +3703,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 3fed31897..473966097 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", @@ -205,6 +207,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",