Skip to content

Feature: Generate User Tokens #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jun 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
002c679
Lint fix.
benloh Apr 11, 2025
cb83aef
wcag: Line up filter summary height with "ANALYSIS" button
benloh Apr 11, 2025
506760b
user-token: Add "User Tokens" to Advanced panel.
benloh Apr 11, 2025
95d36d8
user-token: Fix missed outdated `this.state` reference.
benloh Apr 26, 2025
184836a
user-token: Add project template `salt` to seed user tokens (in place…
benloh Apr 29, 2025
c6c8efd
user-token: Add `adminPassword` field to project template and NCAdvan…
benloh Apr 29, 2025
f28e32b
user-token: Use adminPassword for NCNode and NCEdge (instead of `sett…
benloh Apr 29, 2025
11f1d45
user-token: Use adminPassword for NCImportExport (instead of `settings`)
benloh Apr 30, 2025
9dd7292
user-token: Improve handling of async data updates
benloh Apr 30, 2025
7c05638
user-token: Improve handling of async data updates NCNode
benloh Apr 30, 2025
d8fb9f5
user-token: Show ❄︎ in NCAdvancedPanel if `adminPassword` has not bee…
benloh Apr 30, 2025
6333c30
user-token: If template salt is not defined, show warning and default…
benloh May 1, 2025
5aabd2b
hotfix-table-optimization: Skip table render if table is not open. #…
benloh May 8, 2025
47ec2da
hotfix-table-optimization2: Fold FILTER_SUMMARY_UPDATE into FILTERED…
benloh May 24, 2025
aeeb001
hotfix-table-optimization2: Fix bad NCInfoPanel mouse drag events
benloh May 24, 2025
39cc85c
hotfix-table-optimization2: Move filter summary to new NCFiltersSumma…
benloh May 24, 2025
c26235c
hotfix-table-optimization2: Skip NCNodeTable and NCEdgeTable renders …
benloh May 24, 2025
41aa444
hotfix-table-optimization2: Fix filter summary and InfoPanel dragger …
benloh May 24, 2025
424299c
hotfix-table-optimization2: Cache HumanDate and HumanDateShort render…
benloh May 24, 2025
57d9649
Merge branch 'hotfix-table-optimization2' into dev-bl/user-token
benloh May 26, 2025
7ec9566
user-token: Show `Admin Mode Disabled` and console error if `adminPas…
benloh May 26, 2025
5b5a520
user-token: Label adminPassword field "admin:"
benloh May 26, 2025
1d2af6c
Merge remote-tracking branch 'origin/dev-dhi' into dev-bl/user-token
benloh May 29, 2025
ce1750a
Merge remote-tracking branch 'origin/dev-dhi' into dev-bl/user-token
benloh May 29, 2025
9ef69d7
user-token: Use `secretCall` rather than `salt` to make it more obvio…
benloh Jun 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ v2.0.0 introduces "commenting" and a fresh user interface. Database/file data f
- Conversion of JSON templates to TOML/YAML has been deprecated

**Significant Features**
- Generate "User Tokens" via the Advanced Panel #389
- Nodes/Edges can be imported using "Replace" or "Merge" #386
- All user interface widgets should be accessible #362
- keyboard control is supported
- voice control is supported
Expand Down
2 changes: 2 additions & 0 deletions app-templates/_default.template.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
version = 2
name = "Untitled Project"
description = "No Description"
secretKey = "exp626"
adminPassword = "danishpowers"
requireLogin = false
hideDeleteNodeButton = false
allowLoggedInUserToImport = false
Expand Down
40 changes: 28 additions & 12 deletions app/unisys/common-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var m_current_groupid = null;

`dataset` is not currently being used, but is retained for future use.
*/
SESUTIL.DecodeToken = function (token, dataset) {
SESUTIL.DecodeToken = function (token, templateSalt) {
const DELIMITER = '-';
if (token === undefined) return {};
// 2024/08 Allow optional `dataset` so tokens can be shared across graphs
Expand All @@ -55,10 +55,20 @@ SESUTIL.DecodeToken = function (token, dataset) {
if (tokenBits[2]) hashedId = tokenBits[2].toUpperCase();
if (tokenBits[3]) subId = tokenBits[3].toUpperCase();
// initialize hashid structure
// 2024/08 Allow optional `dataset` so tokens can be shared across graphs
// Orig code: let salt = `${classId}${projId}${dataset}`;
let salt = `${classId}${projId}`; // skips `dataset`
if (DBG) console.warn('commen-session ignoring "dataset" to allow decoding of shared tokens');

// Allow shareable tokens by setting `dataset` to undefined
let salt;
if (templateSalt !== undefined) {
salt = `${classId}${projId}${templateSalt}`;
} else {
salt = `${classId}${projId}`; // skips `dataset`
console.warn('"salt" is not defined. Using only classId and projId.');
}

if (DBG)
console.warn(
'commen-session ignoring "dataset" to allow decoding of shared tokens'
);
try {
let hashids = new HashIds(salt, HASH_MINLEN, HASH_ABET);
// try to decode the groupId
Expand Down Expand Up @@ -106,8 +116,8 @@ SESUTIL.DecodeToken = function (token, dataset) {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/** Return TRUE if the token decodes into an expected range of values
*/
SESUTIL.IsValidToken = function (token, dataset) {
let decoded = SESUTIL.DecodeToken(token, dataset);
SESUTIL.IsValidToken = function (token, templateSalt) {
let decoded = SESUTIL.DecodeToken(token, templateSalt);
return decoded && Number.isInteger(decoded.groupId);
};
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Expand All @@ -117,7 +127,7 @@ SESUTIL.IsValidToken = function (token, dataset) {

`dataset` is not currently being used, but is retained for future use.
*/
SESUTIL.MakeToken = function (classId, projId, groupId, dataset) {
SESUTIL.MakeToken = function (classId, projId, groupId, templateSalt) {
// type checking
if (typeof classId !== 'string')
throw Error(`classId arg1 '${classId}' must be string`);
Expand All @@ -133,10 +143,16 @@ SESUTIL.MakeToken = function (classId, projId, groupId, dataset) {
// initialize hashid structure
classId = classId.toUpperCase();
projId = projId.toUpperCase();
// 2024/08 Allow optional `dataset` so tokens can be shared across graphs
// Orig code: let salt = `${classId}${projId}${dataset}`;
let salt = `${classId}${projId}`; // skips `dataset`
if (DBG) console.warn('commen-session ignoring "dataset" to allow creation of shared tokens');

// Allow shareable tokens by setting `dataset` to undefined
let salt;
if (templateSalt !== undefined) salt = `${classId}${projId}${templateSalt}`;
else salt = `${classId}${projId}`; // skips `dataset`

if (DBG)
console.warn(
'commen-session ignoring "dataset" to allow creation of shared tokens'
);
let hashids = new HashIds(salt, HASH_MINLEN, HASH_ABET);
let hashedId = hashids.encode(groupId);
return `${classId}-${projId}-${hashedId}`;
Expand Down
14 changes: 9 additions & 5 deletions app/unisys/component/SessionShell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ class SessionShell extends UNISYS.Component {
hashedId: null,
subId: null,
groupId: null,
isValid: false
isValid: false,
templateSalt: undefined // this is set in componentDidMount()
};
this.previousIsValid = false; // to track changes in loggedIn status

Expand Down Expand Up @@ -346,7 +347,10 @@ class SessionShell extends UNISYS.Component {

const { routeProps } = SETTINGS.GetRouteInfoFromURL();
let { token } = routeProps;
let decoded = SESSION.DecodeToken(token, window.NC_CONFIG.dataset) || {};
const TEMPLATE = this.AppState('TEMPLATE');
const templateSalt = TEMPLATE && TEMPLATE.secretKey;
this.setState({ templateSalt });
const decoded = SESSION.DecodeToken(token, templateSalt) || {};
this.SetAppState('SESSION', decoded);
this.previousIsValid = decoded.isValid;
}
Expand All @@ -359,7 +363,7 @@ class SessionShell extends UNISYS.Component {
let { token } = routeProps;

if (!token) return; // don't bother to check if this was a result of changes from the form
let decoded = SESSION.DecodeToken(token, window.NC_CONFIG.dataset);
let decoded = SESSION.DecodeToken(token, this.state.templateSalt);
if (decoded.isValid !== this.previousIsValid) {
this.SetAppState('SESSION', decoded);
this.previousIsValid = decoded.isValid;
Expand All @@ -370,7 +374,7 @@ class SessionShell extends UNISYS.Component {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handleChange(event) {
let token = event.target.value;
let decoded = SESSION.DecodeToken(token, window.NC_CONFIG.dataset);
let decoded = SESSION.DecodeToken(token, this.state.templateSalt);
let { classId, projId, hashedId, subId, groupId } = decoded;
this.setState(decoded);
}
Expand Down Expand Up @@ -409,7 +413,7 @@ class SessionShell extends UNISYS.Component {
if (!token) return this.renderLogin();

// try to decode token
let decoded = SESSION.DecodeToken(token, window.NC_CONFIG.dataset);
let decoded = SESSION.DecodeToken(token, this.state.templateSalt);
if (decoded.isValid) {
return this.renderLoggedIn(decoded);
} else {
Expand Down
12 changes: 12 additions & 0 deletions app/view/netcreate/components/NCAdvancedPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.NCAdvancedPanel .footer {
display: flex;
flex: 1;
justify-content: center;
align-items: end;
padding: 1rem 0 0.25rem 0;
font-size: var(--font-size-xs);
color: var(--clr-system-faded);
}
.NCAdvancedPanel .footer input {
font-size: var(--font-size-xs);
}
111 changes: 104 additions & 7 deletions app/view/netcreate/components/NCAdvancedPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
/* eslint-disable react/no-unescaped-entities */
/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\

## OVERVIEW
# NCAdvancedPanel

NCAdvancedPanel handles:
- Template Import/Export
- Node/Edge Import/Export
- User Tokens
- Admin Password

By default, only Export nodes/edges is enabled for normal users.
(The "Import/Export" tab will display "Export" only).
The other functions are admin-only.


### PERMISSIONS App State

The `PERMISSIONS` app state is used to track the admin permissions.
This app state is used by NCNode, NCEdge, NCImportExport to
enable/disable admin-only features.

REVIEW: This probably should be moved to a permissions manager.


### Admin Password

Only administrators (teachers) can manage templates, import data, and manage
user tokens.

The admin password is defined in the project template with the `adminPassword`
property and is not visible to students.

Admin features will be enabled as soon as you enter the correct password.
(You don't need to hit return). When the password is validated, the input
form will turn into a "Reset Password"

Vocabulary displays a list of common terms

\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/

import React, { useState, useEffect } from 'react';
import UNISYS from 'unisys/client';
import NCImportExport from './NCImportExport';
import NCTemplate from './NCTemplate';
import NCUserTokens from './NCUserTokens';
import MURSettingEditor from './MURSettingsEditor';
import URPopover from './URPopover';

Expand All @@ -24,6 +56,7 @@ const DBG = false;
const VIEWS = {
template: 'Template',
importexport: 'Import/Export',
usertokens: 'User Tokens',
settings: 'Settings'
};

Expand All @@ -33,39 +66,81 @@ const VIEWS = {
function NCAdvancedPanel() {
const [isOpen, setIsOpen] = useState(false);
const [openTab, setOpenTab] = useState('importexport');
const [password, setPassword] = useState('');
const [hasAdminPermissions, setHasAdminPermissions] = useState(undefined);

useEffect(() => {
const PERMISSIONS = UDATA.AppState('PERMISSIONS');
UDATA.SetAppState('PERMISSIONS', {
...PERMISSIONS,
isAdmin: hasAdminPermissions
});

UDATA.OnAppStateChange('PANELSTATE', evt_ToggleAdvanced);
assessAdminPrivileges();
return () => {
UDATA.AppStateChangeOff('PANELSTATE', evt_ToggleAdvanced);
};
}, []);

useEffect(() => {
assessAdminPrivileges();
}, [password]);

/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function evt_ToggleAdvanced(PANELSTATE) {
setIsOpen(PANELSTATE.advancedIsOpen);
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function assessAdminPrivileges() {
const TEMPLATE = UDATA.AppState('TEMPLATE');
if (TEMPLATE && TEMPLATE.adminPassword === undefined)
console.warn(
'No admin password defined! Please set it if you need admin access'
);
const isAdmin =
TEMPLATE && TEMPLATE.adminPassword && TEMPLATE.adminPassword === password;
setHasAdminPermissions(isAdmin);

const PERMISSIONS = UDATA.AppState('PERMISSIONS');
UDATA.SetAppState('PERMISSIONS', { ...PERMISSIONS, isAdmin });
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function ui_CloseAdvanced() {
const PANELSTATE = UDATA.AppState('PANELSTATE');
UDATA.SetAppState('PANELSTATE', { ...PANELSTATE, advancedIsOpen: false });
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function ui_SelectTab(tab) {
console.log('setting tab to', tab);
setOpenTab(tab);
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function ui_PasswordChange(e) {
const password = e.target.value;
setPassword(password);
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function ui_PasswordClear() {
setPassword('');
}

// COMPONENT RENDER ////////////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const TABS = hasAdminPermissions
? VIEWS // show all tabs to admin
: { export: 'Export' }; // show "Export" only

let jsx;
switch (openTab) {
case 'template':
jsx = <NCTemplate />;
break;
case 'importexport':
jsx = <NCImportExport />;
case 'export':
jsx = <NCImportExport isAdmin={hasAdminPermissions} />;
break;
case 'usertokens':
jsx = <NCUserTokens />;
break;
case 'settings':
jsx = <MURSettingEditor />;
Expand All @@ -75,11 +150,31 @@ function NCAdvancedPanel() {
}

if (!isOpen) return null;

let adminStatus;
if (hasAdminPermissions === undefined) {
adminStatus = <span>Admin Mode Disabled</span>;
console.error(
'"adminPassword" has not been defined in template! You will not be able to access admin features. Add a "adminPassword" property to the template to enable admin features.'
);
} else if (hasAdminPermissions === false)
adminStatus = (
<label>
admin: <input type="password" id="password" onChange={ui_PasswordChange} />
</label>
);
else if (hasAdminPermissions === true)
adminStatus = (
<button type="button" onClick={ui_PasswordClear}>
Admin Logout
</button>
);

return (
<URPopover title="Advanced" onClose={ui_CloseAdvanced}>
<div id="NCTabPanel">
<div id="NCTabPanel" className="NCAdvancedPanel">
<div className="tabs" role="tablist">
{Object.keys(VIEWS).map(k => (
{Object.keys(TABS).map(k => (
<button
key={k}
role="tab"
Expand All @@ -89,12 +184,14 @@ function NCAdvancedPanel() {
tabIndex={openTab === k ? '0' : '-1'}
onClick={() => ui_SelectTab(k)}
>
{VIEWS[k]}
{TABS[k]}
</button>
))}
</div>

<div className="tabpanels">{jsx}</div>

<div className="footer">{adminStatus}</div>
</div>
</URPopover>
);
Expand Down
Loading