Javascript linting (#3329)

* feat(ci/dev): adding npm/eslint config for javascript linting
* feat(ci/dev): adding js-prettier config for consistency in formatting
* fix(css): linting
* fix(js): linting the js things
This commit is contained in:
James Hodgkinson 2025-01-04 15:25:46 +10:00 committed by GitHub
parent 3430a1c31d
commit b74883ae0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1323 additions and 73 deletions

View file

@ -2,9 +2,21 @@
root = true
[*.md]
charset = utf-8
end_of_line = lf
indent_size = 2
max_line_length = 100
trim_trailing_whitespace = true
[*.js]
tab_width = 4
max_line_length = 120
print_width = 120
[*.mjs]
tab_width = 4
max_line_length = 120
print_width = 120

21
.github/workflows/javascript_lint.yml vendored Normal file
View file

@ -0,0 +1,21 @@
---
name: Javascript Linting
"on":
- push
- pull_request
jobs:
javascript_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run ESLint to check Javascript files
run:
make eslint
javascript_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Prettier to check Javascript files
run:
make prettier

3
.gitignore vendored
View file

@ -45,3 +45,6 @@ scripts/oauth_proxy/envfile
# IDEs
.idea/
.vscode/
# javascript test things
node_modules/

View file

@ -325,4 +325,33 @@ coverage: ## Run the coverage tests using cargo-tarpaulin
coveralls: ## Run cargo tarpaulin and upload to coveralls
coveralls:
cargo tarpaulin --coveralls $(COVERALLS_REPO_TOKEN)
@echo "Coveralls repo information is at https://coveralls.io/github/kanidm/kanidm"
@echo "Coveralls repo information is at https://coveralls.io/github/kanidm/kanidm"
.PHONY: eslint
eslint: ## Run eslint on the UI javascript things
eslint: eslint/setup
@echo "################################"
@echo " Running eslint..."
@echo "################################"
cd server/core && find ./static -name '*js' -not -path '*/external/*' -exec eslint "{}" \;
@echo "################################"
@echo "Done!"
.PHONY: eslint/setup
eslint/setup: ## Install eslint for the UI javascript things
cd server/core && npm ci
.PHONY: prettier
prettier: ## Run prettier on the UI javascript things
prettier: eslint/setup
@echo " Running prettier..."
cd server/core && npm run prettier
@echo "Done!"
.PHONY: prettier/fix
prettier/fix: ## Run prettier on the UI javascript things and write back changes
prettier/fix: eslint/setup
@echo " Running prettier..."
cd server/core && npm run prettier:fix
@echo "Done!"

View file

@ -0,0 +1,16 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
languageOptions: {
globals: {
...globals.browser,
Base64 : "writeable" // to feed the Base64 class into the global scope
}
}
},
pluginJs.configs.recommended,
];

1104
server/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
server/core/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "kanidm",
"version": "0.0.1-dev",
"description": "Kanidm UI Javascript - not a publishable package, just for development",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/kanidm/kanidm.git"
},
"author": "",
"license": "MPL-2.0",
"bugs": {
"url": "https://github.com/kanidm/kanidm/issues"
},
"homepage": "https://kanidm.com",
"devDependencies": {
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"globals": "^15.14.0",
"prettier": "^3.4.2"
},
"scripts" : {
"prettier": "npx prettier --ignore-path 'prettier-ignore' ./static --check",
"prettier:fix": "npx prettier --ignore-path 'prettier-ignore' ./static --write"
}
}

View file

@ -0,0 +1 @@
static/external/

View file

