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

Open
wants to merge 12 commits into
base: dev-dhi
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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"
salt = "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
4 changes: 3 additions & 1 deletion app/unisys/component/SessionShell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ 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.salt;
const decoded = SESSION.DecodeToken(token, templateSalt) || {};
this.SetAppState('SESSION', decoded);
this.previousIsValid = decoded.isValid;
}
Expand Down
10 changes: 10 additions & 0 deletions app/view/netcreate/components/NCAdvancedPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.NCAdvancedPanel .footer {
display: flex;
flex: 1;
justify-content: center;
align-items: end;
padding: 1rem 0 0.25rem 0;
}
.NCAdvancedPanel .footer input {
font-size: var(--font-size-xs);
}
105 changes: 98 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,25 @@ function NCAdvancedPanel() {
}

if (!isOpen) return null;

let adminStatus;
if (hasAdminPermissions === undefined) adminStatus = <span>❄︎</span>;
else if (hasAdminPermissions === false)
adminStatus = (
<input type="password" id="password" onChange={ui_PasswordChange} />
);
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 +178,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
13 changes: 11 additions & 2 deletions app/view/netcreate/components/NCEdge.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import URCommentVBtn from './URCommentVBtn';
const DBG = false;
const PR = 'NCEdge';
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const isAdmin = SETTINGS.IsAdmin();
// const isAdmin = SETTINGS.IsAdmin();
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const TABS = {
// Also used as labels
Expand All @@ -63,6 +63,7 @@ class NCEdge extends UNISYS.Component {

this.state = {
isLoggedIn: false,
isAdmin: false,
animateHeight: 0
}; // initialized on componentDidMount and clearSelection

Expand All @@ -71,6 +72,7 @@ class NCEdge extends UNISYS.Component {
this.urstate_SESSION = this.urstate_SESSION.bind(this);
this.urstate_LOCKSTATE = this.urstate_LOCKSTATE.bind(this);
this.urstate_NCDATA = this.urstate_NCDATA.bind(this);
this.urstate_PERMISSIONS = this.urstate_PERMISSIONS.bind(this);
this.IsLoggedIn = this.IsLoggedIn.bind(this);
this.DerivePermissions = this.DerivePermissions.bind(this);

Expand Down Expand Up @@ -128,6 +130,7 @@ class NCEdge extends UNISYS.Component {
this.OnAppStateChange('NCDATA', this.urstate_NCDATA);
this.OnAppStateChange('SELECTION', this.urstate_SELECTION);
this.OnAppStateChange('LOCKSTATE', this.urstate_LOCKSTATE);
this.OnAppStateChange('PERMISSIONS', this.urstate_PERMISSIONS);
this.HandleMessage('EDGE_OPEN', this.ReqLoadEdge);
this.HandleMessage('EDGE_DESELECT', this.ClearSelection);
this.HandleMessage('EDGE_EDIT', this.EditEdge); // EdgeTable request
Expand All @@ -149,6 +152,7 @@ class NCEdge extends UNISYS.Component {
this.AppStateChangeOff('NCDATA', this.urstate_NCDATA);
this.AppStateChangeOff('SELECTION', this.urstate_SELECTION);
this.AppStateChangeOff('LOCKSTATE', this.urstate_LOCKSTATE);
this.AppStateChangeOff('PERMISSIONS', this.urstate_PERMISSIONS);
this.DropMessage('EDGE_OPEN', this.ReqLoadEdge);
this.DropMessage('EDGE_DESELECT', this.ClearSelection);
this.DropMessage('EDGE_EDIT', this.EditEdge);
Expand Down Expand Up @@ -179,6 +183,7 @@ class NCEdge extends UNISYS.Component {

// SYSTEM STATE
// isLoggedIn: false, // don't clear session state!
isAdmin: false,
// previousState: {},

// UI State 'u'
Expand Down Expand Up @@ -236,6 +241,9 @@ class NCEdge extends UNISYS.Component {
const permissionsState = this.DerivePermissions(this.state.id);
this.setState({ ...permissionsState });
}
urstate_PERMISSIONS(PERMISSIONS) {
this.setState({ isAdmin: PERMISSIONS.isAdmin });
}

/*
Called by NCDATA AppState updates
Expand Down Expand Up @@ -898,7 +906,8 @@ class NCEdge extends UNISYS.Component {
id,
dSourceNode = { label: undefined },
dTargetNode = { label: undefined },
type
type,
isAdmin
} = this.state;
const bgcolor = uBackgroundColor + '66'; // hack opacity
const TEMPLATE = this.AppState('TEMPLATE');
Expand Down
2 changes: 1 addition & 1 deletion app/view/netcreate/components/NCEdgeTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function NCEdgeTable({ isOpen }) {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function urstate_SESSION(decoded) {
const isLocked = !decoded.isValid;
if (isLocked === this.state.isLocked) {
if (isLocked === state.isLocked) {
return;
}
setState(prevState => ({ ...prevState, isLocked }));
Expand Down
4 changes: 4 additions & 0 deletions app/view/netcreate/components/NCHelpPanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#NCTabPanel {
display: flex;
flex-direction: column;
width: 100%;
}
#NCTabPanel .tabs button {
font-weight: var(--font-weight-normal);
Expand All @@ -17,6 +18,9 @@
border: none;
border-radius: 0;
}
#NCTabPanel.NCAdvancedPanel .tabs button {
padding: 8px 8px 2px 8px;
}
#NCTabPanel .tabs button:hover {
opacity: 100%;
}
Expand Down
Loading