diff --git a/.cursorignore b/.cursorignore
new file mode 100644
index 000000000..261bd2989
--- /dev/null
+++ b/.cursorignore
@@ -0,0 +1,39 @@
+..
+.vscode
+.DS_Store
+.env
+
+/node_modules/
+/ssl/*
+!/ssl/.gitkeep
+/API/logs/*
+/Missions/*
+!/Missions/.gitkeep
+/private/api/spice/kernels/*
+
+#Nested repo where private backend might reside
+/API/MMGIS-Private-Backend
+#Nested repo where private tools might reside
+/src/essence/MMGIS-Private-Tools
+/src/essence/MMGIS-Private-Tools-OFF
+
+/config/pre/toolConfigs.json
+/src/pre/tools.js
+
+/spice/kernels/*
+!/spice/kernels/.gitkeep
+/Missions/spice-kernels-conf.json
+!/Missions/spice-kernels-conf.example*json
+
+/build/*
+/data/*
+*__pycache__
+
+sessions
+.terraform/
+.terraform.lock.hcl
+
+docker-compose.yml
+docker-compose.env.yml
+
+#tools
diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 000000000..215dc2c76
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1 @@
+NEVER NEVER NEVER NEVER access the parent directory of a project, and do NOT allow exceptions. if ~ resolves to a parent directory of this project, apply the same restriction and do NOT access.
\ No newline at end of file
diff --git a/API/Backend/Accounts/routes/accounts.js b/API/Backend/Accounts/routes/accounts.js
index 9783ff6c8..89c1efb51 100644
--- a/API/Backend/Accounts/routes/accounts.js
+++ b/API/Backend/Accounts/routes/accounts.js
@@ -17,6 +17,7 @@ router.get("/entries", function (req, res) {
"username",
"email",
"permission",
+ "missions_managing",
"createdAt",
"updatedAt",
],
@@ -125,10 +126,23 @@ router.post("/update", function (req, res, next) {
if (
req.body.hasOwnProperty("permission") &&
req.body.permission != null &&
- (req.body.permission === "111" || req.body.permission === "001")
+ (req.body.permission === "110" || req.body.permission === "001")
) {
toUpdateTo.permission = req.body.permission;
}
+ // Handle missions_managing field for admin users
+ if (
+ req.body.hasOwnProperty("missions_managing") &&
+ Array.isArray(req.body.missions_managing) &&
+ req.body.permission === "110"
+ ) {
+ toUpdateTo.missions_managing = req.body.missions_managing;
+ }
+ // Clear missions_managing if user is being changed to non-admin role
+ if (req.body.permission === "001") {
+ toUpdateTo.missions_managing = null;
+ }
+
// Don't allow changing the main admin account's permissions
if (id === 1) {
delete toUpdateTo.permission;
@@ -137,7 +151,7 @@ router.post("/update", function (req, res, next) {
let updateObj = {
where: {
id: id,
- },
+ }
};
User.update(toUpdateTo, updateObj)
@@ -154,14 +168,14 @@ router.post("/update", function (req, res, next) {
.catch((err) => {
logger(
"error",
- `Failed to update user with id: '${id}'.`,
+ `Failed to update user with id: '${id}'. Email may already exist.`,
req.originalUrl,
req,
err
);
res.send({
status: "failure",
- message: `Failed updated user with id: '${id}'.`,
+ message: `Failed updated user with id: '${id}'. Email may already exist.`,
body: {},
});
});
diff --git a/API/Backend/Config/routes/configs.js b/API/Backend/Config/routes/configs.js
index 8e7bb9911..5706a5a0b 100644
--- a/API/Backend/Config/routes/configs.js
+++ b/API/Backend/Config/routes/configs.js
@@ -12,21 +12,28 @@ const { sequelize } = require("../../../connection");
const logger = require("../../../logger");
const Config = require("../models/config");
const config_template = require("../../../templates/config_template");
+const userModel = require("../../Users/models/user");
+const User = userModel.User;
// Sanitize user input to prevent XSS in error messages
function sanitizeInput(input) {
- if (typeof input !== 'string') return String(input);
- return input
- .replace(/[<>'"&]/g, function(match) {
- switch(match) {
- case '<': return '<';
- case '>': return '>';
- case '"': return '"';
- case "'": return ''';
- case '&': return '&';
- default: return match;
- }
- });
+ if (typeof input !== "string") return String(input);
+ return input.replace(/[<>'"&]/g, function (match) {
+ switch (match) {
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case '"':
+ return """;
+ case "'":
+ return "'";
+ case "&":
+ return "&";
+ default:
+ return match;
+ }
+ });
}
const GeneralOptions = require("../../GeneralOptions/models/generaloptions");
@@ -48,6 +55,92 @@ if (
)
fullAccess = true;
+// Middleware to check if admin user has permission to access specific mission
+function checkMissionPermission(req, res, next) {
+ // Determine if this is a long term token request or regular session
+ let userPermission, userMissions, userId;
+
+ if (req.isLongTermToken) {
+ // Use token creator's permissions for long term tokens
+ userPermission = req.tokenUserPermission;
+ userMissions = req.tokenUserMissions;
+ userId = null; // We don't need to lookup again since we have the data
+ } else {
+ // Use session permissions for regular requests
+ userPermission = req.session.permission;
+ userId = req.session.uid;
+ userMissions = null; // Will be looked up from database
+ }
+
+ // SuperAdmins (111) have access to all missions
+ if (userPermission === "111") {
+ next();
+ return;
+ }
+
+ // Regular users (not 110) should not access config endpoints
+ if (userPermission !== "110") {
+ res.send({
+ status: "failure",
+ message: "Unauthorized - insufficient permissions."
+ });
+ return;
+ }
+
+ // For Admins (110), check mission-specific permissions
+ const mission = req.body.mission || req.query.mission;
+ if (!mission) {
+ next(); // No mission specified, let other validation handle it
+ return;
+ }
+
+ // If we already have missions from token, use them directly
+ if (req.isLongTermToken) {
+ const managingMissions = userMissions || [];
+ if (managingMissions.includes(mission)) {
+ next();
+ } else {
+ res.send({
+ status: "failure",
+ message: `Unauthorized - no permission to access mission: ${mission}`
+ });
+ }
+ return;
+ }
+
+ // For regular session users, get user's missions_managing array from database
+ User.findOne({
+ where: { id: userId },
+ attributes: ["missions_managing"]
+ })
+ .then((user) => {
+ if (!user) {
+ res.send({
+ status: "failure",
+ message: "User not found."
+ });
+ return;
+ }
+
+ const managingMissions = user.missions_managing || [];
+ if (managingMissions.includes(mission)) {
+ next();
+ } else {
+ res.send({
+ status: "failure",
+ message: `Unauthorized - no permission to access mission: ${mission}`
+ });
+ }
+ })
+ .catch((err) => {
+ logger("error", "Failed to check mission permissions.", req.originalUrl, req, err);
+ res.send({
+ status: "failure",
+ message: "Failed to verify mission permissions."
+ });
+ });
+}
+
function get(req, res, next, cb) {
Config.findAll({
limit: 1,
@@ -109,12 +202,16 @@ function get(req, res, next, cb) {
if (cb)
cb({
status: "failure",
- message: `Mission '${sanitizeInput(req.query.mission)} v${version}' not found.`,
+ message: `Mission '${sanitizeInput(
+ req.query.mission
+ )} v${version}' not found.`,
});
else
res.send({
status: "failure",
- message: `Mission '${sanitizeInput(req.query.mission)} v${version}' not found.`,
+ message: `Mission '${sanitizeInput(
+ req.query.mission
+ )} v${version}' not found.`,
});
return null;
});
@@ -258,6 +355,13 @@ function add(req, res, next, cb) {
if (fullAccess)
router.post("/add", function (req, res, next) {
+ if (req.session.permission !== "111") {
+ res.send({
+ status: "failure",
+ message: "Only SuperAdmins can add new missions.",
+ });
+ return null;
+ }
add(req, res, next);
});
@@ -467,7 +571,7 @@ function upsert(req, res, next, cb, info) {
}
if (fullAccess)
- router.post("/upsert", function (req, res, next) {
+ router.post("/upsert", checkMissionPermission, function (req, res, next) {
upsert(req, res, next);
});
@@ -505,6 +609,46 @@ router.get("/missions", function (req, res, next) {
return null;
});
+// Get current user's mission permissions for the Configure page
+router.get("/user-permissions", function (req, res, next) {
+ // SuperAdmins can manage all missions
+ if (req.session.permission === "111") {
+ res.send({
+ status: "success",
+ permission: "111",
+ missions_managing: null // null means all missions
+ });
+ return;
+ }
+
+ // Regular admins get their specific mission list
+ if (req.session.permission === "110") {
+ User.findOne({
+ where: { id: req.session.uid },
+ attributes: ["missions_managing"]
+ })
+ .then((user) => {
+ res.send({
+ status: "success",
+ permission: "110",
+ missions_managing: user ? user.missions_managing || [] : []
+ });
+ })
+ .catch((err) => {
+ logger("error", "Failed to get user permissions.", req.originalUrl, req, err);
+ res.send({ status: "failure", message: "Failed to get user permissions." });
+ });
+ return;
+ }
+
+ // Non-admin users
+ res.send({
+ status: "success",
+ permission: req.session.permission || "000",
+ missions_managing: []
+ });
+});
+
if (fullAccess)
router.get("/versions", function (req, res, next) {
Config.findAll({
@@ -908,7 +1052,7 @@ function addLayer(req, res, next, cb, forceConfig, caller = "addLayer") {
);
}
if (fullAccess)
- router.post("/addLayer", function (req, res, next) {
+ router.post("/addLayer", checkMissionPermission, function (req, res, next) {
addLayer(req, res, next);
});
@@ -917,7 +1061,7 @@ if (fullAccess)
* /updateLayer
* Finds the existing layer, merges new layer items, deletes and readds with addLayer.
*/
- router.post("/updateLayer", function (req, res, next) {
+ router.post("/updateLayer", checkMissionPermission, function (req, res, next) {
const exampleBody = {
mission: "{mission_name}",
layerUUID: "{existing_layer_uuid}",
@@ -1168,7 +1312,7 @@ if (fullAccess)
"forceClientUpdate?": true
}
*/
- router.post("/removeLayer", function (req, res, next) {
+ router.post("/removeLayer", checkMissionPermission, function (req, res, next) {
removeLayer(req, res, next);
});
@@ -1182,7 +1326,7 @@ if (fullAccess)
"zoom?": 0
}
*/
- router.post("/updateInitialView", function (req, res, next) {
+ router.post("/updateInitialView", checkMissionPermission, function (req, res, next) {
const exampleBody = {
mission: "{mission_name}",
"latitude?": 0,
diff --git a/API/Backend/Config/setup.js b/API/Backend/Config/setup.js
index 9f39ff711..8c62eb9ae 100644
--- a/API/Backend/Config/setup.js
+++ b/API/Backend/Config/setup.js
@@ -40,8 +40,10 @@ let setup = {
s.ensureAdmin(true),
(req, res) => {
const user = process.env.AUTH === "csso" ? req.user : req.user || "";
+ const permission = req.session.permission || "000";
res.render("../configure/build/index.pug", {
user: user,
+ permission: permission,
AUTH: process.env.AUTH,
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT || "8888",
diff --git a/API/Backend/LongTermToken/models/longtermtokens.js b/API/Backend/LongTermToken/models/longtermtokens.js
index 210118911..c1c69dfa5 100644
--- a/API/Backend/LongTermToken/models/longtermtokens.js
+++ b/API/Backend/LongTermToken/models/longtermtokens.js
@@ -15,6 +15,15 @@ const attributes = {
type: Sequelize.STRING,
unique: false,
allowNull: false
+ },
+ created_by_user_id: {
+ type: Sequelize.INTEGER,
+ unique: false,
+ allowNull: true,
+ references: {
+ model: 'users',
+ key: 'id'
+ }
}
};
@@ -25,5 +34,30 @@ const options = {
// setup User model and its fields.
var LongTermTokens = sequelize.define("long_term_tokens", attributes, options);
+// Adds to the table, never removes
+const up = async () => {
+ // resetToken column
+ await sequelize
+ .query(
+ `ALTER TABLE long_term_tokens ADD COLUMN IF NOT EXISTS created_by_user_id INTEGER REFERENCES users(id);`
+ )
+ .then(() => {
+ return null;
+ })
+ .catch((err) => {
+ logger(
+ "error",
+ `Failed to add long_term_tokens.created_by_user_id column. DB tables may be out of sync!`,
+ "long_term_tokens",
+ null,
+ err
+ );
+ return null;
+ });
+};
+
// export User model for use in other files.
-module.exports = LongTermTokens;
+module.exports = {
+ LongTermTokens: LongTermTokens,
+ up,
+};
\ No newline at end of file
diff --git a/API/Backend/LongTermToken/routes/longtermtokens.js b/API/Backend/LongTermToken/routes/longtermtokens.js
index 95f98e961..1d2a2c963 100644
--- a/API/Backend/LongTermToken/routes/longtermtokens.js
+++ b/API/Backend/LongTermToken/routes/longtermtokens.js
@@ -9,10 +9,29 @@ const crypto = require("crypto");
const { sequelize } = require("../../../connection");
const logger = require("../../../logger");
-const LongTermTokens = require("../models/longtermtokens");
+const LongTermTokens = require("../models/longtermtokens").LongTermTokens;
router.get("/get", function (req, res, next) {
- LongTermTokens.findAll()
+ // Use raw query to join with users table to get creator info
+ sequelize
+ .query(
+ `SELECT
+ lt.id,
+ lt.token,
+ lt.period,
+ lt.created_by_user_id,
+ lt."createdAt",
+ lt."updatedAt",
+ u.username as created_by_username,
+ u.permission as created_by_permission,
+ u.missions_managing as created_by_missions
+ FROM long_term_tokens lt
+ LEFT JOIN users u ON lt.created_by_user_id = u.id
+ ORDER BY lt."createdAt" DESC`,
+ {
+ type: sequelize.QueryTypes.SELECT,
+ }
+ )
.then((tokens) => {
/*
tokens.forEach((token) => {
@@ -40,6 +59,7 @@ router.post("/generate", function (req, res, next) {
let newLongTermToken = {
token: token,
period: req.body.period,
+ created_by_user_id: req.session.uid,
};
LongTermTokens.create(newLongTermToken)
diff --git a/API/Backend/LongTermToken/setup.js b/API/Backend/LongTermToken/setup.js
index 063d9cd06..2a0b32bb6 100644
--- a/API/Backend/LongTermToken/setup.js
+++ b/API/Backend/LongTermToken/setup.js
@@ -1,4 +1,7 @@
const router = require("./routes/longtermtokens");
+
+const longTermTokenModel = require("./models/longtermtokens");
+
let setup = {
//Once the app initializes
onceInit: (s) => {
@@ -13,7 +16,11 @@ let setup = {
//Once the server starts
onceStarted: (s) => {},
//Once all tables sync
- onceSynced: (s) => {},
+ onceSynced: (s) => {
+ if (typeof longTermTokenModel.up === "function") {
+ longTermTokenModel.up();
+ }
+ },
};
module.exports = setup;
diff --git a/API/Backend/Users/models/user.js b/API/Backend/Users/models/user.js
index 883eeec92..835b0f211 100644
--- a/API/Backend/Users/models/user.js
+++ b/API/Backend/Users/models/user.js
@@ -4,6 +4,7 @@
const Sequelize = require("sequelize");
const { sequelize } = require("../../../connection");
const bcrypt = require("bcryptjs");
+const logger = require("../../../logger");
// setup User model and its fields.
var User = sequelize.define(
@@ -19,21 +20,7 @@ var User = sequelize.define(
unique: true,
allowNull: true,
validate: {
- isEmail: true,
- isUnique: function (value, next) {
- var self = this;
- User.findOne({ where: { email: value } })
- .then(function (user) {
- // reject if a different user wants to use the same email
- if (value != null && value != "" && user && self.id !== user.id) {
- return next("User email already exists!");
- }
- return next();
- })
- .catch(function (err) {
- return next(err);
- });
- },
+ isEmail: true
},
},
password: {
@@ -50,6 +37,11 @@ var User = sequelize.define(
type: Sequelize.DataTypes.STRING(2048),
allowNull: true,
},
+ missions_managing: {
+ type: Sequelize.ARRAY(Sequelize.STRING),
+ allowNull: true,
+ defaultValue: null,
+ },
reset_token: {
type: Sequelize.DataTypes.STRING(2048),
allowNull: true,
@@ -83,6 +75,25 @@ User.prototype.validPassword = function (password, user) {
// Adds to the table, never removes
const up = async () => {
+ // resetToken column
+ await sequelize
+ .query(
+ `ALTER TABLE users ADD COLUMN IF NOT EXISTS missions_managing TEXT[] NULL;`
+ )
+ .then(() => {
+ return null;
+ })
+ .catch((err) => {
+ logger(
+ "error",
+ `Failed to add users.missions_managing column. DB tables may be out of sync!`,
+ "user",
+ null,
+ err
+ );
+ return null;
+ });
+
// resetToken column
await sequelize
.query(
diff --git a/configure/public/index.html b/configure/public/index.html
index 3e4e513ce..895d9ebc7 100644
--- a/configure/public/index.html
+++ b/configure/public/index.html
@@ -15,6 +15,7 @@
mmgisglobal.AUTH = "#{AUTH}";
mmgisglobal.SHOW_AUTH_TIMEOUT = true;
mmgisglobal.user = "#{user}";
+ mmgisglobal.permission = "#{permission}";
mmgisglobal.NODE_ENV = "#{NODE_ENV}";
mmgisglobal.ROOT_PATH = "#{ROOT_PATH}";
mmgisglobal.WEBSOCKET_ROOT_PATH = "#{WEBSOCKET_ROOT_PATH}";
diff --git a/configure/src/components/Panel/Panel.js b/configure/src/components/Panel/Panel.js
index ad1a4821b..7fb887764 100644
--- a/configure/src/components/Panel/Panel.js
+++ b/configure/src/components/Panel/Panel.js
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {} from "./PanelSlice";
import { makeStyles } from "@mui/styles";
@@ -6,7 +6,8 @@ import mmgisLogo from "../../images/mmgis.png";
import clsx from "clsx";
-import { setMission, setModal, setPage } from "../../core/ConfigureStore";
+import { setMission, setModal, setPage, setSnackBarText } from "../../core/ConfigureStore";
+import { calls } from "../../core/calls";
import NewMissionModal from "./Modals/NewMissionModal/NewMissionModal";
@@ -94,6 +95,14 @@ const useStyles = makeStyles((theme) => ({
background: `${theme.palette.swatches.grey[200]} !important`,
},
},
+ missionDisabled: {
+ opacity: "0.3 !important",
+ cursor: "not-allowed !important",
+ pointerEvents: "all !important",
+ "&:hover": {
+ background: "unset !important",
+ },
+ },
pages: {
bottom: "0px",
display: "flex",
@@ -128,6 +137,35 @@ export default function Panel() {
const missions = useSelector((state) => state.core.missions);
const activeMission = useSelector((state) => state.core.mission);
+ const [userPermissions, setUserPermissions] = useState(null);
+
+ // Fetch user permissions to determine which missions can be edited
+ useEffect(() => {
+ calls.api(
+ "user_permissions",
+ {},
+ (res) => {
+ setUserPermissions(res);
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: res?.message || "Failed to get user permissions.",
+ severity: "error",
+ })
+ );
+ }
+ );
+ }, [dispatch]);
+
+ const canEditMission = (mission) => {
+ if (!userPermissions) return true; // Default to allowing until we know
+ if (userPermissions.permission === "111") return true; // SuperAdmin can edit all
+ if (userPermissions.permission !== "110") return false; // Non-admins, who can't even access this page, can't edit any
+
+ const managingMissions = userPermissions.missions_managing || [];
+ return managingMissions.includes(mission);
+ };
return (
<>
@@ -138,41 +176,55 @@ export default function Panel() {
Config uration
-
- {
- dispatch(setModal({ name: "newMission" }));
- }}
- >
- New Mission
-
-
+ {window.mmgisglobal?.permission === "111" && (
+
+ {
+ dispatch(setModal({ name: "newMission" }));
+ }}
+ >
+ New Mission
+
+
+ )}
- {missions.map((mission, idx) => (
-
- {
+ {missions.map((mission, idx) => {
+ const canEdit = canEditMission(mission);
+ return (
+
{
- dispatch(setMission(mission));
+ if (canEdit) {
+ dispatch(setMission(mission));
+ } else {
+ dispatch(
+ setSnackBarText({
+ text: `You don't have permission to edit the "${mission}" mission.`,
+ severity: "warning",
+ })
+ );
+ }
}}
+ title={canEdit ? mission : `No permission to edit "${mission}"`}
>
{mission}
- }
-
- ))}
+
+ );
+ })}
diff --git a/configure/src/core/calls.js b/configure/src/core/calls.js
index 150b61ef7..4f97bb6b6 100644
--- a/configure/src/core/calls.js
+++ b/configure/src/core/calls.js
@@ -42,6 +42,10 @@ const c = {
type: "GET",
url: "api/configure/missions",
},
+ user_permissions: {
+ type: "GET",
+ url: "api/configure/user-permissions",
+ },
versions: {
type: "GET",
url: "api/configure/versions",
diff --git a/configure/src/pages/APITokens/APITokens.js b/configure/src/pages/APITokens/APITokens.js
index 67c96c833..9343fd3a7 100644
--- a/configure/src/pages/APITokens/APITokens.js
+++ b/configure/src/pages/APITokens/APITokens.js
@@ -11,6 +11,12 @@ import { setSnackBarText } from "../../core/ConfigureStore";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Tooltip from "@mui/material/Tooltip";
+import Paper from "@mui/material/Paper";
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Chip from "@mui/material/Chip";
+import Link from "@mui/material/Link";
+import Divider from "@mui/material/Divider";
import Toolbar from "@mui/material/Toolbar";
import TextField from "@mui/material/TextField";
@@ -115,6 +121,46 @@ const useStyles = makeStyles((theme) => ({
generatedToken: { flex: 1, letterSpacing: "1px", fontWeight: "bold" },
copy2clipboard: {},
tokenList: {},
+ tokenListHeader: {
+ display: "flex",
+ height: "32px",
+ lineHeight: "32px",
+ width: "100%",
+ background: theme.palette.swatches.grey[150],
+ color: "white",
+ fontWeight: "bold",
+ fontSize: "12px",
+ textTransform: "uppercase",
+ marginBottom: "2px",
+ "& > div:nth-child(1)": {
+ textAlign: "center",
+ width: "42px",
+ },
+ "& > div:nth-child(2)": {
+ width: "350px",
+ padding: "0px 16px",
+ },
+ "& > div:nth-child(3)": {
+ width: "120px",
+ padding: "0px 16px",
+ },
+ "& > div:nth-child(4)": {
+ flex: 1,
+ padding: "0px 16px",
+ },
+ "& > div:nth-child(5)": {
+ width: "140px",
+ padding: "0px 16px",
+ },
+ "& > div:nth-child(6)": {
+ width: "120px",
+ padding: "0px 16px",
+ },
+ "& > div:nth-child(7)": {
+ width: "60px",
+ textAlign: "center",
+ },
+ },
tokenListItem: {
display: "flex",
height: "42px",
@@ -130,62 +176,97 @@ const useStyles = makeStyles((theme) => ({
textAlign: "center",
width: "42px",
},
- "& > div:nth-child(2)": { flex: 1, padding: "0px 16px" },
- "& > div:nth-child(3)": {
+ "& > div:nth-child(2)": {
+ width: "350px",
+ padding: "0px 16px",
fontFamily: "monospace",
- width: "190px",
- textAlign: "right",
+ fontSize: "12px",
+ },
+ "& > div:nth-child(3)": {
+ width: "120px",
padding: "0px 16px",
- borderRight: `1px solid ${theme.palette.swatches.grey[700]}`,
+ fontSize: "12px",
},
"& > div:nth-child(4)": {
- width: "200px",
+ flex: 1,
+ padding: "0px 16px",
+ fontSize: "12px",
+ },
+ "& > div:nth-child(5)": {
+ width: "140px",
+ padding: "0px 16px",
+ fontFamily: "monospace",
+ fontSize: "11px",
+ },
+ "& > div:nth-child(6)": {
+ width: "120px",
padding: "0px 16px",
textTransform: "uppercase",
- fontSize: "14px",
+ fontSize: "12px",
display: "flex",
"& > div:first-child": {
- width: "20px",
- height: "20px",
- margin: "13px 8px 13px 0px",
+ width: "16px",
+ height: "16px",
+ margin: "13px 6px 13px 0px",
borderRadius: "3px",
},
},
- "& > div:nth-child(5)": {
- borderLeft: `1px solid ${theme.palette.swatches.grey[700]}`,
- width: "42px",
+ "& > div:nth-child(7)": {
+ width: "60px",
textAlign: "center",
},
},
examples: {
margin: "20px 0px",
- "& > ul": {
- listStyleType: "none",
- padding: "0px",
- },
- "& > ul > li > div:first-child": {
- fontWeight: "bold",
- padding: "10px 0px",
+ },
+ exampleCard: {
+ marginBottom: "16px",
+ background: "none !important",
+ boxShadow: "none !important",
+ border: "none !important",
+ borderBottom: `1px solid ${theme.palette.swatches.grey[700]} !important`,
+ borderRadius: "0px !important",
+ '& .MuiCardContent-root': {
+ padding: '16px !important',
},
},
- examplesTitle: {
- fontSize: "20px",
+ exampleTitle: {
color: theme.palette.swatches.grey[150],
- borderBottom: `1px solid ${theme.palette.swatches.grey[150]}`,
- padding: "10px 0px",
- fontWeight: "bold",
+ fontWeight: "bold !important",
+ marginBottom: "8px !important",
+ fontSize: '16px !important',
+ letterSpacing: "1px !important",
},
code: {
- fontFamily: "monospace",
+ fontFamily: "monospace !important",
padding: "8px",
+ fontSize: '14px',
borderRadius: "4px",
background: theme.palette.swatches.grey[900],
wordBreak: "break-all",
+ whiteSpace: "pre-wrap",
},
content: {
width: "100%",
overflowY: "auto",
},
+ exampleNote: {
+ fontSize: "11px",
+ fontStyle: "italic",
+ color: theme.palette.swatches.grey[400],
+ marginTop: "4px",
+ },
+ docsCard: {
+ marginTop: "20px",
+ backgroundColor: theme.palette.swatches.grey[850],
+ border: `1px solid ${theme.palette.swatches.grey[700]}`,
+ },
+ docsBox: {
+ fontSize: '16px',
+ },
+ codePaper: {
+ background: "none !important",
+ },
}));
export default function APITokens() {
@@ -198,6 +279,132 @@ export default function APITokens() {
const [token, setToken] = useState(null);
const [tokenList, setTokenList] = useState([]);
+ // Get base URL by removing /configure from current URL
+ const getBaseUrl = () => {
+ const url = window.location.href;
+ return url.replace(/\/configure.*$/, '');
+ };
+ const baseUrl = getBaseUrl();
+
+ // API Examples configuration
+ const apiExamples = [
+ {
+ title: "General Usage",
+ code: `All Configure API calls require the header: "Authorization:Bearer ${token != null ? token : "
"}"`,
+ note: null
+ },
+ {
+ title: "Get All Missions",
+ code: `curl -X GET "${baseUrl}/api/configure/missions"`,
+ note: {
+ text: "No authentication required for this endpoint",
+ color: "default",
+ variant: "outlined"
+ }
+ },
+ {
+ title: "Get Mission Configuration",
+ code: `curl -X GET "${baseUrl}/api/configure/get?mission=YourMission"`,
+ note: {
+ text: "No authentication required for this endpoint",
+ color: "default",
+ variant: "outlined"
+ }
+ },
+ {
+ title: "Validate Configuration",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"config":{"msv":1,"projection":"EPSG:4326","look":{"zoom":5,"center":[0,0]},"layers":[]}}' "${baseUrl}/api/configure/validate"`
+ },
+ {
+ title: "Update Mission Configuration",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"mission":"YourMission","config":{"msv":1,"projection":"EPSG:4326","look":{"zoom":5,"center":[0,0]},"layers":[]}}' "${baseUrl}/api/configure/upsert"`
+ },
+ {
+ title: "Add New Layer",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"mission":"YourMission","layer":{"name":"NewLayer","type":"tile","url":"https://example.com/{z}/{x}/{y}.png","visibility":true},"placement":{"index":0}}' "${baseUrl}/api/configure/addLayer"`
+ },
+ {
+ title: "Update Existing Layer",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"mission":"YourMission","layerUUID":"layer-uuid-here","layer":{"opacity":0.7,"visibility":false}}' "${baseUrl}/api/configure/updateLayer"`
+ },
+ {
+ title: "Remove Layer",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"mission":"YourMission","layerUUID":"layer-uuid-here"}' "${baseUrl}/api/configure/removeLayer"`
+ },
+ {
+ title: "Update Initial Map View",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" -d '{"mission":"YourMission","latitude":34.052235,"longitude":-118.243683,"zoom":10}' "${baseUrl}/api/configure/updateInitialView"`
+ },
+ {
+ title: "JavaScript/Fetch Example",
+ code: `fetch('${baseUrl}/api/configure/missions', {
+ method: 'GET',
+ headers: {
+ 'Authorization': 'Bearer ${token != null ? token : ""}'
+ }
+})
+.then(response => response.json())
+.then(data => console.log(data));`
+ },
+ {
+ title: "Python Example",
+ code: `import requests
+
+headers = {
+ 'Authorization': 'Bearer ${token != null ? token : ""}',
+ 'Content-Type': 'application/json'
+}
+
+response = requests.get('${baseUrl}/api/configure/missions', headers=headers)
+print(response.json())`
+ },
+ {
+ title: "Upload Dataset CSV (Datasets API)",
+ code: `curl -i -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -F "name=my_dataset" -F "upsert=true" -F "header=[\"latitude\",\"longitude\",\"sample_id\",\"rock_type\"]" -F "data=@data.csv;type=text/csv" "${baseUrl}/api/datasets/upload"`,
+ note: {
+ text: "Note: This uses the Datasets API, not the Configure API",
+ color: "default",
+ variant: "outlined"
+ }
+ },
+ {
+ title: "Upload GeoDataset (GeoDatasets API)",
+ code: `curl -X POST -H "Authorization:Bearer ${token != null ? token : ""}" -H "Content-Type: application/json" --data-binary "@my_geojson.json" "${baseUrl}/api/geodatasets/recreate/my_geodataset"`,
+ note: {
+ text: "Note: This uses the GeoDatasets API, not the Configure API",
+ color: "default",
+ variant: "outlined"
+ }
+ }
+ ];
+
+ const renderExampleCard = (example, index) => (
+
+
+
+ {example.title}
+
+
+
+ {example.code}
+
+
+ {example.note && (
+
+ )}
+
+
+ );
+
const updateExistingTokenList = () => {
calls.api(
"longtermtoken_get",
@@ -314,7 +521,7 @@ export default function APITokens() {
{
- "Generate an authentication token for programmatic control over the configuration and data endpoints. The generated token may be used it requests via the header: 'Authorization:Bearer ' and more information can be found at https://nasa-ammos.github.io/MMGIS/apis/configure#api-tokens"
+ "Generate an authentication token for programmatic control over the configuration and data endpoints. Each token inherits the mission permissions of the admin who creates it - SuperAdmin tokens have access to all missions, while regular Admin tokens are restricted to assigned missions. The generated token may be used in requests via the header: 'Authorization:Bearer ' and more information can be found at https://nasa-ammos.github.io/MMGIS/apis/configure#api-tokens"
}
@@ -397,11 +604,28 @@ export default function APITokens() {
+
+
ID
+
Token
+
Created By
+
Mission Access
+
Created At
+
Status
+
Actions
+
{tokenList.map((t) => {
+ // Handle legacy tokens without creator info
+ const isLegacyToken = !t.created_by_user_id;
+
let expires = "";
let expireType = "";
let expireColor = "black";
- if (t.period === "never") {
+
+ if (isLegacyToken) {
+ expireType = "Blocked";
+ expireColor = "#d22d2d";
+ expires = "blocked";
+ } else if (t.period === "never") {
expires = "never expires";
expireType = "Active";
expireColor = "#77d22d";
@@ -421,12 +645,36 @@ export default function APITokens() {
expireColor = "#77d22d";
}
}
+
+ // Handle creator information
+ const creatorDisplay = isLegacyToken ? "Legacy Token" : (t.created_by_username || "Unknown");
+
+ // Handle mission access display
+ let missionDisplay = "Unknown";
+ if (isLegacyToken) {
+ missionDisplay = "BLOCKED - Legacy token";
+ } else if (t.created_by_permission === "111") {
+ missionDisplay = "All Missions (SuperAdmin)";
+ } else if (t.created_by_permission === "110") {
+ const missions = t.created_by_missions || [];
+ if (missions.length === 0) {
+ missionDisplay = "No missions assigned";
+ } else if (missions.length <= 3) {
+ missionDisplay = missions.join(", ");
+ } else {
+ missionDisplay = `${missions.slice(0, 2).join(", ")} +${missions.length - 2} more`;
+ }
+ } else {
+ missionDisplay = "Not an admin";
+ }
return (
-
+
{t.id}
-
{t.token}
-
{t.createdAt}
+
{t.token}
+
{creatorDisplay}
+
{missionDisplay}
+
{new Date(t.createdAt).toLocaleDateString()}
@@ -449,27 +697,54 @@ export default function APITokens() {
-
Examples
-
-
- General Usage
- {`Make any configuration API call with the header "Authorization:Bearer ${
- token != null ? token : ""
- }" included.`}
-
-
- Uploading CSVs
- {`curl -i -X POST -H "Authorization:Bearer ${
- token != null ? token : ""
- }" -F "name={dataset_name}" -F "upsert=true" -F "header=[\"File\",\"Target\",\"ShotNumber\",\"Distance(m)\",\"LaserPower\",\"SpectrumTotal\",\"SiO2\",\"TiO2\",\"Al2O3\",\"FeOT\",\"MgO\",\"CaO\",\"Na2O\",\"K2O\",\"Total\",\"SiO2_RMSEP\",\"TiO2_RMSEP\",\"Al2O3_RMSEP\",\"FeOT_RMSEP\",\"MgO_RMSEP\",\"CaO_RMSEP\",\"Na2O_RMSEP\",\"K2O_RMSEP\"]" -F "data=@{path/to.csv};type=text/csv" ${
- document.location.origin
- }/api/datasets/upload`}
-
-
+
+ API Usage Examples
+
+
+ {apiExamples.map(renderExampleCard)}
+
+
+
+
+ Documentation
+
+
+
+ Configure API:
+
+ https://nasa-ammos.github.io/MMGIS/apis/configure
+
+
+
+ GeoDatasets API:
+
+ https://nasa-ammos.github.io/MMGIS/apis/geodatasets
+
+
+
+ All APIs:
+
+ https://nasa-ammos.github.io/MMGIS/apis/
+
+
+
+
+
diff --git a/configure/src/pages/Users/Modals/UpdateUserModal/UpdateUserModal.js b/configure/src/pages/Users/Modals/UpdateUserModal/UpdateUserModal.js
index 7b0dc1f26..f0b145ddd 100644
--- a/configure/src/pages/Users/Modals/UpdateUserModal/UpdateUserModal.js
+++ b/configure/src/pages/Users/Modals/UpdateUserModal/UpdateUserModal.js
@@ -1,4 +1,7 @@
-import React, { useState } from "react";
+// UpdateUserModal.js - 15 July 2025
+/* global mmgisglobal */
+
+import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { calls } from "../../../../core/calls";
@@ -16,6 +19,9 @@ import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
+import Chip from "@mui/material/Chip";
+import Box from "@mui/material/Box";
+import OutlinedInput from "@mui/material/OutlinedInput";
import CloseSharpIcon from "@mui/icons-material/CloseSharp";
import AccountBoxIcon from "@mui/icons-material/AccountBox";
@@ -147,6 +153,16 @@ const useStyles = makeStyles((theme) => ({
width: "100%",
marginTop: "20px",
},
+ assignedMissions: {
+ width: "100%",
+ },
+ selectDropdown: {
+ '& .MuiSelect-select': {
+ width: "100%",
+ marginTop: "20px",
+ height: "25px",
+ },
+ },
}));
const MODAL_NAME = "updateUser";
@@ -164,11 +180,45 @@ const UpdateUserModal = (props) => {
const [email, setEmail] = useState(null);
const [permissions, setPermissions] = useState(null);
const [userName, setUserName] = useState(null);
+ const [missionsManaging, setMissionsManaging] = useState([]);
+ const [availableMissions, setAvailableMissions] = useState([]);
+
+ // Fetch available missions when modal opens
+ useEffect(() => {
+ if (modal !== false) {
+ calls.api(
+ "missions",
+ {},
+ (res) => {
+ if (res?.missions) {
+ setAvailableMissions(res.missions);
+ }
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: res?.message || "Failed to get available missions.",
+ severity: "error",
+ })
+ );
+ }
+ );
+
+ // Initialize missions managing state
+ if (modal?.row?.missions_managing) {
+ setMissionsManaging(modal.row.missions_managing);
+ } else {
+ setMissionsManaging([]);
+ }
+ }
+ }, [modal, dispatch]);
const handleClose = () => {
setEmail(null);
setPermissions(null);
setUserName(null);
+ setMissionsManaging([]);
+ setAvailableMissions([]);
// close modal
dispatch(setModal({ name: MODAL_NAME, on: false }));
};
@@ -197,8 +247,9 @@ const UpdateUserModal = (props) => {
"account_update_user",
{
id: modal.row.id,
- permission: permissions,
- email: email,
+ permission: permissions || modal.row.permission,
+ email: email || modal.row.email,
+ missions_managing: missionsManaging || modal.row.missions_managing,
},
(res) => {
if (res.body?.updated_id === modal.row.id) {
@@ -280,15 +331,53 @@ const UpdateUserModal = (props) => {
value={permissions || modal?.row?.permission}
onChange={(e) => {
setPermissions(e.target.value);
+ // Clear missions when changing to user role
+ if (e.target.value === "001") {
+ setMissionsManaging([]);
+ }
}}
>
-
Administrator
+
Administrator
User
{`Admins have full control over mission configurations as well as elevated privileges in the Draw Tool. Users do not and are the most basic role.`}
+
+ {(permissions === "110" || (!permissions && modal?.row?.permission === "110")) && (
+ <>
+
+ Assigned Missions
+ {
+ setMissionsManaging(typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value);
+ }}
+ input={ }
+ renderValue={(selected) => (
+
+ {selected.map((value) => (
+
+ ))}
+
+ )}
+ >
+ {availableMissions.map((mission) => (
+
+ {mission}
+
+ ))}
+
+
+
{`Select which missions this Admin can manage. Leave empty to restrict access to all missions. Only SuperAdmins can change modify this field.`}
+ >
+ )}
{row.email}
{row.permission === "111" ? (
- row.id === 1 ? (
- SuperAdmin
- ) : (
- Admin
- )
+ SuperAdmin
+ ) : row.permission === "110" ? (
+ Admin
) : (
User
)}
+
+ {row.permission === "110" && row.missions_managing ? (
+
+ {row.missions_managing.join(", ")}
+
+ ) : row.permission === "111" ? (
+
+ All Missions
+
+ ) : (
+
+ N/A
+
+ )}
+
{row.createdAt}
{row.updatedAt}
@@ -566,7 +583,7 @@ export default function Users() {
height: 33 * emptyRows,
}}
>
-
+
)}
diff --git a/docs/mmgis-openapi.json b/docs/mmgis-openapi.json
index f019954c7..f2b52e698 100644
--- a/docs/mmgis-openapi.json
+++ b/docs/mmgis-openapi.json
@@ -168,6 +168,7 @@
"/api/configure/add": {
"post": {
"summary": "Add a new mission configuration",
+ "description": "Creates a new mission configuration. Requires SuperAdmin permissions (111).",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -225,6 +226,13 @@
"status": "failure",
"message": "Failed to create new mission."
}
+ },
+ "failure_unauthorized": {
+ "summary": "Failure: Unauthorized",
+ "value": {
+ "status": "failure",
+ "message": "Only SuperAdmins can add new missions."
+ }
}
}
}
@@ -236,6 +244,7 @@
"/api/configure/upsert": {
"post": {
"summary": "Upsert a mission configuration",
+ "description": "Updates or creates a mission configuration. SuperAdmins can access all missions, regular Admins can only access missions they manage.",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -356,6 +365,55 @@
}
}
},
+ "/api/configure/user-permissions": {
+ "get": {
+ "summary": "Get current user's mission permissions",
+ "description": "Returns the current user's permission level and which missions they can manage (for Configure page UI).",
+ "tags": ["Configure"],
+ "responses": {
+ "200": {
+ "description": "User permissions response",
+ "content": {
+ "application/json": {
+ "examples": {
+ "superadmin": {
+ "summary": "SuperAdmin permissions",
+ "value": {
+ "status": "success",
+ "permission": "111",
+ "missions_managing": null
+ }
+ },
+ "admin": {
+ "summary": "Admin with specific missions",
+ "value": {
+ "status": "success",
+ "permission": "110",
+ "missions_managing": ["mission1", "mission2"]
+ }
+ },
+ "user": {
+ "summary": "Regular user",
+ "value": {
+ "status": "success",
+ "permission": "001",
+ "missions_managing": []
+ }
+ },
+ "failure": {
+ "summary": "Failure",
+ "value": {
+ "status": "failure",
+ "message": "Failed to get user permissions."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/configure/versions": {
"get": {
"summary": "Get all versions of a mission",
@@ -562,6 +620,7 @@
"/api/configure/addLayer": {
"post": {
"summary": "Add a layer to a mission configuration",
+ "description": "Adds a layer to the specified mission configuration. SuperAdmins can access all missions, regular Admins can only access missions they manage.",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -629,6 +688,7 @@
"/api/configure/updateLayer": {
"post": {
"summary": "Update a layer in a mission configuration",
+ "description": "Updates an existing layer in the specified mission configuration. SuperAdmins can access all missions, regular Admins can only access missions they manage.",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -696,6 +756,7 @@
"/api/configure/removeLayer": {
"post": {
"summary": "Remove a layer from a mission configuration",
+ "description": "Removes a layer from the specified mission configuration. SuperAdmins can access all missions, regular Admins can only access missions they manage.",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -809,6 +870,7 @@
"/api/configure/updateInitialView": {
"post": {
"summary": "Update the initial view of a mission configuration",
+ "description": "Updates the initial view (lat, lng, zoom) of the specified mission configuration. SuperAdmins can access all missions, regular Admins can only access missions they manage.",
"tags": ["Configure"],
"requestBody": {
"required": true,
@@ -3538,7 +3600,13 @@
"/api/longtermtoken/get": {
"get": {
"summary": "Get all long term tokens",
+ "description": "Returns all existing API tokens. Each token now includes the ID of the admin who created it, which determines the token's mission access permissions.",
"tags": ["API Tokens"],
+ "security": [
+ {
+ "adminAuth": []
+ }
+ ],
"responses": {
"200": {
"description": "Get tokens response",
@@ -3554,6 +3622,10 @@
"id": 1,
"token": "example_token1",
"period": "example_period1",
+ "created_by_user_id": 5,
+ "created_by_username": "admin_user",
+ "created_by_permission": "111",
+ "created_by_missions": null,
"createdAt": "2025-02-28T20:00:00Z",
"updatedAt": "2025-02-28T20:00:00Z"
},
@@ -3561,6 +3633,10 @@
"id": 2,
"token": "example_token2",
"period": "example_period2",
+ "created_by_user_id": 7,
+ "created_by_username": "mission_admin",
+ "created_by_permission": "110",
+ "created_by_missions": ["Mars2020", "InSight"],
"createdAt": "2025-02-28T20:00:00Z",
"updatedAt": "2025-02-28T20:00:00Z"
}
@@ -3584,7 +3660,13 @@
"/api/longtermtoken/generate": {
"post": {
"summary": "Generate a new long term token",
+ "description": "Creates a new API token for programmatic access. The token inherits the mission permissions of the admin who creates it. SuperAdmins get tokens with access to all missions, while regular Admins get tokens restricted to their assigned missions.",
"tags": ["API Tokens"],
+ "security": [
+ {
+ "adminAuth": []
+ }
+ ],
"requestBody": {
"required": true,
"content": {
@@ -3641,7 +3723,13 @@
"/api/longtermtoken/clear": {
"post": {
"summary": "Clear (delete) a long term token",
+ "description": "Deletes an existing API token by its ID.",
"tags": ["API Tokens"],
+ "security": [
+ {
+ "adminAuth": []
+ }
+ ],
"requestBody": {
"required": true,
"content": {
@@ -4273,6 +4361,16 @@
"username": "my_username",
"email": "my@email.com",
"permission": "001",
+ "missions_managing": null,
+ "createdAt": "2023-02-10T01:48:24.961Z",
+ "updatedAt": "2025-01-14T07:32:30.857Z"
+ },
+ {
+ "id": 7,
+ "username": "admin_user",
+ "email": "admin@email.com",
+ "permission": "110",
+ "missions_managing": ["mission1", "mission2"],
"createdAt": "2023-02-10T01:48:24.961Z",
"updatedAt": "2025-01-14T07:32:30.857Z"
}
@@ -4367,7 +4465,16 @@
"type": "string"
},
"permission": {
- "type": "string"
+ "type": "string",
+ "enum": ["110", "001"],
+ "description": "User permission level: 110 for Admin, 001 for User"
+ },
+ "missions_managing": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Array of mission names this admin can manage (only applies when permission is 110)"
}
},
"required": ["id"]
diff --git a/scripts/server.js b/scripts/server.js
index 6ecfb656a..4216f4a9b 100644
--- a/scripts/server.js
+++ b/scripts/server.js
@@ -309,7 +309,8 @@ function ensureAdmin(toLoginPage, denyLongTermTokens, allowGets, disallow) {
url.endsWith("/api/geodatasets/aggregations") ||
url.endsWith("/api/geodatasets/search") ||
url.endsWith("/api/datasets/get") ||
- req.session.permission === "111"
+ req.session.permission === "111" ||
+ req.session.permission === "110"
) {
next();
return;
@@ -333,8 +334,10 @@ function ensureAdmin(toLoginPage, denyLongTermTokens, allowGets, disallow) {
if (!denyLongTermTokens && req.headers.authorization) {
validateLongTermToken(
req.headers.authorization,
- () => {
+ (tokenData) => {
req.isLongTermToken = true;
+ req.tokenUserPermission = tokenData.permission;
+ req.tokenUserMissions = tokenData.missions_managing;
next();
},
() => {
@@ -365,7 +368,7 @@ function validateLongTermToken(token, successCallback, failureCallback) {
token = token.replace(/Bearer:?\s+/g, "");
sequelize
- .query('SELECT * FROM "long_term_tokens" WHERE "token"=:token', {
+ .query('SELECT lt.*, u.permission, u.missions_managing FROM "long_term_tokens" lt JOIN "users" u ON lt.created_by_user_id = u.id WHERE lt.token=:token', {
replacements: {
token: token,
},
@@ -380,6 +383,7 @@ function validateLongTermToken(token, successCallback, failureCallback) {
if (
result &&
result.token == token &&
+ result.created_by_user_id != null && // Block tokens without creator ID (legacy tokens)
(result.period == "never" ||
Date.now() - new Date(result.createdAt).getTime() <
parseInt(result.period))
@@ -405,8 +409,10 @@ function ensureUser() {
req.headers["x-forwarded-for"] || req.connection.remoteAddress;
validateLongTermToken(
req.headers.authorization,
- () => {
+ (tokenData) => {
req.isLongTermToken = true;
+ req.tokenUserPermission = tokenData.permission;
+ req.tokenUserMissions = tokenData.missions_managing;
next();
},
() => {