@ -1,4 +1,4 @@
console.debug('credupdate: loaded');
console.debug("credupdate: loaded");
// Makes the password form interactive (e.g. shows when passwords don't match)
function setupInteractivePwdFormListeners() {
@ -18,10 +18,10 @@ function setupInteractivePwdFormListeners() {
pwd_submit.disabled = true;
}
new_pwd.addEventListener("input", (_) => {
new_pwd.addEventListener("input", () => {
// Don't mark invalid if user didn't fill in the confirmation box yet
// Also my password manager (keepassxc with autocomplete)
// likes to fire off input events when both inputs were empty.
// Also KeepassXC with autocomplete likes to fire off input events when
// both inputs are empty.
if (new_pwd_check.value !== "") {
if (new_pwd.value === new_pwd_check.value) {
markPwdCheckValid();
@ -32,7 +32,7 @@ function setupInteractivePwdFormListeners() {
new_pwd.classList.remove("is-invalid");
});
new_pwd_check.addEventListener("input", (_) => {
new_pwd_check.addEventListener("input", () => {
// No point in updating the status if confirmation box is empty
if (new_pwd_check.value === "") return;
if (new_pwd_check.value === new_pwd.value) {
@ -43,38 +43,44 @@ function setupInteractivePwdFormListeners() {
});
}
window.stillSwapFailureResponse = function(event) {
window.stillSwapFailureResponse = function (event) {
if (event.detail.xhr.status === 422 || event.detail.xhr.status === 500) {
console.log("Still swapping failure response")
console.debug(`Got HTTP/${event.detail.xhr.status}, still swapping failure response`);
event.detail.shouldSwap = true;
event.detail.isError = false;
}
}
};
function onPasskeyCreated(assertion) {
try {
console.log(assertion)
console.log(assertion);
let creationData = {};
creationData.id = assertion.id;
creationData.rawId = Base64.fromUint8Array(new Uint8Array(assertion.rawId))
creationData.rawId = Base64.fromUint8Array(new Uint8Array(assertion.rawId));
creationData.response = {};
creationData.response.attestationObject = Base64.fromUint8Array(new Uint8Array(assertion.response.attestationObject))
creationData.response.clientDataJSON = Base64.fromUint8Array(new Uint8Array(assertion.response.clientDataJSON))
creationData.type = assertion.type
creationData.extensions = assertion.getClientExtensionResults()
creationData.extensions.uvm = undefined
creationData.response.attestationObject = Base64.fromUint8Array(
new Uint8Array(assertion.response.attestationObject),
);
creationData.response.clientDataJSON = Base64.fromUint8Array(new Uint8Array(assertion.response.clientDataJSON));
creationData.type = assertion.type;
creationData.extensions = assertion.getClientExtensionResults();
creationData.extensions.uvm = undefined;
// Put the passkey creation data into the form for submission
document.getElementById("passkey-create-data").value = JSON.stringify(creationData)
document.getElementById("passkey-create-data").value = JSON.stringify(creationData);
// Make the name input visible and hide the "Begin Passkey Enrollment" button
document.getElementById("passkeyNamingSafariPre").classList.add("d-none")
document.getElementById("passkeyNamingForm").classList.remove("d-none")
document.getElementById("passkeyNamingSubmitBtn").classList.remove("d-none")
document.getElementById("passkeyNamingSafariPre").classList.add("d-none");
document.getElementById("passkeyNamingForm").classList.remove("d-none");
document.getElementById("passkeyNamingSubmitBtn").classList.remove("d-none");
} catch (e) {
console.log(e)
if (confirm("Failed to encode your new passkey's data for transmission, confirm to reload this page.\nReport this issue if it keeps occurring.")) {
console.log(e);
if (
confirm(
"Failed to encode your new passkey's data for transmission, confirm to reload this page.\nReport this issue if it keeps occurring.",
)
) {
window.location.reload();
}
}
@ -82,36 +88,41 @@ function onPasskeyCreated(assertion) {
function startPasskeyEnrollment() {
try {
const data_elem = document.getElementById('data');
const data_elem = document.getElementById("data");
const credentialRequestOptions = JSON.parse(data_elem.textContent);
credentialRequestOptions.publicKey.challenge = Base64.toUint8Array(credentialRequestOptions.publicKey.challenge);
credentialRequestOptions.publicKey.challenge = Base64.toUint8Array(
credentialRequestOptions.publicKey.challenge,
);
credentialRequestOptions.publicKey.user.id = Base64.toUint8Array(credentialRequestOptions.publicKey.user.id);
console.log(credentialRequestOptions)
navigator.credentials
.create({publicKey: credentialRequestOptions.publicKey})
.then((assertion) => {
console.log(credentialRequestOptions);
navigator.credentials.create({ publicKey: credentialRequestOptions.publicKey }).then(
(assertion) => {
onPasskeyCreated(assertion);
}, (reason) => {
alert("Passkey creation failed " + reason.toString())
console.log("Passkey creation failed: " + reason.toString())
});
},
(reason) => {
alert(`Passkey creation failed ${reason.toString()}`);
console.log(`Passkey creation failed: ${reason.toString()}`);
},
);
} catch (e) {
console.log(e)
if (confirm("Failed to initialize passkey creation, confirm to reload this page.\nReport this issue if it keeps occurring.")) {
console.log(`Failed to initialize passkey creation: ${e}`);
if (
confirm(
"Failed to initialize passkey creation, confirm to reload this page.\nReport this issue if it keeps occurring.",
)
) {
window.location.reload();
}
}
}
function setupPasskeyNamingSafariButton() {
document.getElementById("passkeyNamingSafariBtn")
.addEventListener("click", startPasskeyEnrollment)
document.getElementById("passkeyNamingSafariBtn").addEventListener("click", startPasskeyEnrollment);
}
function setupSubmitBtnVisibility() {
document.getElementById("passkey-label")
?.addEventListener("input", updateSubmitButtonVisibility)
document.getElementById("passkey-label")?.addEventListener("input", updateSubmitButtonVisibility);
}
function updateSubmitButtonVisibility(event) {
@ -119,12 +130,14 @@ function updateSubmitButtonVisibility(event) {
submitButton.disabled = event.value === "";
}
(function() {
console.debug('credupdate: init');
document.body.addEventListener("addPasswordSwapped", () => { setupInteractivePwdFormListeners() });
(function () {
console.debug("credupdate: init");
document.body.addEventListener("addPasswordSwapped", () => {
setupInteractivePwdFormListeners();
});
document.body.addEventListener("addPasskeySwapped", () => {
setupPasskeyNamingSafariButton();
startPasskeyEnrollment();
setupSubmitBtnVisibility();
});
})()
})();

View file

@ -1,29 +1,45 @@
/**
* Initiates the passkey login process by requesting credentials from the user.
*
* This function retrieves the credential request options from the DOM, converts
* necessary fields from Base64 to Uint8Array, and then uses the Web Authentication API
* to get the user's credentials. Upon successful retrieval, it encodes the assertion
* response back to Base64 and submits the form with the credential data.
*
* @function asskey_login
* @throws {Error} If the passkey authentication process fails.
*/
function asskey_login() {
let credentialRequestOptions = JSON.parse(document.getElementById('data').textContent);
let credentialRequestOptions = JSON.parse(document.getElementById("data").textContent);
credentialRequestOptions.publicKey.challenge = Base64.toUint8Array(credentialRequestOptions.publicKey.challenge);
credentialRequestOptions.publicKey.allowCredentials?.forEach(function (listItem) {
listItem.id = Base64.toUint8Array(listItem.id)
listItem.id = Base64.toUint8Array(listItem.id);
});
navigator.credentials.get({ publicKey: credentialRequestOptions.publicKey })
.then((assertion) => {
document.getElementById("cred").value = JSON.stringify({
id: assertion.id,
rawId: Base64.fromUint8Array(new Uint8Array(assertion.rawId), true),
type: assertion.type,
response: {
authenticatorData: Base64.fromUint8Array(new Uint8Array(assertion.response.authenticatorData), true),
clientDataJSON: Base64.fromUint8Array(new Uint8Array(assertion.response.clientDataJSON), true),
signature: Base64.fromUint8Array(new Uint8Array(assertion.response.signature), true),
userHandle: Base64.fromUint8Array(new Uint8Array(assertion.response.userHandle), true)
},
navigator.credentials
.get({ publicKey: credentialRequestOptions.publicKey })
.then((assertion) => {
document.getElementById("cred").value = JSON.stringify({
id: assertion.id,
rawId: Base64.fromUint8Array(new Uint8Array(assertion.rawId), true),
type: assertion.type,
response: {
authenticatorData: Base64.fromUint8Array(
new Uint8Array(assertion.response.authenticatorData),
true,
),
clientDataJSON: Base64.fromUint8Array(new Uint8Array(assertion.response.clientDataJSON), true),
signature: Base64.fromUint8Array(new Uint8Array(assertion.response.signature), true),
userHandle: Base64.fromUint8Array(new Uint8Array(assertion.response.userHandle), true),
},
});
document.getElementById("cred-form").submit();
})
.catch((error) => {
console.error(`Failed to complete passkey authentication: ${error}`);
throw error;
});
document.getElementById("cred-form").submit();
}).catch((error) => {
console.error(`Failed to complete passkey authentication: ${error}`);
throw error;
});
}
try {
@ -31,17 +47,23 @@ try {
myButton.addEventListener("click", () => {
asskey_login();
});
} catch (_error) {};
} catch (error) {
console.error(`Failed to add button event listener for passkey authentication: ${error}`);
}
try {
const myButton = document.getElementById("start-seckey-button");
myButton.addEventListener("click", () => {
asskey_login();
});
} catch (_error) {};
} catch (error) {
console.error(`Failed to add button event listener for security key authentication: ${error}`);
}
try {
window.addEventListener("load", (event) => {
asskey_login()
addEventListener("load", () => {
asskey_login();
});
} catch (_error) {};
} catch (error) {
console.error(`Failed to add load-time event listener for passkey authentication: ${error}`);
}

View file

@ -30,14 +30,15 @@ body {
.side-menu-item {
--icon-size: 24px;
padding: .4rem .7rem;
padding: 0.4rem 0.7rem;
text-decoration: none;
&.active {
font-weight: 600;
}
&:hover, &.active {
&:hover,
&.active {
background-color: var(--bs-gray-300);
}
@ -53,7 +54,6 @@ body {
* Personal Settings sidemenu
*/
/*
* Navbar
*/
@ -80,7 +80,9 @@ body {
margin: auto;
border-radius: 15px;
background: #21252915;
box-shadow: -5px -5px 11px #ededed, 5px 5px 11px #ffffff;
box-shadow:
-5px -5px 11px #ededed,
5px 5px 11px #ffffff;
margin: 15px;
}
@ -150,9 +152,9 @@ body {
}
.btn-tiny {
--bs-btn-padding-y: .05rem;
--bs-btn-padding-x: .4rem;
--bs-btn-font-size: .75rem;
--bs-btn-padding-y: 0.05rem;
--bs-btn-padding-x: 0.4rem;
--bs-btn-font-size: 0.75rem;
}
#cred-update-commit-bar {