From 18e9453fed02fca52256549670e816fc02f85ca4 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Fri, 8 Aug 2025 18:36:30 +0200 Subject: [PATCH 1/6] [ADD] JS functions to the AA framework --- .../framework/js/auth-framework.js | 219 ++++++++++++++++++ .../templates/allianceauth/base-bs5.html | 1 + .../templates/bundles/auth-framework-js.html | 3 + docs/development/custom/aa-framework.md | 1 + docs/development/custom/framework/js.md | 102 ++++++++ 5 files changed, 326 insertions(+) create mode 100644 allianceauth/framework/static/allianceauth/framework/js/auth-framework.js create mode 100644 allianceauth/templates/bundles/auth-framework-js.html create mode 100644 docs/development/custom/framework/js.md diff --git a/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js b/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js new file mode 100644 index 00000000..b9e06391 --- /dev/null +++ b/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js @@ -0,0 +1,219 @@ +/* jshint -W097 */ +'use strict'; + +/** + * Checks if the given item is an array. + * + * @usage + * ```javascript + * if (isArray(someVariable)) { + * console.log('This is an array'); + * } else { + * console.log('This is not an array'); + * } + * ``` + * + * @param {*} item - The item to check. + * @returns {boolean} True if the item is an array, false otherwise. + */ +const isArray = (item) => { + return Array.isArray(item); +}; + +/** + * Checks if the given item is a plain object, excluding arrays and dates. + * + * @usage + * ```javascript + * if (isObject(someVariable)) { + * console.log('This is a plain object'); + * } else { + * console.log('This is not a plain object'); + * } + * ``` + * + * @param {*} item - The item to check. + * @returns {boolean} True if the item is a plain object, false otherwise. + */ +const isObject = (item) => { + return ( + item && typeof item === 'object' && !isArray(item) && !(item instanceof Date) + ); +}; + +/** + * Fetch data from an ajax URL + * + * Do not call this function directly, use `fetchGet` or `fetchPost` instead. + * + * @param {string} url The URL to fetch data from + * @param {string} method The HTTP method to use for the request (default: 'get') + * @param {string|null} csrfToken The CSRF token to include in the request headers (default: null) + * @param {string|null} payload The payload (JSON|Object) to send with the request (default: null) + * @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true) + * @returns {Promise} The fetched data + * @throws {Error} Throws an error when: + * - The method is not valid (only `get` and `post` are allowed). + * - The CSRF token is required but not provided for POST requests. + * - The payload is not an object when using POST method. + * - The response status is not OK (HTTP 200-299). + * - There is a network error or if the response cannot be parsed as JSON. + */ +const _fetchAjaxData = async ({ + url, + method = 'get', + csrfToken = null, + payload = null, + responseIsJson = true +}) => { + const normalizedMethod = method.toLowerCase(); + + // Validate the method + const validMethods = ['get', 'post']; + + if (!validMethods.includes(normalizedMethod)) { + throw new Error(`Invalid method: ${method}. Valid methods are: get, post`); + } + + const headers = {}; + + // Set headers based on response type + if (responseIsJson) { + headers['Accept'] = 'application/json'; // jshint ignore:line + headers['Content-Type'] = 'application/json'; + } + + let requestUrl = url; + let body = null; + + if (normalizedMethod === 'post') { + if (!csrfToken) { + throw new Error('CSRF token is required for POST requests'); + } + + headers['X-CSRFToken'] = csrfToken; + + if (payload !== null && !isObject(payload)) { + throw new Error('Payload must be an object when using POST method'); + } + + body = payload ? JSON.stringify(payload) : null; + } else if (normalizedMethod === 'get' && payload) { + const queryParams = new URLSearchParams(payload).toString(); // jshint ignore:line + + requestUrl += (url.includes('?') ? '&' : '?') + queryParams; + } + + /** + * Throws an error with a formatted message. + * + * @param {Response} response The error object containing the message to throw. + */ + const throwHTTPStatusError = (response) => { + throw new Error(`Error: ${response.status} - ${response.statusText}`); + }; + + try { + const response = await fetch(requestUrl, { + method: method.toUpperCase(), + headers: headers, + body: body + }); + + /** + * Throws an error if the response status is not OK (HTTP 200-299). + * This is used to handle HTTP errors gracefully. + */ + if (!response.ok) { + throwHTTPStatusError(response); + } + + return responseIsJson ? await response.json() : await response.text(); + } catch (error) { + // Log the error message to the console + console.log(`Error: ${error.message}`); + + throw error; + } +}; + +/** + * Fetch data from an ajax URL using the GET method. + * This function is a wrapper around _fetchAjaxData to simplify GET requests. + * + * @usage + * ```javascript + * fetchGet({ + * url: url, + * responseIsJson: false + * }).then((data) => { + * // Process the fetched data + * }).catch((error) => { + * console.error(`Error: ${error.message}`); + * + * // Handle the error appropriately + * }); + * ``` + * + * @param {string} url The URL to fetch data from + * @param {string|null} payload The payload (JSON) to send with the request (default: null) + * @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true) + * @return {Promise} The fetched data + */ +const fetchGet = ({ + url, + payload = null, + responseIsJson = true +}) => { + return _fetchAjaxData({ + url: url, + method: 'get', + payload: payload, + responseIsJson: responseIsJson + }); +}; + +/** + * Fetch data from an ajax URL using the POST method. + * This function is a wrapper around _fetchAjaxData to simplify POST requests. + * It requires a CSRF token for security purposes. + * + * @usage + * ```javascript + * fetchPost({ + * url: url, + * csrfToken: csrfToken, + * payload: { + * key: 'value', + * anotherKey: 'anotherValue' + * }, + * responseIsJson: true + * }).then((data) => { + * // Process the fetched data + * }).catch((error) => { + * console.error(`Error: ${error.message}`); + * + * // Handle the error appropriately + * }); + * ``` + * + * @param {string} url The URL to fetch data from + * @param {string|null} csrfToken The CSRF token to include in the request headers (default: null) + * @param {string|null} payload The payload (JSON) to send with the request (default: null) + * @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true) + * @return {Promise} The fetched data + */ +const fetchPost = ({ + url, + csrfToken, + payload = null, + responseIsJson = true +}) => { + return _fetchAjaxData({ + url: url, + method: 'post', + csrfToken: csrfToken, + payload: payload, + responseIsJson: responseIsJson + }); +}; diff --git a/allianceauth/templates/allianceauth/base-bs5.html b/allianceauth/templates/allianceauth/base-bs5.html index d96049f1..ad64120e 100644 --- a/allianceauth/templates/allianceauth/base-bs5.html +++ b/allianceauth/templates/allianceauth/base-bs5.html @@ -21,6 +21,7 @@ {% theme_css %} {% include 'bundles/fontawesome.html' %} + {% include 'bundles/auth-framework-js.html' %} {% include 'bundles/auth-framework-css.html' %}