diff --git a/.gitignore b/.gitignore index 0c34bddb..17f74627 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .keys out +**/node_modules +db.* diff --git a/core/app.js b/core/app.js index 1960a90c..1b1ae75e 100644 --- a/core/app.js +++ b/core/app.js @@ -6,20 +6,37 @@ let g_calls = {}; let gSessionIndex = 0; +/** + * TODOC + * @returns + */ function makeSessionId() { return (gSessionIndex++).toString(); } +/** + * TODOC + * @returns + */ function App() { this._on_output = null; this._send_queue = []; return this; } +/** + * TODOC + * @param {*} callback + */ App.prototype.readOutput = function(callback) { this._on_output = callback; } +/** + * TODOC + * @param {*} api + * @returns + */ App.prototype.makeFunction = function(api) { let self = this; let result = function() { @@ -43,6 +60,10 @@ App.prototype.makeFunction = function(api) { return result; } +/** + * TODOC + * @param {*} message + */ App.prototype.send = function(message) { if (this._send_queue) { if (this._on_output) { @@ -57,11 +78,17 @@ App.prototype.send = function(message) { } } +/** + * TODOC + * @param {*} request + * @param {*} response + * @param {*} client + */ function socket(request, response, client) { let process; let options = {}; let credentials = auth.query(request.headers); - let refresh = auth.make_refresh(credentials); + let refresh = auth.makeRefresh(credentials); response.onClose = async function() { if (process && process.task) { diff --git a/core/auth.js b/core/auth.js index 580e403a..befd13a2 100644 --- a/core/auth.js +++ b/core/auth.js @@ -5,9 +5,15 @@ let gDatabase = new Database("auth"); const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000; +/** + * Makes a Base64 value URL safe + * @param {string} value + * @returns TODOC + */ function b64url(value) { value = value.replaceAll('+', '-').replaceAll('/', '_'); let equals = value.indexOf('='); + if (equals !== -1) { return value.substring(0, equals); } else { @@ -15,9 +21,15 @@ function b64url(value) { } } +/** + * TODOC + * @param {string} value + * @returns + */ function unb64url(value) { value = value.replaceAll('-', '+').replaceAll('_', '/'); let remainder = value.length % 4; + if (remainder == 3) { return value + '='; } else if (remainder == 2) { @@ -27,31 +39,68 @@ function unb64url(value) { } } +/** + * Creates a JSON Web Token + * @param {object} payload Object: {"name": "username"} + * @returns the JWT + */ function makeJwt(payload) { - let ids = ssb.getIdentities(':auth'); + const ids = ssb.getIdentities(':auth'); let id; + if (ids?.length) { id = ids[0]; } else { id = ssb.createIdentity(':auth'); } - let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval})))); - let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.'); + const final_payload = b64url( + base64Encode( + JSON.stringify( + Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval} + ) + ) + ) + ); + + const jwt = [ + b64url( + base64Encode( + JSON.stringify({ + alg: 'HS256', + typ: 'JWT' + }) + ) + ), + final_payload, + b64url( + ssb.hmacsha256sign(final_payload, ':auth', id) + ) + ].join('.'); + return jwt; } +/** + * Validates a JWT ? + * @param {*} session TODOC + * @returns + */ function readSession(session) { let jwt_parts = session?.split('.'); + if (jwt_parts?.length === 3) { let [header, payload, signature] = jwt_parts; header = JSON.parse(utf8Decode(base64Decode(unb64url(header)))); + if (header.typ === 'JWT' && header.alg === 'HS256') { signature = unb64url(signature); let id = ssb.getIdentities(':auth'); + if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) { - let result = JSON.parse(utf8Decode(base64Decode(unb64url(payload)))); - let now = new Date().valueOf() + const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload)))); + const now = new Date().valueOf() + if (now < result.exp) { print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`); return result; @@ -67,21 +116,42 @@ function readSession(session) { } } +/** + * Check the provided password matches the hash + * @param {string} password + * @param {string} hash bcrypt hash + * @returns true if the password matches the hash + */ function verifyPassword(password, hash) { - return bCrypt.hashpw(password, hash) == hash; + return bCrypt.hashpw(password, hash) === hash; } +/** + * Hashes a password + * @param {string} password + * @returns {string} TODOC + */ function hashPassword(password) { let salt = bCrypt.gensalt(12); return bCrypt.hashpw(password, salt); } +/** + * Check if there is an administrator on the instance + * @returns TODOC + */ function noAdministrator() { - return !core.globalSettings || !core.globalSettings.permissions || !Object.keys(core.globalSettings.permissions).some(function(name) { + return !core.globalSettings || + !core.globalSettings.permissions || + !Object.keys(core.globalSettings.permissions).some(function(name) { return core.globalSettings.permissions[name].indexOf("administration") != -1; }); } +/** + * Makes a user an administrator + * @param {string} name the user's name + */ function makeAdministrator(name) { if (!core.globalSettings.permissions) { core.globalSettings.permissions = {}; @@ -92,9 +162,15 @@ function makeAdministrator(name) { if (core.globalSettings.permissions[name].indexOf("administration") == -1) { core.globalSettings.permissions[name].push("administration"); } + core.setGlobalSettings(core.globalSettings); } +/** + * TODOC + * @param {*} headers most likely an object + * @returns + */ function getCookies(headers) { let cookies = {}; @@ -111,13 +187,27 @@ function getCookies(headers) { return cookies; } +/** + * Validates a username + * @param {string} name + * @returns false | boolean[] ? + */ function isNameValid(name) { + // TODO(tasiaiso): convert this into a regex let c = name.charAt(0); return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9')); } +/** + * Request handler ? + * @param {*} request TODOC + * @param {*} response + * @returns + */ function handler(request, response) { + // TODO(tasiaiso): split this function let session = getCookies(request.headers).session; + if (request.uri == "/login") { let formData = form.decodeForm(request.query); if (query(request.headers)?.permissions?.authenticated) { @@ -226,6 +316,11 @@ function handler(request, response) { } } +/** + * Gets a user's permissions based on it's session ? + * @param {*} session TODOC + * @returns + */ function getPermissions(session) { let permissions; let entry = readSession(session); @@ -236,6 +331,11 @@ function getPermissions(session) { return permissions || {}; } +/** + * Get a user's permissions ? + * @param {string} userName TODOC + * @returns + */ function getPermissionsForUser(userName) { let permissions = {}; if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) { @@ -246,6 +346,11 @@ function getPermissionsForUser(userName) { return permissions; } +/** + * TODOC + * @param {*} headers + * @returns + */ function query(headers) { let session = getCookies(headers).session; let entry; @@ -258,7 +363,12 @@ function query(headers) { } } -function make_refresh(credentials) { +/** + * Refreshes a JWT ? + * @param {*} credentials TODOC + * @returns + */ +function makeRefresh(credentials) { if (credentials?.session?.name) { return { token: makeJwt({name: credentials.session.name}), @@ -267,4 +377,4 @@ function make_refresh(credentials) { } } -export { handler, query, make_refresh }; +export { handler, query, makeRefresh }; diff --git a/core/client.js b/core/client.js index a85e8fac..e5263577 100644 --- a/core/client.js +++ b/core/client.js @@ -12,7 +12,7 @@ let gOriginalInput; let kErrorColor = "#dc322f"; let kStatusColor = "#fff"; -/* Functions that server-side app code can call through the app object. */ +// Functions that server-side app code can call through the app object. const k_api = { setDocument: {args: ['content'], func: api_setDocument}, postMessage: {args: ['message'], func: api_postMessage}, @@ -24,6 +24,7 @@ const k_api = { setHash: {args: ['hash'], func: api_setHash}, }; +// TODO(tasiaiso): this is only used once, move it down ? const k_global_style = css` a:link { color: #268bd2; @@ -42,6 +43,9 @@ const k_global_style = css` } `; +/** + * Class that represents the top bar + */ class TfNavigationElement extends LitElement { static get properties() { return { @@ -63,6 +67,10 @@ class TfNavigationElement extends LitElement { this.spark_lines = {}; } + /** + * TODOC + * @param {*} event + */ toggle_edit(event) { event.preventDefault(); if (editing()) { @@ -72,10 +80,20 @@ class TfNavigationElement extends LitElement { } } + /** + * TODOC + * @param {*} key + */ reset_permission(key) { send({action: "resetPermission", permission: key}); } + /** + * TODOC + * @param {*} key + * @param {*} options + * @returns + */ get_spark_line(key, options) { if (!this.spark_lines[key]) { let spark_line = document.createElement('tf-sparkline'); @@ -94,6 +112,10 @@ class TfNavigationElement extends LitElement { return this.spark_lines[key]; } + /** + * TODOC + * @returns + */ render_login() { if (this?.credentials?.session?.name) { return html`logout ${this.credentials.session.name}`; @@ -102,6 +124,10 @@ class TfNavigationElement extends LitElement { } } + /** + * TODOC + * @returns + */ render_permissions() { if (this.show_permissions) { return html` @@ -122,6 +148,10 @@ class TfNavigationElement extends LitElement { } } + /** + * TODOC + * @returns + */ render() { let self = this; return html` @@ -157,8 +187,12 @@ class TfNavigationElement extends LitElement { `; } } + customElements.define('tf-navigation', TfNavigationElement); +/** + * TODOC + */ class TfFilesElement extends LitElement { static get properties() { return { @@ -174,6 +208,10 @@ class TfFilesElement extends LitElement { this.dropping = 0; } + /** + * TODOC + * @param {*} file + */ file_click(file) { this.dispatchEvent(new CustomEvent('file_click', { detail: { @@ -184,6 +222,11 @@ class TfFilesElement extends LitElement { })); } + /** + * TODOC + * @param {*} file + * @returns + */ render_file(file) { let classes = ['file']; if (file == this.current) { @@ -195,6 +238,10 @@ class TfFilesElement extends LitElement { return html`