Browse Source

Add session manager ui

KernelDeimos 1 year ago
parent
commit
09bf422686

+ 1 - 1
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, {

+ 3 - 1
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);
 
     //-------------------------------------------------------------

+ 43 - 2
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;

+ 13 - 0
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 += `</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
@@ -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');

+ 148 - 0
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;
+        // '<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;

+ 58 - 0
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;
+}

+ 4 - 0
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",