Convert backend to ESM

- About 5 years overdue
- Remove eslint, use bomejs instead
This commit is contained in:
Jamie Curnow
2025-09-02 21:43:00 +10:00
parent 5b6ca1bf00
commit a12553fec7
89 changed files with 4799 additions and 5107 deletions

View File

@@ -4,28 +4,32 @@
* "scope" in this file means "where did this token come from and what is using it", so 99% of the time
* the "scope" is going to be "user" because it would be a user token. This is not to be confused with
* the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
*
*
*/
const _ = require('lodash');
const logger = require('../logger').access;
const Ajv = require('ajv/dist/2020');
const error = require('./error');
const userModel = require('../models/user');
const proxyHostModel = require('../models/proxy_host');
const TokenModel = require('../models/token');
const roleSchema = require('./access/roles.json');
const permsSchema = require('./access/permissions.json');
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import { access as logger } from "../logger.js";
import proxyHostModel from "../models/proxy_host.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import permsSchema from "./access/permissions.json" with { type: "json" };
import roleSchema from "./access/roles.json" with { type: "json" };
import errs from "./error.js";
module.exports = function (token_string) {
let Token = new TokenModel();
let token_data = null;
let initialised = false;
let object_cache = {};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default function (token_string) {
const Token = TokenModel();
let token_data = null;
let initialised = false;
const object_cache = {};
let allow_internal_access = false;
let user_roles = [];
let permissions = {};
let user_roles = [];
let permissions = {};
/**
* Loads the Token object from the token string
@@ -37,10 +41,10 @@ module.exports = function (token_string) {
if (initialised) {
resolve();
} else if (!token_string) {
reject(new error.PermissionError('Permission Denied'));
reject(new errs.PermissionError("Permission Denied"));
} else {
resolve(Token.load(token_string)
.then((data) => {
resolve(
Token.load(token_string).then((data) => {
token_data = data;
// At this point we need to load the user from the DB and make sure they:
@@ -48,21 +52,25 @@ module.exports = function (token_string) {
// - still have the appropriate scopes for this token
// This is only required when the User ID is supplied or if the token scope has `user`
if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) {
if (
token_data.attrs.id ||
(typeof token_data.scope !== "undefined" &&
_.indexOf(token_data.scope, "user") !== -1)
) {
// Has token user id or token user scope
return userModel
.query()
.where('id', token_data.attrs.id)
.andWhere('is_deleted', 0)
.andWhere('is_disabled', 0)
.allowGraph('[permissions]')
.withGraphFetched('[permissions]')
.where("id", token_data.attrs.id)
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.allowGraph("[permissions]")
.withGraphFetched("[permissions]")
.first()
.then((user) => {
if (user) {
// make sure user has all scopes of the token
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
user.roles.push('user');
user.roles.push("user");
let is_ok = true;
_.forEach(token_data.scope, (scope_item) => {
@@ -72,21 +80,19 @@ module.exports = function (token_string) {
});
if (!is_ok) {
throw new error.AuthError('Invalid token scope for User');
} else {
initialised = true;
user_roles = user.roles;
permissions = user.permissions;
throw new errs.AuthError("Invalid token scope for User");
}
initialised = true;
user_roles = user.roles;
permissions = user.permissions;
} else {
throw new error.AuthError('User cannot be loaded for Token');
throw new errs.AuthError("User cannot be loaded for Token");
}
});
} else {
initialised = true;
}
}));
initialised = true;
}),
);
}
});
};
@@ -101,53 +107,55 @@ module.exports = function (token_string) {
*/
this.loadObjects = (object_type) => {
return new Promise((resolve, reject) => {
if (Token.hasScope('user')) {
if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) {
reject(new error.AuthError('User Token supplied without a User ID'));
if (Token.hasScope("user")) {
if (
typeof token_data.attrs.id === "undefined" ||
!token_data.attrs.id
) {
reject(new errs.AuthError("User Token supplied without a User ID"));
} else {
let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
const token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
let query;
if (typeof object_cache[object_type] === 'undefined') {
if (typeof object_cache[object_type] === "undefined") {
switch (object_type) {
// USERS - should only return yourself
case 'users':
resolve(token_user_id ? [token_user_id] : []);
break;
// USERS - should only return yourself
case "users":
resolve(token_user_id ? [token_user_id] : []);
break;
// Proxy Hosts
case 'proxy_hosts':
query = proxyHostModel
.query()
.select('id')
.andWhere('is_deleted', 0);
case "proxy_hosts":
query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
if (permissions.visibility === 'user') {
query.andWhere('owner_user_id', token_user_id);
}
if (permissions.visibility === "user") {
query.andWhere("owner_user_id", token_user_id);
}
resolve(query
.then((rows) => {
let result = [];
_.forEach(rows, (rule_row) => {
result.push(rule_row.id);
});
resolve(
query.then((rows) => {
const result = [];
_.forEach(rows, (rule_row) => {
result.push(rule_row.id);
});
// enum should not have less than 1 item
if (!result.length) {
result.push(0);
}
// enum should not have less than 1 item
if (!result.length) {
result.push(0);
}
return result;
})
);
break;
return result;
}),
);
break;
// DEFAULT: null
default:
resolve(null);
break;
default:
resolve(null);
break;
}
} else {
resolve(object_cache[object_type]);
@@ -156,11 +164,10 @@ module.exports = function (token_string) {
} else {
resolve(null);
}
})
.then((objects) => {
object_cache[object_type] = objects;
return objects;
});
}).then((objects) => {
object_cache[object_type] = objects;
return objects;
});
};
/**
@@ -170,50 +177,48 @@ module.exports = function (token_string) {
* @returns {Object}
*/
this.getObjectSchema = (permission_label) => {
let base_object_type = permission_label.split(':').shift();
const base_object_type = permission_label.split(":").shift();
let schema = {
$id: 'objects',
description: 'Actor Properties',
type: 'object',
const schema = {
$id: "objects",
description: "Actor Properties",
type: "object",
additionalProperties: false,
properties: {
properties: {
user_id: {
anyOf: [
{
type: 'number',
enum: [Token.get('attrs').id]
}
]
type: "number",
enum: [Token.get("attrs").id],
},
],
},
scope: {
type: 'string',
pattern: '^' + Token.get('scope') + '$'
}
}
type: "string",
pattern: `^${Token.get("scope")}$`,
},
},
};
return this.loadObjects(base_object_type)
.then((object_result) => {
if (typeof object_result === 'object' && object_result !== null) {
schema.properties[base_object_type] = {
type: 'number',
enum: object_result,
minimum: 1
};
} else {
schema.properties[base_object_type] = {
type: 'number',
minimum: 1
};
}
return this.loadObjects(base_object_type).then((object_result) => {
if (typeof object_result === "object" && object_result !== null) {
schema.properties[base_object_type] = {
type: "number",
enum: object_result,
minimum: 1,
};
} else {
schema.properties[base_object_type] = {
type: "number",
minimum: 1,
};
}
return schema;
});
return schema;
});
};
return {
token: Token,
/**
@@ -222,7 +227,7 @@ module.exports = function (token_string) {
* @returns {Promise}
*/
load: (allow_internal) => {
return new Promise(function (resolve/*, reject*/) {
return new Promise((resolve /*, reject*/) => {
if (token_string) {
resolve(Token.load(token_string));
} else {
@@ -240,68 +245,60 @@ module.exports = function (token_string) {
* @param {*} [data]
* @returns {Promise}
*/
can: (permission, data) => {
can: async (permission, data) => {
if (allow_internal_access === true) {
return Promise.resolve(true);
//return true;
} else {
return this.init()
.then(() => {
// Initialised, token decoded ok
return this.getObjectSchema(permission)
.then((objectSchema) => {
const data_schema = {
[permission]: {
data: data,
scope: Token.get('scope'),
roles: user_roles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates
}
};
let permissionSchema = {
$async: true,
$id: 'permissions',
type: 'object',
additionalProperties: false,
properties: {}
};
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [
roleSchema,
permsSchema,
objectSchema,
permissionSchema
]
});
return ajv.validate('permissions', data_schema)
.then(() => {
return data_schema[permission];
});
});
})
.catch((err) => {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw new error.PermissionError('Permission Denied', err);
});
return true;
}
}
try {
await this.init();
const objectSchema = await this.getObjectSchema(permission);
const dataSchema = {
[permission]: {
data: data,
scope: Token.get("scope"),
roles: user_roles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
},
};
const permissionSchema = {
$async: true,
$id: "permissions",
type: "object",
additionalProperties: false,
properties: {},
};
const rawData = fs.readFileSync(
`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`,
{ encoding: "utf8" },
);
permissionSchema.properties[permission] = JSON.parse(rawData);
const ajv = new Ajv({
verbose: true,
allErrors: true,
breakOnError: true,
coerceTypes: true,
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
});
const valid = ajv.validate("permissions", dataSchema);
return valid && dataSchema[permission];
} catch (err) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
}
},
};
};
}