Compare commits
33 Commits
master
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692be2b476 | ||
|
|
87df009d66 | ||
|
|
049603f71b | ||
|
|
392c182735 | ||
|
|
95b97f8c95 | ||
|
|
eb01bba963 | ||
|
|
991aa1c037 | ||
|
|
e8b4907dfb | ||
|
|
28b40df577 | ||
|
|
b11a1d7765 | ||
|
|
b621a452a2 | ||
|
|
36d0bbdfea | ||
|
|
dec5146c58 | ||
|
|
07c2f75be5 | ||
|
|
3b34a3acf4 | ||
|
|
984f268af4 | ||
|
|
a5dd2ad75a | ||
|
|
d0bdcc575a | ||
|
|
e5f629041f | ||
|
|
3a872f78d0 | ||
|
|
854c54925a | ||
|
|
20f86cdbdd | ||
|
|
6b505447cc | ||
|
|
230b85edb7 | ||
|
|
87bf2247b3 | ||
|
|
3b1e9c01c7 | ||
|
|
a308c802af | ||
|
|
1a5379baec | ||
|
|
9c31da1756 | ||
|
|
125fb1ccf4 | ||
|
|
0a85868c1e | ||
|
|
5cd781bd51 | ||
|
|
1b886f8573 |
4
.github/workflows/autofix.yml
vendored
4
.github/workflows/autofix.yml
vendored
@ -30,10 +30,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Update RDAP DNS data from IANA
|
|
||||||
run: wget -O server/model/rdap-dns.json https://data.iana.org/rdap/dns.json
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Auto-fix JavaScript/Vue linting issues
|
- name: Auto-fix JavaScript/Vue linting issues
|
||||||
run: npm run lint-fix:js
|
run: npm run lint-fix:js
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
"shorthand-property-no-redundant-values": null,
|
"shorthand-property-no-redundant-values": null,
|
||||||
"color-hex-length": null,
|
"color-hex-length": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
"at-rule-no-unknown": null
|
"at-rule-no-unknown": null,
|
||||||
|
"function-no-unknown": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ console.log("== Uptime Kuma Reset Password Tool ==");
|
|||||||
const Database = require("../server/database");
|
const Database = require("../server/database");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const { passwordStrength } = require("check-password-strength");
|
const { validatePassword } = require("../server/password-util");
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
const { initJWTSecret } = require("../server/util-server");
|
||||||
const User = require("../server/model/user");
|
const User = require("../server/model/user");
|
||||||
const { io } = require("socket.io-client");
|
const { io } = require("socket.io-client");
|
||||||
@ -46,15 +46,24 @@ const main = async () => {
|
|||||||
"Warning: the password might be stored, in plain text, in your shell's history"
|
"Warning: the password might be stored, in plain text, in your shell's history"
|
||||||
);
|
);
|
||||||
password = confirmPassword = args["new-password"] + "";
|
password = confirmPassword = args["new-password"] + "";
|
||||||
if (passwordStrength(password).value === "Too weak") {
|
const passwordValidation = await validatePassword(password, true);
|
||||||
throw new Error("Password is too weak, please use a stronger password.");
|
if (!passwordValidation.ok) {
|
||||||
|
throw new Error(passwordValidation.msg);
|
||||||
|
}
|
||||||
|
if (passwordValidation.warning) {
|
||||||
|
console.warn("\x1b[33m%s\x1b[0m",
|
||||||
|
"Warning: " + passwordValidation.warning);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
password = await question("New Password: ");
|
password = await question("New Password: ");
|
||||||
if (passwordStrength(password).value === "Too weak") {
|
const passwordValidation = await validatePassword(password, true);
|
||||||
console.log("Password is too weak, please try again.");
|
if (!passwordValidation.ok) {
|
||||||
|
console.log(passwordValidation.msg);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (passwordValidation.warning) {
|
||||||
|
console.warn("\x1b[33m%s\x1b[0m", "Warning: " + passwordValidation.warning);
|
||||||
|
}
|
||||||
confirmPassword = await question("Confirm New Password: ");
|
confirmPassword = await question("Confirm New Password: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
397
package-lock.json
generated
397
package-lock.json
generated
@ -18,7 +18,6 @@
|
|||||||
"badge-maker": "~3.3.1",
|
"badge-maker": "~3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"chardet": "~1.4.0",
|
"chardet": "~1.4.0",
|
||||||
"check-password-strength": "^2.0.5",
|
|
||||||
"cheerio": "~1.0.0-rc.12",
|
"cheerio": "~1.0.0-rc.12",
|
||||||
"chroma-js": "~2.4.2",
|
"chroma-js": "~2.4.2",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
@ -60,6 +59,7 @@
|
|||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
"net-snmp": "^3.11.2",
|
"net-snmp": "^3.11.2",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-fetch-cache": "^5.1.0",
|
||||||
"node-radius-utils": "~1.2.0",
|
"node-radius-utils": "~1.2.0",
|
||||||
"nodemailer": "~7.0.12",
|
"nodemailer": "~7.0.12",
|
||||||
"nostr-tools": "^2.17.0",
|
"nostr-tools": "^2.17.0",
|
||||||
@ -163,7 +163,8 @@
|
|||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^7.2.0",
|
"wait-on": "^7.2.0",
|
||||||
"whatwg-url": "~12.0.1"
|
"whatwg-url": "~12.0.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20.4.0"
|
"node": ">= 20.4.0"
|
||||||
@ -3654,7 +3655,6 @@
|
|||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
@ -3664,7 +3664,6 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/balanced-match": "^4.0.1"
|
"@isaacs/balanced-match": "^4.0.1"
|
||||||
@ -4028,6 +4027,18 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@npmcli/fs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@npmcli/move-file": {
|
"node_modules/@npmcli/move-file": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz",
|
||||||
@ -5822,40 +5833,6 @@
|
|||||||
"testcontainers": "^11.11.0"
|
"testcontainers": "^11.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@testcontainers/mysql/node_modules/testcontainers": {
|
|
||||||
"version": "11.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz",
|
|
||||||
"integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@balena/dockerignore": "^1.0.2",
|
|
||||||
"@types/dockerode": "^3.3.47",
|
|
||||||
"archiver": "^7.0.1",
|
|
||||||
"async-lock": "^1.4.1",
|
|
||||||
"byline": "^5.0.0",
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"docker-compose": "^1.3.0",
|
|
||||||
"dockerode": "^4.0.9",
|
|
||||||
"get-port": "^7.1.0",
|
|
||||||
"proper-lockfile": "^4.1.2",
|
|
||||||
"properties-reader": "^2.3.0",
|
|
||||||
"ssh-remote-port-forward": "^1.0.4",
|
|
||||||
"tar-fs": "^3.1.1",
|
|
||||||
"tmp": "^0.2.5",
|
|
||||||
"undici": "^7.16.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@testcontainers/mysql/node_modules/undici": {
|
|
||||||
"version": "7.18.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
|
|
||||||
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.18.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@testcontainers/postgresql": {
|
"node_modules/@testcontainers/postgresql": {
|
||||||
"version": "11.11.0",
|
"version": "11.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.11.0.tgz",
|
||||||
@ -5866,40 +5843,6 @@
|
|||||||
"testcontainers": "^11.11.0"
|
"testcontainers": "^11.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@testcontainers/postgresql/node_modules/testcontainers": {
|
|
||||||
"version": "11.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz",
|
|
||||||
"integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@balena/dockerignore": "^1.0.2",
|
|
||||||
"@types/dockerode": "^3.3.47",
|
|
||||||
"archiver": "^7.0.1",
|
|
||||||
"async-lock": "^1.4.1",
|
|
||||||
"byline": "^5.0.0",
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"docker-compose": "^1.3.0",
|
|
||||||
"dockerode": "^4.0.9",
|
|
||||||
"get-port": "^7.1.0",
|
|
||||||
"proper-lockfile": "^4.1.2",
|
|
||||||
"properties-reader": "^2.3.0",
|
|
||||||
"ssh-remote-port-forward": "^1.0.4",
|
|
||||||
"tar-fs": "^3.1.1",
|
|
||||||
"tmp": "^0.2.5",
|
|
||||||
"undici": "^7.16.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@testcontainers/postgresql/node_modules/undici": {
|
|
||||||
"version": "7.18.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
|
|
||||||
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.18.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@testcontainers/rabbitmq": {
|
"node_modules/@testcontainers/rabbitmq": {
|
||||||
"version": "10.28.0",
|
"version": "10.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testcontainers/rabbitmq/-/rabbitmq-10.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testcontainers/rabbitmq/-/rabbitmq-10.28.0.tgz",
|
||||||
@ -8013,6 +7956,85 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cacache": {
|
||||||
|
"version": "20.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz",
|
||||||
|
"integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@npmcli/fs": "^5.0.0",
|
||||||
|
"fs-minipass": "^3.0.0",
|
||||||
|
"glob": "^13.0.0",
|
||||||
|
"lru-cache": "^11.1.0",
|
||||||
|
"minipass": "^7.0.3",
|
||||||
|
"minipass-collect": "^2.0.1",
|
||||||
|
"minipass-flush": "^1.0.5",
|
||||||
|
"minipass-pipeline": "^1.2.4",
|
||||||
|
"p-map": "^7.0.2",
|
||||||
|
"ssri": "^13.0.0",
|
||||||
|
"unique-filename": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacache/node_modules/glob": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.1.1",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"path-scurry": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacache/node_modules/lru-cache": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacache/node_modules/minimatch": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacache/node_modules/path-scurry": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cacheable-lookup": {
|
"node_modules/cacheable-lookup": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||||
@ -8236,12 +8258,6 @@
|
|||||||
"dayjs": "^1.9.7"
|
"dayjs": "^1.9.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/check-password-strength": {
|
|
||||||
"version": "2.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.10.tgz",
|
|
||||||
"integrity": "sha512-HRM5ICPmtnNtLnTv2QrfVkq1IxI9z3bzYpDJ1k5ixwD9HtJGHuv265R6JmHOV6r8wLhQMlULnIUVpkrC2yaiCw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cheerio": {
|
"node_modules/cheerio": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||||
@ -9002,6 +9018,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||||
@ -10675,6 +10700,29 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||||
@ -10885,6 +10933,27 @@
|
|||||||
"node": ">= 14.17"
|
"node": ">= 14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formdata-node": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -10986,6 +11055,18 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-minipass": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@ -11865,7 +11946,6 @@
|
|||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
@ -13066,6 +13146,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/locko": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locko/-/locko-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-pYB2dzRY93fJkg2RIl41AMNgTQftEjyTK9vlPrGOJvuGQsOjb267VJBw15BjiN3RBd1oBoKkOu9E2dRdFKIfAA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@ -13713,6 +13799,18 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass-collect": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass-fetch": {
|
"node_modules/minipass-fetch": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
|
||||||
@ -13749,7 +13847,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
|
||||||
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
|
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^3.0.0"
|
"minipass": "^3.0.0"
|
||||||
},
|
},
|
||||||
@ -13762,7 +13859,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -13775,7 +13871,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
|
||||||
"integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
|
"integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^3.0.0"
|
"minipass": "^3.0.0"
|
||||||
},
|
},
|
||||||
@ -13788,7 +13883,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -14161,6 +14255,26 @@
|
|||||||
"command-exists": "^1.2.9"
|
"command-exists": "^1.2.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@ -14181,6 +14295,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch-cache": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch-cache/-/node-fetch-cache-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-4j3rRHNGIKGX7VzXSrBT0bh7+wFuyJv1DxCfCLDHsnDahJWoD9lXe3BzL3BJg/GEIJiM7KIvqVs3byW1GFtRsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cacache": "^20.0.1",
|
||||||
|
"formdata-node": "^6.0.3",
|
||||||
|
"locko": "^1.1.0",
|
||||||
|
"node-fetch": "3.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch-cache/node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch/node_modules/tr46": {
|
"node_modules/node-fetch/node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@ -14657,6 +14804,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-map": {
|
||||||
|
"version": "7.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
|
||||||
|
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-try": {
|
"node_modules/p-try": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
@ -17214,6 +17373,18 @@
|
|||||||
"nan": "^2.23.0"
|
"nan": "^2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssri": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
@ -18120,27 +18291,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/testcontainers": {
|
"node_modules/testcontainers": {
|
||||||
"version": "11.5.0",
|
"version": "11.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.11.0.tgz",
|
||||||
"integrity": "sha512-RPhuRqJ7OZR5e/uw9UEGbxuKjHGXruLlorRRcJvx429xzVapYammBNxmO2PNUW4M5lM/l6NryOY/AVECXaunSw==",
|
"integrity": "sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/dockerignore": "^1.0.2",
|
"@balena/dockerignore": "^1.0.2",
|
||||||
"@types/dockerode": "^3.3.42",
|
"@types/dockerode": "^3.3.47",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"async-lock": "^1.4.1",
|
"async-lock": "^1.4.1",
|
||||||
"byline": "^5.0.0",
|
"byline": "^5.0.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.3",
|
||||||
"docker-compose": "^1.2.0",
|
"docker-compose": "^1.3.0",
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.9",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"properties-reader": "^2.3.0",
|
"properties-reader": "^2.3.0",
|
||||||
"ssh-remote-port-forward": "^1.0.4",
|
"ssh-remote-port-forward": "^1.0.4",
|
||||||
"tar-fs": "^3.1.0",
|
"tar-fs": "^3.1.1",
|
||||||
"tmp": "^0.2.3",
|
"tmp": "^0.2.5",
|
||||||
"undici": "^7.12.0"
|
"undici": "^7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/testcontainers/node_modules/undici": {
|
"node_modules/testcontainers/node_modules/undici": {
|
||||||
@ -18579,6 +18750,30 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unique-filename": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"unique-slug": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unique-slug": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"imurmurhash": "^0.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unique-string": {
|
"node_modules/unique-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||||
@ -19230,6 +19425,15 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
@ -20131,6 +20335,13 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxcvbn": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,6 @@
|
|||||||
"badge-maker": "~3.3.1",
|
"badge-maker": "~3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"chardet": "~1.4.0",
|
"chardet": "~1.4.0",
|
||||||
"check-password-strength": "^2.0.5",
|
|
||||||
"cheerio": "~1.0.0-rc.12",
|
"cheerio": "~1.0.0-rc.12",
|
||||||
"chroma-js": "~2.4.2",
|
"chroma-js": "~2.4.2",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
"net-snmp": "^3.11.2",
|
"net-snmp": "^3.11.2",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
|
"node-fetch-cache": "^5.1.0",
|
||||||
"node-radius-utils": "~1.2.0",
|
"node-radius-utils": "~1.2.0",
|
||||||
"nodemailer": "~7.0.12",
|
"nodemailer": "~7.0.12",
|
||||||
"nostr-tools": "^2.17.0",
|
"nostr-tools": "^2.17.0",
|
||||||
@ -225,6 +225,7 @@
|
|||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^7.2.0",
|
"wait-on": "^7.2.0",
|
||||||
"whatwg-url": "~12.0.1"
|
"whatwg-url": "~12.0.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,19 +4,33 @@ const { log, TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD } = require("../../src/u
|
|||||||
const { parse: parseTld } = require("tldts");
|
const { parse: parseTld } = require("tldts");
|
||||||
const { setting, setSetting } = require("../util-server");
|
const { setting, setSetting } = require("../util-server");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
|
const { default: NodeFetchCache, MemoryCache } = require("node-fetch-cache");
|
||||||
const TranslatableError = require("../translatable-error");
|
const TranslatableError = require("../translatable-error");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
// Load static RDAP DNS data from local file (auto-updated by CI)
|
const cachedFetch = process.env.NODE_ENV
|
||||||
const rdapDnsData = require("./rdap-dns.json");
|
? NodeFetchCache.create({
|
||||||
|
// cache for 8h
|
||||||
|
cache: new MemoryCache({ ttl: 1000 * 60 * 60 * 8 }),
|
||||||
|
})
|
||||||
|
: fetch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the RDAP server for a given TLD
|
* Find the RDAP server for a given TLD
|
||||||
* @param {string} tld TLD
|
* @param {string} tld TLD
|
||||||
* @returns {string|null} First RDAP server found
|
* @returns {Promise<string>} First RDAP server found
|
||||||
*/
|
*/
|
||||||
function getRdapServer(tld) {
|
async function getRdapServer(tld) {
|
||||||
const services = rdapDnsData["services"] ?? [];
|
let rdapList;
|
||||||
|
try {
|
||||||
|
const res = await cachedFetch("https://data.iana.org/rdap/dns.json");
|
||||||
|
rdapList = await res.json();
|
||||||
|
} catch (error) {
|
||||||
|
log.debug("rdap", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = rdapList["services"] ?? [];
|
||||||
const rootTld = tld?.split(".").pop();
|
const rootTld = tld?.split(".").pop();
|
||||||
if (rootTld) {
|
if (rootTld) {
|
||||||
for (const [tlds, urls] of services) {
|
for (const [tlds, urls] of services) {
|
||||||
@ -36,7 +50,7 @@ function getRdapServer(tld) {
|
|||||||
*/
|
*/
|
||||||
async function getRdapDomainExpiryDate(domain) {
|
async function getRdapDomainExpiryDate(domain) {
|
||||||
const tld = DomainExpiry.parseTld(domain).publicSuffix;
|
const tld = DomainExpiry.parseTld(domain).publicSuffix;
|
||||||
const rdapServer = getRdapServer(tld);
|
const rdapServer = await getRdapServer(tld);
|
||||||
if (rdapServer === null) {
|
if (rdapServer === null) {
|
||||||
log.warn("rdap", `No RDAP server found, TLD ${tld} not supported.`);
|
log.warn("rdap", `No RDAP server found, TLD ${tld} not supported.`);
|
||||||
return null;
|
return null;
|
||||||
@ -164,7 +178,7 @@ class DomainExpiry extends BeanModel {
|
|||||||
|
|
||||||
const publicSuffix = tld.publicSuffix;
|
const publicSuffix = tld.publicSuffix;
|
||||||
const rootTld = publicSuffix.split(".").pop();
|
const rootTld = publicSuffix.split(".").pop();
|
||||||
const rdap = getRdapServer(publicSuffix);
|
const rdap = await getRdapServer(publicSuffix);
|
||||||
if (!rdap) {
|
if (!rdap) {
|
||||||
throw new TranslatableError("domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint", {
|
throw new TranslatableError("domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint", {
|
||||||
publicSuffix,
|
publicSuffix,
|
||||||
|
|||||||
@ -1,21 +1,9 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
|
|
||||||
class Incident extends BeanModel {
|
class Incident extends BeanModel {
|
||||||
/**
|
|
||||||
* Resolve the incident and mark it as inactive
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async resolve() {
|
|
||||||
this.active = false;
|
|
||||||
this.pin = false;
|
|
||||||
this.last_updated_date = R.isoDateTime(dayjs.utc());
|
|
||||||
await R.store(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
* @returns {object} Object ready to parse
|
* @returns {object} Object ready to parse
|
||||||
*/
|
*/
|
||||||
toPublicJSON() {
|
toPublicJSON() {
|
||||||
@ -24,11 +12,9 @@ class Incident extends BeanModel {
|
|||||||
style: this.style,
|
style: this.style,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
content: this.content,
|
content: this.content,
|
||||||
pin: !!this.pin,
|
pin: this.pin,
|
||||||
active: !!this.active,
|
createdDate: this.createdDate,
|
||||||
createdDate: this.created_date,
|
lastUpdatedDate: this.lastUpdatedDate,
|
||||||
lastUpdatedDate: this.last_updated_date,
|
|
||||||
status_page_id: this.status_page_id,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1746,55 +1746,6 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate JSON fields to prevent invalid JSON from being stored in database
|
|
||||||
if (this.kafkaProducerBrokers) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.kafkaProducerBrokers);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Kafka Producer Brokers must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.kafkaProducerSaslOptions) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.kafkaProducerSaslOptions);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Kafka Producer SASL Options must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rabbitmqNodes) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.rabbitmqNodes);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`RabbitMQ Nodes must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.conditions) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.conditions);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Conditions must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.headers) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.headers);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Headers must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.accepted_statuscodes_json) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.accepted_statuscodes_json);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Accepted status codes must be valid JSON: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.type === "ping") {
|
if (this.type === "ping") {
|
||||||
// ping parameters validation
|
// ping parameters validation
|
||||||
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,8 @@ const analytics = require("../analytics/analytics");
|
|||||||
const { marked } = require("marked");
|
const { marked } = require("marked");
|
||||||
const { Feed } = require("feed");
|
const { Feed } = require("feed");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
|
|
||||||
const { setting } = require("../util-server");
|
const { setting } = require("../util-server");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
STATUS_PAGE_ALL_DOWN,
|
STATUS_PAGE_ALL_DOWN,
|
||||||
STATUS_PAGE_ALL_UP,
|
STATUS_PAGE_ALL_UP,
|
||||||
@ -17,7 +17,6 @@ const {
|
|||||||
UP,
|
UP,
|
||||||
MAINTENANCE,
|
MAINTENANCE,
|
||||||
DOWN,
|
DOWN,
|
||||||
INCIDENT_PAGE_SIZE,
|
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
@ -308,13 +307,12 @@ class StatusPage extends BeanModel {
|
|||||||
static async getStatusPageData(statusPage) {
|
static async getStatusPageData(statusPage) {
|
||||||
const config = await statusPage.toPublicJSON();
|
const config = await statusPage.toPublicJSON();
|
||||||
|
|
||||||
// All active incidents
|
// Incident
|
||||||
let incidents = await R.find(
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [statusPage.id]);
|
||||||
"incident",
|
|
||||||
" pin = 1 AND active = 1 AND status_page_id = ? ORDER BY created_date DESC",
|
if (incident) {
|
||||||
[statusPage.id]
|
incident = incident.toPublicJSON();
|
||||||
);
|
}
|
||||||
incidents = incidents.map((i) => i.toPublicJSON());
|
|
||||||
|
|
||||||
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
|
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
|
||||||
|
|
||||||
@ -332,7 +330,7 @@ class StatusPage extends BeanModel {
|
|||||||
// Response
|
// Response
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
incidents,
|
incident,
|
||||||
publicGroupList,
|
publicGroupList,
|
||||||
maintenanceList,
|
maintenanceList,
|
||||||
};
|
};
|
||||||
@ -501,54 +499,6 @@ class StatusPage extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get paginated incident history for a status page using cursor-based pagination
|
|
||||||
* @param {number} statusPageId ID of the status page
|
|
||||||
* @param {string|null} cursor ISO date string cursor (created_date of last item from previous page)
|
|
||||||
* @param {boolean} isPublic Whether to return public or admin data
|
|
||||||
* @returns {Promise<object>} Paginated incident data with cursor
|
|
||||||
*/
|
|
||||||
static async getIncidentHistory(statusPageId, cursor = null, isPublic = true) {
|
|
||||||
let incidents;
|
|
||||||
|
|
||||||
if (cursor) {
|
|
||||||
incidents = await R.find(
|
|
||||||
"incident",
|
|
||||||
" status_page_id = ? AND created_date < ? ORDER BY created_date DESC LIMIT ? ",
|
|
||||||
[statusPageId, cursor, INCIDENT_PAGE_SIZE]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
incidents = await R.find("incident", " status_page_id = ? ORDER BY created_date DESC LIMIT ? ", [
|
|
||||||
statusPageId,
|
|
||||||
INCIDENT_PAGE_SIZE,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = await R.count("incident", " status_page_id = ? ", [statusPageId]);
|
|
||||||
|
|
||||||
const lastIncident = incidents[incidents.length - 1];
|
|
||||||
let nextCursor = null;
|
|
||||||
let hasMore = false;
|
|
||||||
|
|
||||||
if (lastIncident) {
|
|
||||||
const moreCount = await R.count("incident", " status_page_id = ? AND created_date < ? ", [
|
|
||||||
statusPageId,
|
|
||||||
lastIncident.created_date,
|
|
||||||
]);
|
|
||||||
hasMore = moreCount > 0;
|
|
||||||
if (hasMore) {
|
|
||||||
nextCursor = lastIncident.created_date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
incidents: incidents.map((i) => i.toPublicJSON()),
|
|
||||||
total,
|
|
||||||
nextCursor,
|
|
||||||
hasMore,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of maintenances
|
* Get list of maintenances
|
||||||
* @param {number} statusPageId ID of status page to get maintenance for
|
* @param {number} statusPageId ID of status page to get maintenance for
|
||||||
|
|||||||
@ -57,19 +57,6 @@ class Ntfy extends NotificationProvider {
|
|||||||
status = "Up";
|
status = "Up";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include monitor's assigned tags
|
|
||||||
if (monitorJSON && monitorJSON.tags && Array.isArray(monitorJSON.tags)) {
|
|
||||||
const monitorTagNames = monitorJSON.tags.map((tag) => {
|
|
||||||
// Include value if it exists
|
|
||||||
if (tag.value) {
|
|
||||||
return `${tag.name}: ${tag.value}`;
|
|
||||||
}
|
|
||||||
return tag.name;
|
|
||||||
});
|
|
||||||
tags = tags.concat(monitorTagNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
topic: notification.ntfytopic,
|
topic: notification.ntfytopic,
|
||||||
message: heartbeatJSON.msg,
|
message: heartbeatJSON.msg,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const { log } = require("../../src/util");
|
|
||||||
|
|
||||||
class SMTP extends NotificationProvider {
|
class SMTP extends NotificationProvider {
|
||||||
name = "smtp";
|
name = "smtp";
|
||||||
@ -15,24 +14,10 @@ class SMTP extends NotificationProvider {
|
|||||||
host: notification.smtpHost,
|
host: notification.smtpHost,
|
||||||
port: notification.smtpPort,
|
port: notification.smtpPort,
|
||||||
secure: notification.smtpSecure,
|
secure: notification.smtpSecure,
|
||||||
};
|
tls: {
|
||||||
|
|
||||||
// Handle TLS/STARTTLS options
|
|
||||||
if (!notification.smtpSecure && notification.smtpIgnoreSTARTTLS) {
|
|
||||||
// Disable STARTTLS completely for servers that don't support it
|
|
||||||
// Connection will remain unencrypted
|
|
||||||
log.warn(
|
|
||||||
"notification",
|
|
||||||
`SMTP notification using unencrypted connection (STARTTLS disabled) to ${notification.smtpHost}:${notification.smtpPort}`
|
|
||||||
);
|
|
||||||
config.ignoreTLS = true;
|
|
||||||
} else {
|
|
||||||
// SMTPS (implicit TLS on port 465)
|
|
||||||
// or STARTTLS (default behavior for ports 25, 587)
|
|
||||||
config.tls = {
|
|
||||||
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Fix #1129
|
// Fix #1129
|
||||||
if (notification.smtpDkimDomain) {
|
if (notification.smtpDkimDomain) {
|
||||||
|
|||||||
96
server/password-util.js
Normal file
96
server/password-util.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Password validation utility following NIST SP 800-63B guidelines
|
||||||
|
* @module password-util
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum password length as per NIST recommendations
|
||||||
|
*/
|
||||||
|
const MIN_PASSWORD_LENGTH = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if password appears in Have I Been Pwned database using k-anonymity
|
||||||
|
* @param {string} password - The password to check
|
||||||
|
* @returns {Promise<{ breached: boolean, count: number }>} Whether password is breached and count
|
||||||
|
*/
|
||||||
|
async function checkPasswordBreached(password) {
|
||||||
|
try {
|
||||||
|
// Generate SHA-1 hash of password
|
||||||
|
const hash = crypto.createHash("sha1").update(password).digest("hex").toUpperCase();
|
||||||
|
const prefix = hash.substring(0, 5);
|
||||||
|
const suffix = hash.substring(5);
|
||||||
|
|
||||||
|
// Query HIBP API with first 5 characters (k-anonymity)
|
||||||
|
const response = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`, {
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if our hash suffix appears in the response
|
||||||
|
const hashes = response.data.split("\r\n");
|
||||||
|
for (const line of hashes) {
|
||||||
|
const [hashSuffix, count] = line.split(":");
|
||||||
|
if (hashSuffix === suffix) {
|
||||||
|
return { breached: true, count: parseInt(count, 10) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { breached: false, count: 0 };
|
||||||
|
} catch (error) {
|
||||||
|
// If HIBP is unavailable, don't block the password
|
||||||
|
console.warn("Failed to check password against HIBP:", error.message);
|
||||||
|
return { breached: false, count: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a password according to NIST SP 800-63B guidelines.
|
||||||
|
*
|
||||||
|
* NIST guidelines state:
|
||||||
|
* - Passwords should have a minimum length (8-12 characters recommended)
|
||||||
|
* - Composition rules (requiring specific character types) SHALL NOT be imposed
|
||||||
|
* - All printable ASCII characters and Unicode characters should be allowed
|
||||||
|
*
|
||||||
|
* This implementation enforces only minimum length, allowing all character compositions.
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
* @param {boolean} checkBreached - Whether to check against breach database (optional, default: false)
|
||||||
|
* @returns {Promise<{ ok: boolean, msg?: string, warning?: string }>} Validation result
|
||||||
|
*/
|
||||||
|
async function validatePassword(password, checkBreached = false) {
|
||||||
|
if (!password) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: "Password cannot be empty"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Check against breach database (non-blocking warning)
|
||||||
|
if (checkBreached) {
|
||||||
|
const breachResult = await checkPasswordBreached(password);
|
||||||
|
if (breachResult.breached) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
warning: {msg: "passwordFoundInDataBreach", meta: breachResult.count}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validatePassword,
|
||||||
|
checkPasswordBreached,
|
||||||
|
MIN_PASSWORD_LENGTH
|
||||||
|
};
|
||||||
@ -142,30 +142,6 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/api/status-page/:slug/incident-history", cache("5 minutes"), async (request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let slug = request.params.slug;
|
|
||||||
slug = slug.toLowerCase();
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
|
|
||||||
if (!statusPageID) {
|
|
||||||
sendHttpError(response, "Status Page Not Found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cursor = request.query.cursor || null;
|
|
||||||
const result = await StatusPage.getIncidentHistory(statusPageID, cursor, true);
|
|
||||||
response.json({
|
|
||||||
ok: true,
|
|
||||||
...result,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
sendHttpError(response, error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// overall status-page status badge
|
// overall status-page status badge
|
||||||
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
|
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
|||||||
@ -83,7 +83,7 @@ log.debug("server", "Importing http-graceful-shutdown");
|
|||||||
const gracefulShutdown = require("http-graceful-shutdown");
|
const gracefulShutdown = require("http-graceful-shutdown");
|
||||||
log.debug("server", "Importing prometheus-api-metrics");
|
log.debug("server", "Importing prometheus-api-metrics");
|
||||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||||
const { passwordStrength } = require("check-password-strength");
|
const { validatePassword } = require("./password-util");
|
||||||
const TranslatableError = require("./translatable-error");
|
const TranslatableError = require("./translatable-error");
|
||||||
|
|
||||||
log.debug("server", "Importing 2FA Modules");
|
log.debug("server", "Importing 2FA Modules");
|
||||||
@ -683,8 +683,9 @@ let needSetup = false;
|
|||||||
|
|
||||||
socket.on("setup", async (username, password, callback) => {
|
socket.on("setup", async (username, password, callback) => {
|
||||||
try {
|
try {
|
||||||
if (passwordStrength(password).value === "Too weak") {
|
const passwordValidation = await validatePassword(password, true);
|
||||||
throw new TranslatableError("passwordTooWeak");
|
if (!passwordValidation.ok) {
|
||||||
|
throw new TranslatableError("passwordTooShort");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((await R.knex("user").count("id as count").first()).count !== 0) {
|
if ((await R.knex("user").count("id as count").first()).count !== 0) {
|
||||||
@ -704,6 +705,7 @@ let needSetup = false;
|
|||||||
ok: true,
|
ok: true,
|
||||||
msg: "successAdded",
|
msg: "successAdded",
|
||||||
msgi18n: true,
|
msgi18n: true,
|
||||||
|
warning: passwordValidation.warning,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
@ -1416,8 +1418,9 @@ let needSetup = false;
|
|||||||
throw new Error("Invalid new password");
|
throw new Error("Invalid new password");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordStrength(password.newPassword).value === "Too weak") {
|
const passwordValidation = await validatePassword(password.newPassword, true);
|
||||||
throw new TranslatableError("passwordTooWeak");
|
if (!passwordValidation.ok) {
|
||||||
|
throw new TranslatableError("passwordTooShort");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||||
@ -1430,6 +1433,7 @@ let needSetup = false;
|
|||||||
token: User.createJWT(user, server.jwtSecret),
|
token: User.createJWT(user, server.jwtSecret),
|
||||||
msg: "successAuthChangePassword",
|
msg: "successAuthChangePassword",
|
||||||
msgi18n: true,
|
msgi18n: true,
|
||||||
|
warning: passwordValidation.warning,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
|
|||||||
@ -8,21 +8,6 @@ const apicache = require("../modules/apicache");
|
|||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates incident data
|
|
||||||
* @param {object} incident - The incident object
|
|
||||||
* @returns {void}
|
|
||||||
* @throws {Error} If validation fails
|
|
||||||
*/
|
|
||||||
function validateIncident(incident) {
|
|
||||||
if (!incident.title || incident.title.trim() === "") {
|
|
||||||
throw new Error("Please input title");
|
|
||||||
}
|
|
||||||
if (!incident.content || incident.content.trim() === "") {
|
|
||||||
throw new Error("Please input content");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket handlers for status page
|
* Socket handlers for status page
|
||||||
* @param {Socket} socket Socket.io instance to add listeners on
|
* @param {Socket} socket Socket.io instance to add listeners on
|
||||||
@ -40,6 +25,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
throw new Error("slug is not found");
|
throw new Error("slug is not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [statusPageID]);
|
||||||
|
|
||||||
let incidentBean;
|
let incidentBean;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
@ -57,13 +44,12 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
incidentBean.content = incident.content;
|
incidentBean.content = incident.content;
|
||||||
incidentBean.style = incident.style;
|
incidentBean.style = incident.style;
|
||||||
incidentBean.pin = true;
|
incidentBean.pin = true;
|
||||||
incidentBean.active = true;
|
|
||||||
incidentBean.status_page_id = statusPageID;
|
incidentBean.status_page_id = statusPageID;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean.last_updated_date = R.isoDateTime(dayjs.utc());
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
} else {
|
} else {
|
||||||
incidentBean.created_date = R.isoDateTime(dayjs.utc());
|
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
|
||||||
}
|
}
|
||||||
|
|
||||||
await R.store(incidentBean);
|
await R.store(incidentBean);
|
||||||
@ -99,171 +85,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getIncidentHistory", async (slug, cursor, callback) => {
|
|
||||||
try {
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
if (!statusPageID) {
|
|
||||||
throw new Error("slug is not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPublic = !socket.userID;
|
|
||||||
const result = await StatusPage.getIncidentHistory(statusPageID, cursor, isPublic);
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
...result,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("editIncident", async (slug, incidentID, incident, callback) => {
|
|
||||||
try {
|
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
if (!statusPageID) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "slug is not found",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [incidentID, statusPageID]);
|
|
||||||
if (!bean) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "Incident not found or access denied",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
validateIncident(incident);
|
|
||||||
} catch (e) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: e.message,
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validStyles = ["info", "warning", "danger", "primary", "light", "dark"];
|
|
||||||
if (!validStyles.includes(incident.style)) {
|
|
||||||
incident.style = "warning";
|
|
||||||
}
|
|
||||||
|
|
||||||
bean.title = incident.title;
|
|
||||||
bean.content = incident.content;
|
|
||||||
bean.style = incident.style;
|
|
||||||
bean.pin = incident.pin !== false;
|
|
||||||
bean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
|
||||||
|
|
||||||
await R.store(bean);
|
|
||||||
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "Saved.",
|
|
||||||
msgi18n: true,
|
|
||||||
incident: bean.toPublicJSON(),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: error.message,
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("deleteIncident", async (slug, incidentID, callback) => {
|
|
||||||
try {
|
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
if (!statusPageID) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "slug is not found",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [incidentID, statusPageID]);
|
|
||||||
if (!bean) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "Incident not found or access denied",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await R.trash(bean);
|
|
||||||
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "successDeleted",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: error.message,
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("resolveIncident", async (slug, incidentID, callback) => {
|
|
||||||
try {
|
|
||||||
checkLogin(socket);
|
|
||||||
|
|
||||||
let statusPageID = await StatusPage.slugToID(slug);
|
|
||||||
if (!statusPageID) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "slug is not found",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [incidentID, statusPageID]);
|
|
||||||
if (!bean) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: "Incident not found or access denied",
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await bean.resolve();
|
|
||||||
|
|
||||||
callback({
|
|
||||||
ok: true,
|
|
||||||
msg: "Resolved",
|
|
||||||
msgi18n: true,
|
|
||||||
incident: bean.toPublicJSON(),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
callback({
|
|
||||||
ok: false,
|
|
||||||
msg: error.message,
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("getStatusPage", async (slug, callback) => {
|
socket.on("getStatusPage", async (slug, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|||||||
@ -1,119 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="shadow-box alert mb-4 p-4 incident"
|
|
||||||
role="alert"
|
|
||||||
:class="'bg-' + modelValue.style"
|
|
||||||
data-testid="incident-edit"
|
|
||||||
>
|
|
||||||
<strong>{{ $t("Title") }}:</strong>
|
|
||||||
<Editable
|
|
||||||
:model-value="modelValue.title"
|
|
||||||
tag="h4"
|
|
||||||
:contenteditable="true"
|
|
||||||
:noNL="true"
|
|
||||||
class="alert-heading"
|
|
||||||
data-testid="incident-title"
|
|
||||||
@update:model-value="updateField('title', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<strong>{{ $t("Content") }}:</strong>
|
|
||||||
<Editable
|
|
||||||
:model-value="modelValue.content"
|
|
||||||
tag="div"
|
|
||||||
:contenteditable="true"
|
|
||||||
class="content"
|
|
||||||
data-testid="incident-content-editable"
|
|
||||||
@update:model-value="updateField('content', $event)"
|
|
||||||
/>
|
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("markdownSupported") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<button class="btn btn-light me-2" data-testid="post-incident-button" @click="$emit('post')">
|
|
||||||
<font-awesome-icon icon="bullhorn" />
|
|
||||||
{{ $t("Post") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-light me-2" @click="$emit('cancel')">
|
|
||||||
<font-awesome-icon icon="times" />
|
|
||||||
{{ $t("Cancel") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="dropdown d-inline-block me-2">
|
|
||||||
<button
|
|
||||||
id="dropdownMenuButton1"
|
|
||||||
class="btn btn-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
{{ $t("Style") }}: {{ $t(modelValue.style) }}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'info')">
|
|
||||||
{{ $t("info") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'warning')">
|
|
||||||
{{ $t("warning") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'danger')">
|
|
||||||
{{ $t("danger") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'primary')">
|
|
||||||
{{ $t("primary") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'light')">
|
|
||||||
{{ $t("light") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="updateField('style', 'dark')">
|
|
||||||
{{ $t("dark") }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "IncidentEditForm",
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "post", "cancel"],
|
|
||||||
methods: {
|
|
||||||
updateField(field, value) {
|
|
||||||
this.$emit("update:modelValue", {
|
|
||||||
...this.modelValue,
|
|
||||||
[field]: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.incident {
|
|
||||||
.content {
|
|
||||||
&[contenteditable="true"] {
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="incident-group" data-testid="incident-group">
|
|
||||||
<div v-if="loading && incidents.length === 0" class="text-center py-4">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">{{ $t("Loading...") }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="incidents.length === 0" class="text-center py-4 text-muted">
|
|
||||||
{{ $t("No incidents recorded") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="incident-list">
|
|
||||||
<div
|
|
||||||
v-for="incident in incidents"
|
|
||||||
:key="incident.id"
|
|
||||||
class="incident-item"
|
|
||||||
:class="{ resolved: !incident.active }"
|
|
||||||
>
|
|
||||||
<div class="incident-style-indicator" :class="`bg-${incident.style}`"></div>
|
|
||||||
<div class="incident-body">
|
|
||||||
<div class="incident-header d-flex justify-content-between align-items-start">
|
|
||||||
<h5 class="incident-title mb-0">{{ incident.title }}</h5>
|
|
||||||
<div v-if="editMode" class="incident-actions">
|
|
||||||
<button
|
|
||||||
v-if="incident.active"
|
|
||||||
class="btn btn-success btn-sm me-1"
|
|
||||||
:title="$t('Resolve')"
|
|
||||||
@click="$emit('resolve-incident', incident)"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="check" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-secondary btn-sm me-1"
|
|
||||||
:title="$t('Edit')"
|
|
||||||
@click="$emit('edit-incident', incident)"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="edit" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-danger btn-sm"
|
|
||||||
:title="$t('Delete')"
|
|
||||||
@click="$emit('delete-incident', incident)"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="trash" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
|
||||||
<div class="incident-content mt-1" v-html="getIncidentHTML(incident.content)"></div>
|
|
||||||
<div class="incident-meta text-muted small mt-2">
|
|
||||||
<div>{{ $t("createdAt", { date: datetime(incident.createdDate) }) }}</div>
|
|
||||||
<div v-if="incident.lastUpdatedDate">
|
|
||||||
{{ $t("lastUpdatedAt", { date: datetime(incident.lastUpdatedDate) }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { marked } from "marked";
|
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
import datetimeMixin from "../mixins/datetime";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "IncidentHistory",
|
|
||||||
mixins: [datetimeMixin],
|
|
||||||
props: {
|
|
||||||
incidents: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
editMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["edit-incident", "delete-incident", "resolve-incident"],
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* Get sanitized HTML for incident content
|
|
||||||
* @param {string} content - Markdown content
|
|
||||||
* @returns {string} Sanitized HTML
|
|
||||||
*/
|
|
||||||
getIncidentHTML(content) {
|
|
||||||
if (content != null) {
|
|
||||||
return DOMPurify.sanitize(marked(content));
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/vars.scss";
|
|
||||||
|
|
||||||
.incident-group {
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.incident-list {
|
|
||||||
.incident-item {
|
|
||||||
display: flex;
|
|
||||||
padding: 13px 15px 10px 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all ease-in-out 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $highlight-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.resolved {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-style-indicator {
|
|
||||||
width: 6px;
|
|
||||||
min-height: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-meta {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
.incident-group {
|
|
||||||
.incident-list {
|
|
||||||
.incident-item {
|
|
||||||
&:hover {
|
|
||||||
background-color: $dark-bg2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="modal" class="modal fade" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
{{ $t("Edit Incident") }}
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="incident-title" class="form-label">{{ $t("Title") }}</label>
|
|
||||||
<input
|
|
||||||
id="incident-title"
|
|
||||||
v-model="form.title"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
:placeholder="$t('Incident title')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="incident-content" class="form-label">{{ $t("Content") }}</label>
|
|
||||||
<textarea
|
|
||||||
id="incident-content"
|
|
||||||
v-model="form.content"
|
|
||||||
class="form-control"
|
|
||||||
rows="4"
|
|
||||||
:placeholder="$t('Incident description')"
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="incident-style" class="form-label">{{ $t("Style") }}</label>
|
|
||||||
<select id="incident-style" v-model="form.style" class="form-select">
|
|
||||||
<option value="info">{{ $t("info") }}</option>
|
|
||||||
<option value="warning">
|
|
||||||
{{ $t("warning") }}
|
|
||||||
</option>
|
|
||||||
<option value="danger">
|
|
||||||
{{ $t("danger") }}
|
|
||||||
</option>
|
|
||||||
<option value="primary">
|
|
||||||
{{ $t("primary") }}
|
|
||||||
</option>
|
|
||||||
<option value="light">{{ $t("light") }}</option>
|
|
||||||
<option value="dark">{{ $t("dark") }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input id="incident-pin" v-model="form.pin" type="checkbox" class="form-check-input" />
|
|
||||||
<label for="incident-pin" class="form-check-label">
|
|
||||||
{{ $t("Pin this incident") }}
|
|
||||||
</label>
|
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("Pinned incidents are shown prominently on the status page") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
{{ $t("Cancel") }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-primary" :disabled="processing" @click="submit">
|
|
||||||
<span v-if="processing" class="spinner-border spinner-border-sm me-1" role="status"></span>
|
|
||||||
{{ $t("Save") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Confirm
|
|
||||||
ref="confirmDelete"
|
|
||||||
btn-style="btn-danger"
|
|
||||||
:yes-text="$t('Yes')"
|
|
||||||
:no-text="$t('No')"
|
|
||||||
@yes="confirmDeleteIncident"
|
|
||||||
>
|
|
||||||
{{ $t("deleteIncidentMsg") }}
|
|
||||||
</Confirm>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
import Confirm from "./Confirm.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "IncidentManageModal",
|
|
||||||
components: {
|
|
||||||
Confirm,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["incident-updated"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
modal: null,
|
|
||||||
processing: false,
|
|
||||||
incidentId: null,
|
|
||||||
pendingDeleteIncident: null,
|
|
||||||
form: {
|
|
||||||
title: "",
|
|
||||||
content: "",
|
|
||||||
style: "warning",
|
|
||||||
pin: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.modal = new Modal(this.$refs.modal);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* Show the modal for editing an existing incident
|
|
||||||
* @param {object} incident - The incident to edit
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
showEdit(incident) {
|
|
||||||
this.incidentId = incident.id;
|
|
||||||
this.form = {
|
|
||||||
title: incident.title,
|
|
||||||
content: incident.content,
|
|
||||||
style: incident.style || "warning",
|
|
||||||
pin: !!incident.pin,
|
|
||||||
};
|
|
||||||
this.modal.show();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show delete confirmation dialog
|
|
||||||
* @param {object} incident - The incident to delete
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
showDelete(incident) {
|
|
||||||
this.pendingDeleteIncident = incident;
|
|
||||||
this.$refs.confirmDelete.show();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit the form to edit the incident
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
submit() {
|
|
||||||
if (!this.form.title || this.form.title.trim() === "") {
|
|
||||||
this.$root.toastError(this.$t("Please input title"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.form.content || this.form.content.trim() === "") {
|
|
||||||
this.$root.toastError(this.$t("Please input content"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processing = true;
|
|
||||||
|
|
||||||
this.$root.getSocket().emit("editIncident", this.slug, this.incidentId, this.form, (res) => {
|
|
||||||
this.processing = false;
|
|
||||||
this.$root.toastRes(res);
|
|
||||||
if (res.ok) {
|
|
||||||
this.modal.hide();
|
|
||||||
this.$emit("incident-updated");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm and delete the incident
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
confirmDeleteIncident() {
|
|
||||||
if (!this.pendingDeleteIncident) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$root.getSocket().emit("deleteIncident", this.slug, this.pendingDeleteIncident.id, (res) => {
|
|
||||||
this.$root.toastRes(res);
|
|
||||||
if (res.ok) {
|
|
||||||
this.$emit("incident-updated");
|
|
||||||
}
|
|
||||||
this.pendingDeleteIncident = null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.modal-body {
|
|
||||||
.form-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
113
src/components/PasswordStrengthMeter.vue
Normal file
113
src/components/PasswordStrengthMeter.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="password && strength !== null" class="password-strength mt-2">
|
||||||
|
<div
|
||||||
|
class="strength-meter mx-auto"
|
||||||
|
role="progressbar"
|
||||||
|
:aria-valuenow="strength"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="4"
|
||||||
|
:aria-label="strengthLabel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="strength-meter-fill"
|
||||||
|
:class="strengthClass"
|
||||||
|
:style="{ width: strengthWidth }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<small v-if="strength < 3" class="text-warning d-block mt-1">
|
||||||
|
{{ $t("passwordWeakWarning") }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import zxcvbn from "zxcvbn";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
strength() {
|
||||||
|
if (!this.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInputs = this.username ? [ this.username ] : [];
|
||||||
|
const result = zxcvbn(this.password, userInputs);
|
||||||
|
return result.score;
|
||||||
|
},
|
||||||
|
strengthClass() {
|
||||||
|
if (this.strength === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const classes = [ "strength-very-weak", "strength-weak", "strength-fair", "strength-good", "strength-strong" ];
|
||||||
|
return classes[this.strength] || "";
|
||||||
|
},
|
||||||
|
strengthWidth() {
|
||||||
|
if (this.strength === null) {
|
||||||
|
return "0%";
|
||||||
|
}
|
||||||
|
return `${(this.strength + 1) * 20}%`;
|
||||||
|
},
|
||||||
|
strengthLabel() {
|
||||||
|
if (this.strength === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const labels = [ "Very weak", "Weak", "Fair", "Good", "Strong" ];
|
||||||
|
return `Password strength: ${labels[this.strength] || "unknown"}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.password-strength {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-meter {
|
||||||
|
height: 8px;
|
||||||
|
width: 85%;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-meter-fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color transitions using SCSS variables
|
||||||
|
.strength-very-weak {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-weak {
|
||||||
|
background-color: mix($danger, $warning, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-fair {
|
||||||
|
background-color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-good {
|
||||||
|
background-color: mix($warning, $primary, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-strong {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -56,24 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!$parent.notification.smtpSecure" class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
id="ignore-starttls"
|
|
||||||
v-model="$parent.notification.smtpIgnoreSTARTTLS"
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="ignore-starttls">
|
|
||||||
{{ $t("Disable STARTTLS") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("disableSTARTTLSDescription") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -42,6 +42,12 @@
|
|||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Password strength indicator -->
|
||||||
|
<PasswordStrengthMeter
|
||||||
|
:password="password.newPassword"
|
||||||
|
:username="$root.username"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -146,11 +152,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import Confirm from "../../components/Confirm.vue";
|
import Confirm from "../../components/Confirm.vue";
|
||||||
import TwoFADialog from "../../components/TwoFADialog.vue";
|
import TwoFADialog from "../../components/TwoFADialog.vue";
|
||||||
|
import PasswordStrengthMeter from "../../components/PasswordStrengthMeter.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Confirm,
|
Confirm,
|
||||||
TwoFADialog,
|
TwoFADialog,
|
||||||
|
PasswordStrengthMeter,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -193,6 +201,12 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
|
// Show warning toast if password was found in breach database
|
||||||
|
if (res.ok && res.warning) {
|
||||||
|
this.$root.toastWarning(res.warning);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.password.currentPassword = "";
|
this.password.currentPassword = "";
|
||||||
this.password.newPassword = "";
|
this.password.newPassword = "";
|
||||||
|
|||||||
@ -46,9 +46,6 @@
|
|||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"DateTime": "DateTime",
|
"DateTime": "DateTime",
|
||||||
"Message": "Message",
|
"Message": "Message",
|
||||||
"No incidents recorded": "No incidents recorded",
|
|
||||||
"Load More": "Load More",
|
|
||||||
"Loading...": "Loading...",
|
|
||||||
"No important events": "No important events",
|
"No important events": "No important events",
|
||||||
"Resume": "Resume",
|
"Resume": "Resume",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
@ -65,7 +62,6 @@
|
|||||||
"minuteShort": "{n} min | {n} min",
|
"minuteShort": "{n} min | {n} min",
|
||||||
"years": "{n} year | {n} years",
|
"years": "{n} year | {n} years",
|
||||||
"Response": "Response",
|
"Response": "Response",
|
||||||
"Pin this incident": "Pin this incident",
|
|
||||||
"Ping": "Ping",
|
"Ping": "Ping",
|
||||||
"Monitor Type": "Monitor Type",
|
"Monitor Type": "Monitor Type",
|
||||||
"Keyword": "Keyword",
|
"Keyword": "Keyword",
|
||||||
@ -171,10 +167,6 @@
|
|||||||
"Last Result": "Last Result",
|
"Last Result": "Last Result",
|
||||||
"Create your admin account": "Create your admin account",
|
"Create your admin account": "Create your admin account",
|
||||||
"Repeat Password": "Repeat Password",
|
"Repeat Password": "Repeat Password",
|
||||||
"Incident description": "Incident description",
|
|
||||||
"Incident not found or access denied": "Incident not found or access denied",
|
|
||||||
"Past Incidents": "Past Incidents",
|
|
||||||
"Incident title": "Incident title",
|
|
||||||
"Import Backup": "Import Backup",
|
"Import Backup": "Import Backup",
|
||||||
"Export Backup": "Export Backup",
|
"Export Backup": "Export Backup",
|
||||||
"Export": "Export",
|
"Export": "Export",
|
||||||
@ -231,7 +223,6 @@
|
|||||||
"Blue": "Blue",
|
"Blue": "Blue",
|
||||||
"Indigo": "Indigo",
|
"Indigo": "Indigo",
|
||||||
"Purple": "Purple",
|
"Purple": "Purple",
|
||||||
"Pinned incidents are shown prominently on the status page": "Pinned incidents are shown prominently on the status page",
|
|
||||||
"Pink": "Pink",
|
"Pink": "Pink",
|
||||||
"Custom": "Custom",
|
"Custom": "Custom",
|
||||||
"Search...": "Search…",
|
"Search...": "Search…",
|
||||||
@ -247,7 +238,6 @@
|
|||||||
"Degraded Service": "Degraded Service",
|
"Degraded Service": "Degraded Service",
|
||||||
"Add Group": "Add Group",
|
"Add Group": "Add Group",
|
||||||
"Add a monitor": "Add a monitor",
|
"Add a monitor": "Add a monitor",
|
||||||
"Edit Incident": "Edit Incident",
|
|
||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
@ -306,8 +296,6 @@
|
|||||||
"successKeyword": "Success Keyword",
|
"successKeyword": "Success Keyword",
|
||||||
"successKeywordExplanation": "MQTT Keyword that will be considered as success",
|
"successKeywordExplanation": "MQTT Keyword that will be considered as success",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
"Resolve": "Resolve",
|
|
||||||
"Resolved": "Resolved",
|
|
||||||
"Reset Token": "Reset Token",
|
"Reset Token": "Reset Token",
|
||||||
"Done": "Done",
|
"Done": "Done",
|
||||||
"Info": "Info",
|
"Info": "Info",
|
||||||
@ -361,7 +349,6 @@
|
|||||||
"Customize": "Customize",
|
"Customize": "Customize",
|
||||||
"Custom Footer": "Custom Footer",
|
"Custom Footer": "Custom Footer",
|
||||||
"Custom CSS": "Custom CSS",
|
"Custom CSS": "Custom CSS",
|
||||||
"deleteIncidentMsg": "Are you sure you want to delete this incident?",
|
|
||||||
"deleteStatusPageMsg": "Are you sure want to delete this status page?",
|
"deleteStatusPageMsg": "Are you sure want to delete this status page?",
|
||||||
"Proxies": "Proxies",
|
"Proxies": "Proxies",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
@ -384,7 +371,6 @@
|
|||||||
"Stop": "Stop",
|
"Stop": "Stop",
|
||||||
"Add New Status Page": "Add New Status Page",
|
"Add New Status Page": "Add New Status Page",
|
||||||
"Slug": "Slug",
|
"Slug": "Slug",
|
||||||
"slug is not found": "Slug is not found",
|
|
||||||
"Accept characters:": "Accept characters:",
|
"Accept characters:": "Accept characters:",
|
||||||
"startOrEndWithOnly": "Start or end with {0} only",
|
"startOrEndWithOnly": "Start or end with {0} only",
|
||||||
"No consecutive dashes": "No consecutive dashes",
|
"No consecutive dashes": "No consecutive dashes",
|
||||||
@ -408,8 +394,6 @@
|
|||||||
"Trust Proxy": "Trust Proxy",
|
"Trust Proxy": "Trust Proxy",
|
||||||
"Other Software": "Other Software",
|
"Other Software": "Other Software",
|
||||||
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
|
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
|
||||||
"Please input content": "Please input content",
|
|
||||||
"Please input title": "Please input title",
|
|
||||||
"Please read": "Please read",
|
"Please read": "Please read",
|
||||||
"Subject:": "Subject:",
|
"Subject:": "Subject:",
|
||||||
"Valid To:": "Valid To:",
|
"Valid To:": "Valid To:",
|
||||||
@ -601,8 +585,6 @@
|
|||||||
"secureOptionNone": "None / STARTTLS (25, 587)",
|
"secureOptionNone": "None / STARTTLS (25, 587)",
|
||||||
"secureOptionTLS": "TLS (465)",
|
"secureOptionTLS": "TLS (465)",
|
||||||
"Ignore TLS Error": "Ignore TLS Error",
|
"Ignore TLS Error": "Ignore TLS Error",
|
||||||
"Disable STARTTLS": "Disable STARTTLS",
|
|
||||||
"disableSTARTTLSDescription": "Enable this option for SMTP servers that do not support STARTTLS. This will send emails over an unencrypted connection.",
|
|
||||||
"From Email": "From Email",
|
"From Email": "From Email",
|
||||||
"emailCustomisableContent": "Customisable content",
|
"emailCustomisableContent": "Customisable content",
|
||||||
"smtpLiquidIntroduction": "The following two fields are templatable via the Liquid templating Language. Please refer to the {0} for usage instructions. These are the available variables:",
|
"smtpLiquidIntroduction": "The following two fields are templatable via the Liquid templating Language. Please refer to the {0} for usage instructions. These are the available variables:",
|
||||||
@ -1381,7 +1363,9 @@
|
|||||||
"Show this Maintenance Message on which Status Pages": "Show this Maintenance Message on which Status Pages",
|
"Show this Maintenance Message on which Status Pages": "Show this Maintenance Message on which Status Pages",
|
||||||
"Endpoint": "Endpoint",
|
"Endpoint": "Endpoint",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"passwordTooWeak": "Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.",
|
"passwordTooShort": "Password must be at least 12 characters long.",
|
||||||
|
"passwordWeakWarning": "This password may be easy to guess. Consider using a longer or more unique password.",
|
||||||
|
"passwordFoundInDataBreach": "The chosen password has appeared in a known data breach (via Have I Been Pwned). Please consider changing to a different password. | The chosen password has appeared {n} times in known data breaches (via Have I Been Pwned). Please consider changing to a different password.",
|
||||||
"TLS Alerts": "TLS Alerts",
|
"TLS Alerts": "TLS Alerts",
|
||||||
"Expected TLS Alert": "Expected TLS Alert",
|
"Expected TLS Alert": "Expected TLS Alert",
|
||||||
"None (Successful Connection)": "None (Successful Connection)",
|
"None (Successful Connection)": "None (Successful Connection)",
|
||||||
|
|||||||
@ -395,6 +395,16 @@ export default {
|
|||||||
toast.error(this.$t(msg));
|
toast.error(this.$t(msg));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a warning toast
|
||||||
|
* @param {string} msg Message to show
|
||||||
|
* @param {...any} args Additional parameters for translation interpolation
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
toastWarning(msg, ...args) {
|
||||||
|
toast.warning(this.$t(msg, ...args));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for login
|
* Callback for login
|
||||||
* @callback loginCB
|
* @callback loginCB
|
||||||
|
|||||||
@ -46,6 +46,9 @@
|
|||||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Password strength indicator -->
|
||||||
|
<PasswordStrengthMeter :password="password" :username="username" />
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
<div class="form-floating mt-3">
|
||||||
<input
|
<input
|
||||||
id="repeat"
|
id="repeat"
|
||||||
@ -73,7 +76,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import PasswordStrengthMeter from "../components/PasswordStrengthMeter.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
PasswordStrengthMeter,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
@ -110,6 +118,11 @@ export default {
|
|||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
|
// Show warning toast if password was found in breach database
|
||||||
|
if (res.ok && res.warning) {
|
||||||
|
this.$root.toastWarning(res.warning.msg, res.warning.meta);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
|||||||
@ -297,83 +297,131 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Incident Edit Form -->
|
<!-- Incident -->
|
||||||
<IncidentEditForm
|
<div
|
||||||
v-if="
|
v-if="incident !== null"
|
||||||
editIncidentMode &&
|
class="shadow-box alert mb-4 p-4 incident"
|
||||||
incident !== null &&
|
role="alert"
|
||||||
(!incident.id || !activeIncidents.some((i) => i.id === incident.id))
|
:class="incidentClass"
|
||||||
"
|
data-testid="incident"
|
||||||
v-model="incident"
|
>
|
||||||
@post="postIncident"
|
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||||
@cancel="cancelIncident"
|
<Editable
|
||||||
/>
|
v-model="incident.title"
|
||||||
|
tag="h4"
|
||||||
<!-- Active Pinned Incidents -->
|
:contenteditable="editIncidentMode"
|
||||||
<template v-for="activeIncident in activeIncidents" :key="activeIncident.id">
|
:noNL="true"
|
||||||
<!-- Edit mode for this specific incident -->
|
class="alert-heading"
|
||||||
<IncidentEditForm
|
data-testid="incident-title"
|
||||||
v-if="editIncidentMode && incident !== null && incident.id === activeIncident.id"
|
|
||||||
v-model="incident"
|
|
||||||
@post="postIncident"
|
|
||||||
@cancel="cancelIncident"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Display mode for this incident -->
|
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||||
|
<Editable
|
||||||
|
v-if="editIncidentMode"
|
||||||
|
v-model="incident.content"
|
||||||
|
tag="div"
|
||||||
|
:contenteditable="editIncidentMode"
|
||||||
|
class="content"
|
||||||
|
data-testid="incident-content-editable"
|
||||||
|
/>
|
||||||
|
<div v-if="editIncidentMode" class="form-text">
|
||||||
|
{{ $t("markdownSupported") }}
|
||||||
|
</div>
|
||||||
|
<!-- eslint-disable vue/no-v-html-->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-if="!editIncidentMode"
|
||||||
class="shadow-box alert mb-4 p-4 incident"
|
class="content"
|
||||||
role="alert"
|
data-testid="incident-content"
|
||||||
:class="'bg-' + activeIncident.style"
|
v-html="incidentHTML"
|
||||||
data-testid="incident"
|
></div>
|
||||||
>
|
<!-- eslint-enable vue/no-v-html-->
|
||||||
<h4 class="alert-heading" data-testid="incident-title">{{ activeIncident.title }}</h4>
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div
|
|
||||||
class="content"
|
|
||||||
data-testid="incident-content"
|
|
||||||
v-html="getIncidentHTML(activeIncident.content)"
|
|
||||||
></div>
|
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
|
|
||||||
<!-- Incident Date -->
|
<!-- Incident Date -->
|
||||||
<div class="date mt-3">
|
<div class="date mt-3">
|
||||||
|
{{
|
||||||
|
$t("dateCreatedAtFromNow", {
|
||||||
|
date: $root.datetime(incident.createdDate),
|
||||||
|
fromNow: dateFromNow(incident.createdDate),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<br />
|
||||||
|
<span v-if="incident.lastUpdatedDate">
|
||||||
{{
|
{{
|
||||||
$t("dateCreatedAtFromNow", {
|
$t("lastUpdatedAtFromNow", {
|
||||||
date: $root.datetime(activeIncident.createdDate),
|
date: $root.datetime(incident.lastUpdatedDate),
|
||||||
fromNow: dateFromNow(activeIncident.createdDate),
|
fromNow: dateFromNow(incident.lastUpdatedDate),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
<br />
|
</span>
|
||||||
<span v-if="activeIncident.lastUpdatedDate">
|
</div>
|
||||||
{{
|
|
||||||
$t("lastUpdatedAtFromNow", {
|
<div v-if="editMode" class="mt-3">
|
||||||
date: $root.datetime(activeIncident.lastUpdatedDate),
|
<button
|
||||||
fromNow: dateFromNow(activeIncident.lastUpdatedDate),
|
v-if="editIncidentMode"
|
||||||
})
|
class="btn btn-light me-2"
|
||||||
}}
|
data-testid="post-incident-button"
|
||||||
</span>
|
@click="postIncident"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="bullhorn" />
|
||||||
|
{{ $t("Post") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
|
||||||
|
<font-awesome-icon icon="edit" />
|
||||||
|
{{ $t("Edit") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
{{ $t("Cancel") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
|
||||||
|
<button
|
||||||
|
id="dropdownMenuButton1"
|
||||||
|
class="btn btn-secondary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{{ $t("Style") }}: {{ $t(incident.style) }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'warning'">
|
||||||
|
{{ $t("warning") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'danger'">
|
||||||
|
{{ $t("danger") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'primary'">
|
||||||
|
{{ $t("primary") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'light'">
|
||||||
|
{{ $t("light") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="editMode" class="mt-3">
|
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
|
||||||
<button class="btn btn-light me-2" @click="resolveIncident(activeIncident)">
|
<font-awesome-icon icon="unlink" />
|
||||||
<font-awesome-icon icon="check" />
|
{{ $t("Delete") }}
|
||||||
{{ $t("Resolve") }}
|
</button>
|
||||||
</button>
|
|
||||||
<button class="btn btn-light me-2" @click="editIncident(activeIncident)">
|
|
||||||
<font-awesome-icon icon="edit" />
|
|
||||||
{{ $t("Edit") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-light me-2"
|
|
||||||
@click="$refs.incidentManageModal.showDelete(activeIncident)"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="unlink" />
|
|
||||||
{{ $t("Delete") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<!-- Overall Status -->
|
<!-- Overall Status -->
|
||||||
<div class="shadow-box list p-4 overall-status mb-4">
|
<div class="shadow-box list p-4 overall-status mb-4">
|
||||||
@ -496,56 +544,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Past Incidents -->
|
|
||||||
<div v-if="pastIncidentCount > 0" class="past-incidents-section mb-4">
|
|
||||||
<h2 class="past-incidents-title mb-3">
|
|
||||||
{{ $t("Past Incidents") }}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="past-incidents-content">
|
|
||||||
<div
|
|
||||||
v-for="(dateGroup, dateKey) in groupedIncidentHistory"
|
|
||||||
:key="dateKey"
|
|
||||||
class="incident-date-group mb-4"
|
|
||||||
>
|
|
||||||
<h4 class="incident-date-header">{{ dateKey }}</h4>
|
|
||||||
<div class="shadow-box incident-list-box">
|
|
||||||
<IncidentHistory
|
|
||||||
:incidents="dateGroup"
|
|
||||||
:edit-mode="enableEditMode"
|
|
||||||
:loading="incidentHistoryLoading"
|
|
||||||
@edit-incident="$refs.incidentManageModal.showEdit($event)"
|
|
||||||
@delete-incident="$refs.incidentManageModal.showDelete($event)"
|
|
||||||
@resolve-incident="resolveIncident"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="incidentHistoryHasMore" class="load-more-controls d-flex justify-content-center mt-3">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-secondary btn-sm"
|
|
||||||
:disabled="incidentHistoryLoading"
|
|
||||||
@click="loadMoreIncidentHistory"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="incidentHistoryLoading"
|
|
||||||
class="spinner-border spinner-border-sm me-1"
|
|
||||||
role="status"
|
|
||||||
></span>
|
|
||||||
{{ $t("Load More") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Incident Manage Modal -->
|
|
||||||
<IncidentManageModal
|
|
||||||
v-if="enableEditMode"
|
|
||||||
ref="incidentManageModal"
|
|
||||||
:slug="slug"
|
|
||||||
@incident-updated="loadIncidentHistory"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<footer class="mt-5 mb-4">
|
<footer class="mt-5 mb-4">
|
||||||
<div class="custom-footer-text text-start">
|
<div class="custom-footer-text text-start">
|
||||||
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
|
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
|
||||||
@ -619,9 +617,6 @@ import DOMPurify from "dompurify";
|
|||||||
import Confirm from "../components/Confirm.vue";
|
import Confirm from "../components/Confirm.vue";
|
||||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||||
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||||
import IncidentHistory from "../components/IncidentHistory.vue";
|
|
||||||
import IncidentManageModal from "../components/IncidentManageModal.vue";
|
|
||||||
import IncidentEditForm from "../components/IncidentEditForm.vue";
|
|
||||||
import { getResBaseURL } from "../util-frontend";
|
import { getResBaseURL } from "../util-frontend";
|
||||||
import {
|
import {
|
||||||
STATUS_PAGE_ALL_DOWN,
|
STATUS_PAGE_ALL_DOWN,
|
||||||
@ -655,9 +650,6 @@ export default {
|
|||||||
MaintenanceTime,
|
MaintenanceTime,
|
||||||
Tag,
|
Tag,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
IncidentHistory,
|
|
||||||
IncidentManageModal,
|
|
||||||
IncidentEditForm,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leave Page for vue route change
|
// Leave Page for vue route change
|
||||||
@ -703,10 +695,6 @@ export default {
|
|||||||
updateCountdown: null,
|
updateCountdown: null,
|
||||||
updateCountdownText: null,
|
updateCountdownText: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
incidentHistory: [],
|
|
||||||
incidentHistoryLoading: false,
|
|
||||||
incidentHistoryNextCursor: null,
|
|
||||||
incidentHistoryHasMore: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -832,7 +820,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
incidentHTML() {
|
incidentHTML() {
|
||||||
if (this.incident && this.incident.content != null) {
|
if (this.incident.content != null) {
|
||||||
return DOMPurify.sanitize(marked(this.incident.content));
|
return DOMPurify.sanitize(marked(this.incident.content));
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
@ -858,40 +846,6 @@ export default {
|
|||||||
lastUpdateTimeDisplay() {
|
lastUpdateTimeDisplay() {
|
||||||
return this.$root.datetime(this.lastUpdateTime);
|
return this.$root.datetime(this.lastUpdateTime);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all active pinned incidents for display at the top
|
|
||||||
* @returns {object[]} List of active pinned incidents
|
|
||||||
*/
|
|
||||||
activeIncidents() {
|
|
||||||
return this.incidentHistory.filter((i) => i.active && i.pin);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count of past incidents (non-active or unpinned)
|
|
||||||
* @returns {number} Number of past incidents
|
|
||||||
*/
|
|
||||||
pastIncidentCount() {
|
|
||||||
return this.incidentHistory.filter((i) => !(i.active && i.pin)).length;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group past incidents (non-active or unpinned) by date for display
|
|
||||||
* Active+pinned incidents are shown separately at the top, not in this section
|
|
||||||
* @returns {object} Incidents grouped by date string
|
|
||||||
*/
|
|
||||||
groupedIncidentHistory() {
|
|
||||||
const groups = {};
|
|
||||||
const pastIncidents = this.incidentHistory.filter((i) => !(i.active && i.pin));
|
|
||||||
for (const incident of pastIncidents) {
|
|
||||||
const dateKey = this.formatDateKey(incident.createdDate);
|
|
||||||
if (!groups[dateKey]) {
|
|
||||||
groups[dateKey] = [];
|
|
||||||
}
|
|
||||||
groups[dateKey].push(incident);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
/**
|
/**
|
||||||
@ -994,18 +948,6 @@ export default {
|
|||||||
this.imgDataUrl = this.config.icon;
|
this.imgDataUrl = this.config.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.maintenanceList = res.data.maintenanceList;
|
|
||||||
this.$root.publicGroupList = res.data.publicGroupList;
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
feedInterval = setInterval(
|
|
||||||
() => {
|
|
||||||
this.updateHeartbeatList();
|
|
||||||
},
|
|
||||||
Math.max(5, this.config.autoRefreshInterval) * 1000
|
|
||||||
);
|
|
||||||
|
|
||||||
this.incident = res.data.incident;
|
this.incident = res.data.incident;
|
||||||
this.maintenanceList = res.data.maintenanceList;
|
this.maintenanceList = res.data.maintenanceList;
|
||||||
this.$root.publicGroupList = res.data.publicGroupList;
|
this.$root.publicGroupList = res.data.publicGroupList;
|
||||||
@ -1030,7 +972,6 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.updateHeartbeatList();
|
this.updateHeartbeatList();
|
||||||
this.loadIncidentHistory();
|
|
||||||
|
|
||||||
// Go to edit page if ?edit present
|
// Go to edit page if ?edit present
|
||||||
// null means ?edit present, but no value
|
// null means ?edit present, but no value
|
||||||
@ -1303,8 +1244,7 @@ export default {
|
|||||||
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
|
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.enableEditIncidentMode = false;
|
this.enableEditIncidentMode = false;
|
||||||
this.incident = null;
|
this.incident = res.incident;
|
||||||
this.loadIncidentHistory();
|
|
||||||
} else {
|
} else {
|
||||||
this.$root.toastError(res.msg);
|
this.$root.toastError(res.msg);
|
||||||
}
|
}
|
||||||
@ -1312,14 +1252,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit an incident inline
|
* Click Edit Button
|
||||||
* @param {object} incident - The incident to edit
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
editIncident(incident) {
|
editIncident() {
|
||||||
this.previousIncident = this.incident;
|
|
||||||
this.incident = { ...incident };
|
|
||||||
this.enableEditIncidentMode = true;
|
this.enableEditIncidentMode = true;
|
||||||
|
this.previousIncident = Object.assign({}, this.incident);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1345,18 +1283,6 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get HTML for incident content
|
|
||||||
* @param {string} content - Markdown content
|
|
||||||
* @returns {string} Sanitized HTML
|
|
||||||
*/
|
|
||||||
getIncidentHTML(content) {
|
|
||||||
if (content != null) {
|
|
||||||
return DOMPurify.sanitize(marked(content));
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the relative time difference of a date from now
|
* Get the relative time difference of a date from now
|
||||||
* @param {any} date Date to get time difference
|
* @param {any} date Date to get time difference
|
||||||
@ -1387,105 +1313,6 @@ export default {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Load incident history for the status page
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
loadIncidentHistory() {
|
|
||||||
this.loadIncidentHistoryWithCursor(null);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load incident history using cursor-based pagination
|
|
||||||
* @param {string|null} cursor - Cursor for pagination (created_date of last item)
|
|
||||||
* @param {boolean} append - Whether to append to existing list
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
loadIncidentHistoryWithCursor(cursor, append = false) {
|
|
||||||
this.incidentHistoryLoading = true;
|
|
||||||
|
|
||||||
if (this.enableEditMode) {
|
|
||||||
this.$root.getSocket().emit("getIncidentHistory", this.slug, cursor, (res) => {
|
|
||||||
this.incidentHistoryLoading = false;
|
|
||||||
if (res.ok) {
|
|
||||||
if (append) {
|
|
||||||
this.incidentHistory = [...this.incidentHistory, ...res.incidents];
|
|
||||||
} else {
|
|
||||||
this.incidentHistory = res.incidents;
|
|
||||||
}
|
|
||||||
this.incidentHistoryNextCursor = res.nextCursor;
|
|
||||||
this.incidentHistoryHasMore = res.hasMore;
|
|
||||||
} else {
|
|
||||||
console.error("Failed to load incident history:", res.msg);
|
|
||||||
this.$root.toastError(res.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const url = cursor
|
|
||||||
? `/api/status-page/${this.slug}/incident-history?cursor=${encodeURIComponent(cursor)}`
|
|
||||||
: `/api/status-page/${this.slug}/incident-history`;
|
|
||||||
axios
|
|
||||||
.get(url)
|
|
||||||
.then((res) => {
|
|
||||||
this.incidentHistoryLoading = false;
|
|
||||||
if (res.data.ok) {
|
|
||||||
if (append) {
|
|
||||||
this.incidentHistory = [...this.incidentHistory, ...res.data.incidents];
|
|
||||||
} else {
|
|
||||||
this.incidentHistory = res.data.incidents;
|
|
||||||
}
|
|
||||||
this.incidentHistoryNextCursor = res.data.nextCursor;
|
|
||||||
this.incidentHistoryHasMore = res.data.hasMore;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.incidentHistoryLoading = false;
|
|
||||||
console.error("Failed to load incident history:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load more incident history using cursor-based pagination
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
loadMoreIncidentHistory() {
|
|
||||||
if (this.incidentHistoryHasMore && this.incidentHistoryNextCursor) {
|
|
||||||
this.loadIncidentHistoryWithCursor(this.incidentHistoryNextCursor, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format date key for grouping (e.g., "December 8, 2025")
|
|
||||||
* @param {string} dateStr - ISO date string
|
|
||||||
* @returns {string} Formatted date key
|
|
||||||
*/
|
|
||||||
formatDateKey(dateStr) {
|
|
||||||
if (!dateStr) {
|
|
||||||
return this.$t("Unknown");
|
|
||||||
}
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return date.toLocaleDateString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve an incident
|
|
||||||
* @param {object} incident - The incident to resolve
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
resolveIncident(incident) {
|
|
||||||
this.$root.getSocket().emit("resolveIncident", this.slug, incident.id, (res) => {
|
|
||||||
this.$root.toastRes(res);
|
|
||||||
if (res.ok) {
|
|
||||||
this.loadIncidentHistory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -1604,14 +1431,12 @@ footer {
|
|||||||
|
|
||||||
/* Reset button placed at top-left of the logo */
|
/* Reset button placed at top-left of the logo */
|
||||||
.reset-top-left {
|
.reset-top-left {
|
||||||
transition:
|
position: absolute;
|
||||||
transform $easing-in 0.18s,
|
top: 0;
|
||||||
box-shadow $easing-in 0.18s,
|
left: -15px;
|
||||||
background-color $easing-in 0.18s;
|
z-index: 2;
|
||||||
font-size: 18px;
|
width: 20px;
|
||||||
width: 18px;
|
height: 20px;
|
||||||
height: 18px;
|
|
||||||
padding: 0;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -1620,6 +1445,11 @@ footer {
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition:
|
||||||
|
transform $easing-in 0.18s,
|
||||||
|
box-shadow $easing-in 0.18s,
|
||||||
|
background-color $easing-in 0.18s;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -1756,28 +1586,4 @@ footer {
|
|||||||
.refresh-info {
|
.refresh-info {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.past-incidents-title {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.past-incidents-section {
|
|
||||||
.past-incidents-content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-date-group {
|
|
||||||
.incident-date-header {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--bs-secondary);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list-box {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
var _a;
|
var _a;
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.INCIDENT_PAGE_SIZE = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
||||||
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = void 0;
|
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = void 0;
|
||||||
const dayjs_1 = require("dayjs");
|
const dayjs_1 = require("dayjs");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
@ -31,7 +31,6 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
|||||||
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
||||||
exports.MAX_INTERVAL_SECOND = 2073600;
|
exports.MAX_INTERVAL_SECOND = 2073600;
|
||||||
exports.MIN_INTERVAL_SECOND = 1;
|
exports.MIN_INTERVAL_SECOND = 1;
|
||||||
exports.INCIDENT_PAGE_SIZE = 10;
|
|
||||||
exports.PING_PACKET_SIZE_MIN = 1;
|
exports.PING_PACKET_SIZE_MIN = 1;
|
||||||
exports.PING_PACKET_SIZE_MAX = 65500;
|
exports.PING_PACKET_SIZE_MAX = 65500;
|
||||||
exports.PING_PACKET_SIZE_DEFAULT = 56;
|
exports.PING_PACKET_SIZE_DEFAULT = 56;
|
||||||
|
|||||||
@ -46,8 +46,6 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
|
|||||||
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
|
||||||
export const MIN_INTERVAL_SECOND = 1; // 1 second
|
export const MIN_INTERVAL_SECOND = 1; // 1 second
|
||||||
|
|
||||||
export const INCIDENT_PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
// Packet Size limits
|
// Packet Size limits
|
||||||
export const PING_PACKET_SIZE_MIN = 1;
|
export const PING_PACKET_SIZE_MIN = 1;
|
||||||
export const PING_PACKET_SIZE_MAX = 65500;
|
export const PING_PACKET_SIZE_MAX = 65500;
|
||||||
|
|||||||
132
test/backend-test/test-password-util.js
Normal file
132
test/backend-test/test-password-util.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
const { describe, test } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
|
||||||
|
const { validatePassword, checkPasswordBreached, MIN_PASSWORD_LENGTH } = require("../../server/password-util");
|
||||||
|
|
||||||
|
describe("Password Validation (NIST-aligned)", () => {
|
||||||
|
test("should reject empty password", async () => {
|
||||||
|
const result = await validatePassword("");
|
||||||
|
assert.strictEqual(result.ok, false);
|
||||||
|
assert.match(result.msg, /cannot be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject null password", async () => {
|
||||||
|
const result = await validatePassword(null);
|
||||||
|
assert.strictEqual(result.ok, false);
|
||||||
|
assert.match(result.msg, /cannot be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject undefined password", async () => {
|
||||||
|
const result = await validatePassword(undefined);
|
||||||
|
assert.strictEqual(result.ok, false);
|
||||||
|
assert.match(result.msg, /cannot be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject password shorter than minimum length", async () => {
|
||||||
|
const result = await validatePassword("short");
|
||||||
|
assert.strictEqual(result.ok, false);
|
||||||
|
assert.match(result.msg, /at least \d+ characters/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password exactly at minimum length", async () => {
|
||||||
|
const password = "a".repeat(MIN_PASSWORD_LENGTH);
|
||||||
|
const result = await validatePassword(password);
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept long password with only lowercase letters", async () => {
|
||||||
|
const result = await validatePassword("thisisaverylongpassword");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept passphrase without numbers or special characters", async () => {
|
||||||
|
const result = await validatePassword("CorrectHorseBatteryStaple");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password with mixed case but no numbers", async () => {
|
||||||
|
const result = await validatePassword("MySecretPassword");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password with numbers and special characters", async () => {
|
||||||
|
const result = await validatePassword("MyP@ssw0rd123");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password with only numbers", async () => {
|
||||||
|
const result = await validatePassword("123456789012");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password with only special characters", async () => {
|
||||||
|
const result = await validatePassword("!@#$%^&*(){}");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept password with spaces", async () => {
|
||||||
|
const result = await validatePassword("my secure password here");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept very long password", async () => {
|
||||||
|
const result = await validatePassword("a".repeat(100));
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("minimum password length should be 12 characters", () => {
|
||||||
|
assert.strictEqual(MIN_PASSWORD_LENGTH, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept Unicode characters", async () => {
|
||||||
|
const result = await validatePassword("パスワード123456789");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept emojis in password", async () => {
|
||||||
|
const result = await validatePassword("password🔒🔑123");
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.msg, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should warn about breached password when checking enabled", async () => {
|
||||||
|
// "password" is a well-known breached password
|
||||||
|
const result = await validatePassword("password123456", true);
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.ok(result.warning, "Should have a warning for breached password");
|
||||||
|
assert.strictEqual(result.warning.msg, "passwordFoundInDataBreach");
|
||||||
|
assert.ok(result.warning.meta > 0, "Should have breach count greater than 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should accept non-breached password with no warning", async () => {
|
||||||
|
// Very unlikely to be breached (random strong password)
|
||||||
|
const result = await validatePassword("Xy9#mK2$pQ7!vN8&", true);
|
||||||
|
assert.strictEqual(result.ok, true);
|
||||||
|
assert.strictEqual(result.warning, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HIBP Breach Checking", () => {
|
||||||
|
test("checkPasswordBreached should detect common password", async () => {
|
||||||
|
const result = await checkPasswordBreached("password");
|
||||||
|
assert.strictEqual(result.breached, true);
|
||||||
|
assert.ok(result.count > 1000000, "Password should have been breached many times");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checkPasswordBreached should not detect strong unique password", async () => {
|
||||||
|
// Very unlikely to be in breach database
|
||||||
|
const result = await checkPasswordBreached("Xy9#mK2$pQ7!vN8&zR4@");
|
||||||
|
assert.strictEqual(result.breached, false);
|
||||||
|
assert.strictEqual(result.count, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,150 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
|
||||||
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
|
||||||
|
|
||||||
test.describe("Incident History", () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await restoreSqliteSnapshot(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("past incidents section is hidden when no incidents exist", async ({ page }, testInfo) => {
|
|
||||||
test.setTimeout(60000);
|
|
||||||
|
|
||||||
await page.goto("./add");
|
|
||||||
await login(page);
|
|
||||||
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto("./add-status-page");
|
|
||||||
await page.getByTestId("name-input").fill("Empty Test");
|
|
||||||
await page.getByTestId("slug-input").fill("empty-test");
|
|
||||||
await page.getByTestId("submit-button").click();
|
|
||||||
await page.waitForURL("/status/empty-test?edit");
|
|
||||||
|
|
||||||
await page.getByTestId("save-button").click();
|
|
||||||
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
|
||||||
|
|
||||||
const pastIncidentsSection = page.locator(".past-incidents-section");
|
|
||||||
await expect(pastIncidentsSection).toHaveCount(0);
|
|
||||||
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("active pinned incidents are shown at top and not in past incidents", async ({ page }, testInfo) => {
|
|
||||||
test.setTimeout(60000);
|
|
||||||
|
|
||||||
await page.goto("./add");
|
|
||||||
await login(page);
|
|
||||||
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto("./add-status-page");
|
|
||||||
await page.getByTestId("name-input").fill("Dedup Test");
|
|
||||||
await page.getByTestId("slug-input").fill("dedup-test");
|
|
||||||
await page.getByTestId("submit-button").click();
|
|
||||||
await page.waitForURL("/status/dedup-test?edit");
|
|
||||||
|
|
||||||
await page.getByTestId("create-incident-button").click();
|
|
||||||
await page.getByTestId("incident-title").fill("Active Incident");
|
|
||||||
await page.getByTestId("incident-content-editable").fill("This is an active incident");
|
|
||||||
await page.getByTestId("post-incident-button").click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await page.getByTestId("save-button").click();
|
|
||||||
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
|
||||||
|
|
||||||
const activeIncident = page.getByTestId("incident").filter({ hasText: "Active Incident" });
|
|
||||||
await expect(activeIncident).toBeVisible();
|
|
||||||
|
|
||||||
const pastIncidentsSection = page.locator(".past-incidents-section");
|
|
||||||
await expect(pastIncidentsSection).toHaveCount(0);
|
|
||||||
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resolved incidents appear in past incidents section", async ({ page }, testInfo) => {
|
|
||||||
test.setTimeout(120000);
|
|
||||||
|
|
||||||
await page.goto("./add");
|
|
||||||
await login(page);
|
|
||||||
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto("./add-status-page");
|
|
||||||
await page.getByTestId("name-input").fill("Resolve Test");
|
|
||||||
await page.getByTestId("slug-input").fill("resolve-test");
|
|
||||||
await page.getByTestId("submit-button").click();
|
|
||||||
await page.waitForURL("/status/resolve-test?edit");
|
|
||||||
|
|
||||||
await page.getByTestId("create-incident-button").click();
|
|
||||||
await page.getByTestId("incident-title").fill("Resolved Incident");
|
|
||||||
await page.getByTestId("incident-content-editable").fill("This incident will be resolved");
|
|
||||||
await page.getByTestId("post-incident-button").click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const activeIncidentBanner = page.getByTestId("incident").filter({ hasText: "Resolved Incident" });
|
|
||||||
await expect(activeIncidentBanner).toBeVisible({ timeout: 10000 });
|
|
||||||
|
|
||||||
const resolveButton = activeIncidentBanner.locator("button", { hasText: "Resolve" });
|
|
||||||
await expect(resolveButton).toBeVisible();
|
|
||||||
await resolveButton.click();
|
|
||||||
|
|
||||||
await expect(activeIncidentBanner).toHaveCount(0, { timeout: 10000 });
|
|
||||||
|
|
||||||
await page.getByTestId("save-button").click();
|
|
||||||
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
|
||||||
|
|
||||||
await page.goto("./status/resolve-test");
|
|
||||||
await page.waitForLoadState("networkidle");
|
|
||||||
|
|
||||||
const pastIncidentsSection = page.locator(".past-incidents-section");
|
|
||||||
await expect(pastIncidentsSection).toBeVisible({ timeout: 15000 });
|
|
||||||
|
|
||||||
const resolvedIncidentTitle = pastIncidentsSection.locator(".incident-title");
|
|
||||||
await expect(resolvedIncidentTitle).toContainText("Resolved Incident", { timeout: 15000 });
|
|
||||||
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("incident history pagination loads more incidents", async ({ page }, testInfo) => {
|
|
||||||
test.setTimeout(180000);
|
|
||||||
|
|
||||||
await page.goto("./add");
|
|
||||||
await login(page);
|
|
||||||
await expect(page.getByTestId("monitor-type-select")).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto("./add-status-page");
|
|
||||||
await page.getByTestId("name-input").fill("Pagination Test");
|
|
||||||
await page.getByTestId("slug-input").fill("pagination-test");
|
|
||||||
await page.getByTestId("submit-button").click();
|
|
||||||
await page.waitForURL("/status/pagination-test?edit");
|
|
||||||
|
|
||||||
for (let i = 1; i <= 12; i++) {
|
|
||||||
await page.getByTestId("create-incident-button").click();
|
|
||||||
await page.getByTestId("incident-title").fill("Incident " + i);
|
|
||||||
await page.getByTestId("incident-content-editable").fill("Content for incident " + i);
|
|
||||||
await page.getByTestId("post-incident-button").click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const resolveButton = page.locator("button", { hasText: "Resolve" }).first();
|
|
||||||
if (await resolveButton.isVisible()) {
|
|
||||||
await resolveButton.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByTestId("save-button").click();
|
|
||||||
await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const pastIncidentsSection = page.locator(".past-incidents-section");
|
|
||||||
await expect(pastIncidentsSection).toBeVisible();
|
|
||||||
|
|
||||||
const loadMoreButton = page.locator("button", { hasText: "Load More" });
|
|
||||||
|
|
||||||
if (await loadMoreButton.isVisible()) {
|
|
||||||
await loadMoreButton.click();
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
await screenshot(testInfo, page);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
101
test/e2e/specs/password-strength.spec.js
Normal file
101
test/e2e/specs/password-strength.spec.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { getSqliteDatabaseExists } from "../util-test";
|
||||||
|
|
||||||
|
test.describe("Password Strength Indicator", () => {
|
||||||
|
test.skip(() => !getSqliteDatabaseExists(), "Database must exist before running this test");
|
||||||
|
|
||||||
|
test("should show password strength indicator on setup page", async ({ page }) => {
|
||||||
|
// Navigate to setup page (requires database to exist but not be configured)
|
||||||
|
await page.goto("./setup");
|
||||||
|
|
||||||
|
// Fill username
|
||||||
|
await page.getByPlaceholder("Username").fill("testuser");
|
||||||
|
|
||||||
|
// Initially no strength indicator should be visible
|
||||||
|
const strengthMeter = page.locator(".password-strength");
|
||||||
|
await expect(strengthMeter).not.toBeVisible();
|
||||||
|
|
||||||
|
// Type a weak password
|
||||||
|
const passwordInput = page.getByPlaceholder("Password", { exact: true });
|
||||||
|
await passwordInput.fill("weak");
|
||||||
|
|
||||||
|
// Strength meter should appear
|
||||||
|
await expect(strengthMeter).toBeVisible();
|
||||||
|
|
||||||
|
// Type a stronger password
|
||||||
|
await passwordInput.fill("MyStrongPassword123!");
|
||||||
|
|
||||||
|
// Strength meter should still be visible
|
||||||
|
await expect(strengthMeter).toBeVisible();
|
||||||
|
|
||||||
|
// Check that the strength meter fill has the appropriate class
|
||||||
|
const strengthFill = page.locator(".strength-meter-fill");
|
||||||
|
await expect(strengthFill).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show warning for weak password", async ({ page }) => {
|
||||||
|
await page.goto("./setup");
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
|
||||||
|
// Fill with a password that has low zxcvbn score
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).fill("password1234");
|
||||||
|
|
||||||
|
// Check that warning message appears
|
||||||
|
const warningText = page.locator("text=This password may be easy to guess");
|
||||||
|
await expect(warningText).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not show warning for strong password", async ({ page }) => {
|
||||||
|
await page.goto("./setup");
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
|
||||||
|
// Fill with a strong password
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).fill("Xy9#mK2$pQ7!vN8&zR4@");
|
||||||
|
|
||||||
|
// Check that warning message does not appear
|
||||||
|
const warningText = page.locator("text=This password may be easy to guess");
|
||||||
|
await expect(warningText).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show breach warning toast for compromised password", async ({ page }) => {
|
||||||
|
await page.goto("./setup");
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill("admin");
|
||||||
|
|
||||||
|
// Use a well-known breached password that's long enough (12+ chars)
|
||||||
|
const breachedPassword = "password1234567890";
|
||||||
|
await page.getByPlaceholder("Password", { exact: true }).fill(breachedPassword);
|
||||||
|
await page.getByPlaceholder("Repeat Password").fill(breachedPassword);
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
await page.getByRole("button", { name: "Create" }).click();
|
||||||
|
|
||||||
|
// Wait for and check toast warning appears
|
||||||
|
// Toast messages typically appear in the toast container
|
||||||
|
const toastWarning = page.locator(".toast-warning, .toast.warning, [role='alert']").filter({ hasText: /breach|data breach/i });
|
||||||
|
await expect(toastWarning).toBeVisible({ timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update strength indicator as password changes", async ({ page }) => {
|
||||||
|
await page.goto("./setup");
|
||||||
|
|
||||||
|
await page.getByPlaceholder("Username").fill("testuser");
|
||||||
|
const passwordInput = page.getByPlaceholder("Password", { exact: true });
|
||||||
|
|
||||||
|
// Start with weak password
|
||||||
|
await passwordInput.fill("abc123456789");
|
||||||
|
const strengthFill = page.locator(".strength-meter-fill");
|
||||||
|
|
||||||
|
// Get initial class
|
||||||
|
const initialClass = await strengthFill.getAttribute("class");
|
||||||
|
|
||||||
|
// Change to stronger password
|
||||||
|
await passwordInput.fill("MyVeryStrongP@ssw0rd!");
|
||||||
|
|
||||||
|
// Class should change (indicating different strength)
|
||||||
|
const newClass = await strengthFill.getAttribute("class");
|
||||||
|
expect(initialClass).not.toBe(newClass);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user