PageSourceSearch

https://www.eventeny.com/js/Components/UserSessionsElement.js?v=1790795785

js eventeny.com collected 2026-10-01 10:26:46 UTC 15,181 bytes, 290 lines download raw bytes

1import { WebComponent } from './Base/WebComponent.js';
2
3/**
4 * @class UserSessionsElement
5 * @extends WebComponent
6 *
7 * @classdesc Paginated active-session table with per-row revoke and revoke-all. Works in two modes:
8 *
9 * User mode  (mode="user"):  auto-fetches on connect; reads current-selector attribute
10 *                             to badge the current device as "Current" instead of showing
11 *                             the revoke icon.
12 * Admin mode (mode="admin"): waits for loadForUser(acct_id); no "Current" badge; both
13 *                             single-revoke and revoke-all require an inline confirmation.
14 *
15 * Attributes:
16 *   mode              "user" (default) | "admin"
17 *   current-selector  Refresh token selector for the active device (user mode only)
18 *
19 * Methods:
20 *   loadForUser(acct_id)  Resets state and fetches sessions for the given user (admin mode)
21 */
22class UserSessionsElement extends WebComponent {
23    constructor() {
24        super();
25        this.loadBaseCSS();
26        this.setState({
27            status:        'idle',
28            rows:          [],
29            total:         0,
30            totalPages:    1,
31            page:          1,
32            errorMsg:      '',
33            acct_id:       null,
34            pendingRevoke: null,
35        });
36    }
37
38    get mode()            { return this.getAttribute('mode') || 'user'; }
39    get currentSelector() { return this.getAttribute('current-selector') || ''; }
40
41    connectedCallback() {
42        super.connectedCallback();
43        if (this.mode === 'user') {
44            this._fetch(1);
45        }
46    }
47
48    /**
49     * Resets state for a new user and fetches their sessions.
50     * @param {string|number} acct_id
51     */
52    loadForUser(acct_id) {
53        this.setState({
54            status: 'loading', rows: [], total: 0, totalPages: 1, page: 1,
55            errorMsg: '', acct_id, pendingRevoke: null,
56        });
57        this._fetch(1, acct_id);
58    }
59
60    async _fetch(page, acct_id) {
61        const target = acct_id !== undefined ? acct_id : this.state.acct_id;
62        this.setState({ status: 'loading', page, pendingRevoke: null });
63        try {
64            const formData = new FormData();
65            formData.append('limit', '10');
66            formData.append('page', String(page));
67            if (this.mode === 'admin') {
68                formData.append('post_type', 'get_user_sessions');
69                formData.append('user_acct_id', String(target));
70            } else {
71                formData.append('post_type', 'get_sessions');
72            }
73            const endpoint = this.mode === 'admin'
74                ? '/funcs/admin/test-2017-12-29.php'
75                : '/funcs/common/UserSessionRoute.php';
76            const res  = await fetch(endpoint, { method: 'POST', body: formData });
77            if (!res.ok) throw new Error('Server error (' + res.status + ')');
78            const data = await res.json();
79            if (!data.success) throw new Error(data.err_msg || 'Failed to load sessions');
80            this.setState({
81                status: 'loaded', rows: data.rows, total: data.total,
82                totalPages: data.total_pages, page, errorMsg: '',
83            });
84        } catch(err) {
85            this.setState({ status: 'error', errorMsg: err.message });
86        }
87    }
88
89    async _doRevoke(selector) {
90        const { page, acct_id } = this.state;
91        this.setState({ pendingRevoke: null, status: 'loading' });
92        try {
93            const formData = new FormData();
94            formData.append('selector', selector);
95            if (this.mode === 'admin') {
96                formData.append('post_type', 'revoke_user_session');
97                formData.append('user_acct_id', String(acct_id));
98            } else {
99                formData.append('post_type', 'revoke_session');
100            }
101            const endpoint = this.mode === 'admin'
102                ? '/funcs/admin/test-2017-12-29.php'
103                : '/funcs/common/UserSessionRoute.php';
104            const res  = await fetch(endpoint, { method: 'POST', body: formData });
105            if (!res.ok) throw new Error('Server error (' + res.status + ')');
106            const data = await res.json();
107            if (!data.success) throw new Error(data.err_msg || 'Failed to revoke session');
108            this._fetch(page, acct_id);
109        } catch(err) {
110            this.setState({ status: 'error', errorMsg: err.message });
111        }
112    }
113
114    async _doRevokeAll() {
115        const { acct_id } = this.state;
116        this.setState({ pendingRevoke: null, status: 'loading' });
117        try {
118            const formData = new FormData();
119            if (this.mode === 'admin') {
120                formData.append('post_type', 'revoke_all_user_sessions');
121                formData.append('user_acct_id', String(acct_id));
122            } else {
123                formData.append('post_type', 'revoke_all_sessions');
124            }
125            const endpoint = this.mode === 'admin'
126                ? '/funcs/admin/test-2017-12-29.php'
127                : '/funcs/common/UserSessionRoute.php';
128            const res  = await fetch(endpoint, { method: 'POST', body: formData });
129            if (!res.ok) throw new Error('Server error (' + res.status + ')');
130            const data = await res.json();
131            if (!data.success) throw new Error(data.err_msg || 'Failed to revoke all sessions');
132            this._fetch(1, acct_id);
133        } catch(err) {
134            this.setState({ status: 'error', errorMsg: err.message });
135        }
136    }
137
138    _formatDate(ts) {
139        const d = new Date(Number(ts) * 1000);
140        return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
141             + ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
142    }
143
144    #trashIcon() {
145        return '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">'
146             + '<path d="M6.66667 12C6.84348 12 7.01305 11.9298 7.13807 11.8047C7.2631 11.6797 7.33333 11.5101 7.33333 11.3333V7.33333C7.33333 7.15652 7.2631 6.98695 7.13807 6.86193C7.01305 6.73691 6.84348 6.66667 6.66667 6.66667C6.48986 6.66667 6.32029 6.73691 6.19526 6.86193C6.07024 6.98695 6 7.15652 6 7.33333V11.3333C6 11.5101 6.07024 11.6797 6.19526 11.8047C6.32029 11.9298 6.48986 12 6.66667 12ZM13.3333 4H10.6667V3.33333C10.6667 2.8029 10.456 2.29419 10.0809 1.91912C9.70581 1.54405 9.1971 1.33333 8.66667 1.33333H7.33333C6.8029 1.33333 6.29419 1.54405 5.91912 1.91912C5.54405 2.29419 5.33333 2.8029 5.33333 3.33333V4H2.66667C2.48986 4 2.32029 4.07024 2.19526 4.19526C2.07024 4.32029 2 4.48986 2 4.66667C2 4.84348 2.07024 5.01305 2.19526 5.13807C2.32029 5.2631 2.48986 5.33333 2.66667 5.33333H3.33333V12.6667C3.33333 13.1971 3.54405 13.7058 3.91912 14.0809C4.29419 14.456 4.8029 14.6667 5.33333 14.6667H10.6667C11.1971 14.6667 11.7058 14.456 12.0809 14.0809C12.456 13.7058 12.6667 13.1971 12.6667 12.6667V5.33333H13.3333C13.5101 5.33333 13.6797 5.2631 13.8047 5.13807C13.9298 5.01305 14 4.84348 14 4.66667C14 4.48986 13.9298 4.32029 13.8047 4.19526C13.6797 4.07024 13.5101 4 13.3333 4ZM6.66667 3.33333C6.66667 3.15652 6.7369 2.98695 6.86193 2.86193C6.98695 2.7369 7.15652 2.66667 7.33333 2.66667H8.66667C8.84348 2.66667 9.01305 2.7369 9.13807 2.86193C9.2631 2.98695 9.33333 3.15652 9.33333 3.33333V4H6.66667V3.33333ZM11.3333 12.6667C11.3333 12.8435 11.2631 13.013 11.1381 13.1381C11.013 13.2631 10.8435 13.3333 10.6667 13.3333H5.33333C5.15652 13.3333 4.98695 13.2631 4.86193 13.1381C4.7369 13.013 4.66667 12.8435 4.66667 12.6667V5.33333H11.3333V12.6667ZM9.33333 12C9.51014 12 9.67971 11.9298 9.80474 11.8047C9.92976 11.6797 10 11.5101 10 11.3333V7.33333C10 7.15652 9.92976 6.98695 9.80474 6.86193C9.67971 6.73691 9.51014 6.66667 9.33333 6.66667C9.15652 6.66667 8.98695 6.73691 8.86193 6.86193C8.7369 6.98695 8.66667 7.15652 8.66667 7.33333V11.3333C8.66667 11.5101 8.7369 11.6797 8.86193 11.8047C8.98695 11.9298 9.15652 12 9.33333 12Z" fill="#FD5D6B"/>'
147             + '</svg>';
148    }
149
150    render() {
151        const { status, rows, page, totalPages, errorMsg, pendingRevoke } = this.state;
152        const isUser          = this.mode === 'user';
153        const currentSelector = isUser ? this.currentSelector : '';
154
155        const styles = '<style>'
156            + ':host{display:block;position:relative;}'
157            + '.material-icons{font-family:\'Material Icons\';font-weight:normal;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;}'
158            + '.confirm-overlay{position:absolute;inset:0;background:rgba(255,255,255,0.93);display:flex;align-items:center;justify-content:center;z-index:10;border-radius:4px;}'
159            + '.confirm-card{background:#fff;border:1px solid #e0e0e0;border-radius:6px;padding:1.25rem 1.5rem;max-width:340px;width:100%;box-shadow:0 2px 12px rgba(0,0,0,0.1);}'
160            + '.confirm-title{margin:0 0 0.5rem;font-size:15px;font-weight:600;color:#222;}'
161            + '.confirm-msg{margin:0 0 1rem;font-size:14px;color:#555;}'
162            + '.confirm-actions{display:flex;gap:0.5rem;justify-content:flex-end;}'
163            + '</style>';
164
165        let bodyHtml = '';
166        if (status === 'idle' || status === 'loading') {
167            bodyHtml = '<p style="padding:0.5rem 0; color:#888;">Loading...</p>';
168        } else if (status === 'error') {
169            bodyHtml = '<p style="padding:0.5rem 0; color:var(--color-red,#c0392b);">' + errorMsg + '</p>';
170        } else if (!rows.length) {
171            bodyHtml = '<p style="padding:0.5rem 0; color:#888;">No active sessions.</p>';
172        } else {
173            bodyHtml = '<list-table></list-table>'
174                + '<div style="display:flex;justify-content:flex-end;margin-top:0.75rem;">'
175                + '<button-element variant="negative" class="revoke-all-btn">Revoke all sessions</button-element>'
176                + '</div>';
177        }
178
179        let overlayHtml = '';
180        if (pendingRevoke !== null) {
181            const isAll = pendingRevoke.type === 'all';
182            const title = isAll ? 'Revoke all sessions' : 'Revoke session';
183            const msg   = isAll
184                ? (isUser ? 'This will sign you out on all other devices. Your current session will be preserved.' : 'This will sign the account out on every device.')
185                : 'Revoke this session? That device will be signed out.';
186            overlayHtml = '<div class="confirm-overlay">'
187                + '<div class="confirm-card">'
188                + '<p class="confirm-title">' + title + '</p>'
189                + '<p class="confirm-msg">' + msg + '</p>'
190                + '<div class="confirm-actions">'
191                + '<button-element class="confirm-cancel-btn" variant="negative-secondary">Cancel</button-element>'
192                + '<button-element class="confirm-proceed-btn" variant="negative">Revoke</button-element>'
193                + '</div>'
194                + '</div>'
195                + '</div>';
196        }
197
198        this.loadTemplate(styles + bodyHtml + overlayHtml);
199
200        if (status === 'loaded' && rows.length) {
201            const listTable = this.shadowRoot.querySelector('list-table');
202            const trashIcon = this.#trashIcon();
203
204            listTable.setCheckbox(false);
205            listTable.setActions(false);
206            listTable.setColumns([
207                {
208                    key: 'client_summary',
209                    label: 'Device',
210                    render: (row) => {
211                        const isMirror   = !isUser && row.is_mirror == 1;
212                        const mirrorBadge = isMirror
213                            ? '<span style="background:#fff3cd;color:#856404;font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;margin-left:5px;vertical-align:middle;">MIRROR</span>'
214                            : '';
215                        return '<span style="font-size:12px;">' + (row.client_summary || '-') + mirrorBadge + '</span>';
216                    },
217                },
218                {
219                    key: 'ip',
220                    label: 'IP',
221                    render: (row) => '<span style="color:#919197;font-size:12px;">' + html_entities(row.ip || '-') + '</span>',
222                },
223                {
224                    key: 'timestamp',
225                    label: 'Started',
226                    render: (row) => '<span style="font-size:12px;white-space:nowrap;">' + this._formatDate(row.timestamp) + '</span>',
227                },
228                {
229                    key: 'expiration',
230                    label: 'Expires',
231                    render: (row) => '<span style="font-size:12px;white-space:nowrap;">' + this._formatDate(row.expiration) + '</span>',
232                },
233                {
234                    key: 'action',
235                    label: '',
236                    render: (row) => {
237                        const isCurrent = isUser && row.selector === currentSelector;
238                        if (isCurrent) return '<span class="quiet" style="font-size:12px;">Current</span>';
239                        return '<a href="#" class="session-revoke-btn" data-selector="' + html_entities(row.selector) + '" style="display:inline-flex;align-items:center;padding:4px;opacity:0.75;" title="Revoke session">' + trashIcon + '</a>';
240                    },
241                },
242            ]);
243
244            if (totalPages > 1) {
245                listTable.setPagination({ currentPage: page, totalPages, rowsPerPage: 10 });
246            }
247
248            listTable.setData(rows);
249
250            this.shadowRoot.querySelectorAll('.session-revoke-btn').forEach((btn) => {
251                btn.addEventListener('click', (e) => {
252                    e.preventDefault();
253                    this.setState({ pendingRevoke: { type: 'single', selector: btn.dataset.selector } });
254                });
255            });
256
257            const revokeAllBtn = this.shadowRoot.querySelector('.revoke-all-btn');
258            if (revokeAllBtn) {
259                revokeAllBtn.addEventListener('click', () => {
260                    this.setState({ pendingRevoke: { type: 'all' } });
261                });
262            }
263
264            listTable.addEventListener('page-change', (e) => {
265                this._fetch(e.detail.page, this.state.acct_id);
266            });
267        }
268
269        const cancelBtn  = this.shadowRoot.querySelector('.confirm-cancel-btn');
270        const proceedBtn = this.shadowRoot.querySelector('.confirm-proceed-btn');
271        if (cancelBtn) {
272            cancelBtn.addEventListener('click', () => {
273                this.setState({ pendingRevoke: null });
274            });
275        }
276        if (proceedBtn) {
277            proceedBtn.addEventListener('click', () => {
278                const pending = this.state.pendingRevoke;
279                if (!pending) return;
280                if (pending.type === 'all') {
281                    this._doRevokeAll();
282                } else {
283                    this._doRevoke(pending.selector);
284                }
285            });
286        }
287    }
288}
289
290customElements.define('user-sessions-element', UserSessionsElement);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.