web: all the basic commands are working: build, build types, lint source, lint lockfile, lint packagefile, lint types, lint spelling, format source, format package.

This commit is contained in:
Ken Sternberg
2024-08-08 15:08:05 -07:00
parent 7a0b227b46
commit 9084c7c6b4
15 changed files with 190 additions and 145 deletions

7
web/package-lock.json generated
View File

@ -33,6 +33,7 @@
"@patternfly/elements": "^3.0.2", "@patternfly/elements": "^3.0.2",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^8.24.0", "@sentry/browser": "^8.24.0",
"@types/webappsec-credential-management": "^0.6.8",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^4.4.3", "chart.js": "^4.4.3",
@ -8816,6 +8817,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webappsec-credential-management": {
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@types/webappsec-credential-management/-/webappsec-credential-management-0.6.8.tgz",
"integrity": "sha512-DES/SkK54U7AG8hmMkGCJkOSlywM3R+TzaWT+rBnX3lQTJ3K57jWr+UccWY8ImkuKekC9BjB+AH4zLJB4JKpvQ=="
},
"node_modules/@types/which": { "node_modules/@types/which": {
"version": "2.0.2", "version": "2.0.2",
"dev": true, "dev": true,
@ -26150,6 +26156,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/browser": "^8.23.0", "@sentry/browser": "^8.23.0",
"@types/webappsec-credential-management": "^0.6.8",
"base64-js": "^1.5.1" "base64-js": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,16 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint nyc coverage output
coverage
# Import order matters
poly.ts
src/locale-codes.ts
src/locales/
storybook-static/
# Prettier breaks the tsconfig file
tsconfig.json
.storybook/css-import-maps*
package.json
packages/**/package.json

View File

@ -0,0 +1,23 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
}

View File

@ -1,94 +1,93 @@
# @goauthentik/common # @goauthentik/common
The `common` package is a bit of a grab-bag of tools, utilities, and configuration details used The `common` package is a bit of a grab-bag of tools, utilities, and configuration details used
throughout the Authentik front-end suite. Here, we'll try (emphasis on the *try*) to document what throughout the Authentik front-end suite. Here, we'll try (emphasis on the _try_) to document what
each part does. each part does.
- `./api` - `./api`
The `./api` folder contains helpers and plug-ins for communicating with the Authentik API. Its The `./api` folder contains helpers and plug-ins for communicating with the Authentik API. Its
primary purpose is to provide the default configuration details for establishing a channel to the primary purpose is to provide the default configuration details for establishing a channel to the
API, as well as figuring out the default locale, branding information, and even the favicon. (See API, as well as figuring out the default locale, branding information, and even the favicon. (See
what I said about it being a grab-bag?) It has its own list of todos. what I said about it being a grab-bag?) It has its own list of todos.
- `/helpers/plex` - `/helpers/plex`
Contains configuration tools and access for the Plex TV client. Used by all three primary Contains configuration tools and access for the Plex TV client. Used by all three primary
interfaces, but again, not exactly a foundational tool. interfaces, but again, not exactly a foundational tool.
- `/helpers/webauthn` - `/helpers/webauthn`
Used entirely by the WebAuthn tools in the Flow interface. Used entirely by the WebAuthn tools in the Flow interface.
- `/styles`: - `/styles`:
authentik's overrides for patternfly and dark mode. authentik's overrides for patternfly and dark mode.
TODO: Move this into its own package. TODO: Move this into its own package.
- `/ui`: - `/ui`:
Describes the schema of the UIConfig Attributes Object, which dictates certain details about UI Describes the schema of the UIConfig Attributes Object, which dictates certain details about UI
behavior, such as the preliminary state of drawers, editors, and layouts. It also has an API call behavior, such as the preliminary state of drawers, editors, and layouts. It also has an API call
to fetch that UIConfig object from the server. to fetch that UIConfig object from the server.
- `/constants.ts` - `/constants.ts`
Another grab-bag of configuration details: event names, default classnames for setting some visual Another grab-bag of configuration details: event names, default classnames for setting some visual
details, web socket message type tokens, and the localstorage key. details, web socket message type tokens, and the localstorage key.
- `/enums.ts` - `/enums.ts`
Contains one thing: a mapping of generic UI sizing terms to specific classnames in the CSS. Contains one thing: a mapping of generic UI sizing terms to specific classnames in the CSS.
- `./errors.ts` - `./errors.ts`
An error handling toolkit related to the `./api` above. An error handling toolkit related to the `./api` above.
- `./events.ts` - `./events.ts`
An extension of the API's "Event" types to assist in reporting server-side events to the user. Has An extension of the API's "Event" types to assist in reporting server-side events to the user. Has
nothing to do with the browser's internal Event type. Used entirely within `./admin`, may be nothing to do with the browser's internal Event type. Used entirely within `./admin`, may be
suitable to being moved there. suitable to being moved there.
- `./global.ts` - `./global.ts`
A single function that retrieves any global information for the UI from the `index.html` file in A single function that retrieves any global information for the UI from the `index.html` file in
which it was invoked. Used by our Django application to preload configuration information. which it was invoked. Used by our Django application to preload configuration information.
- `./labels.ts`, - `./labels.ts`,
Maps a variety of API tokens to human-readable labels, including those for: Maps a variety of API tokens to human-readable labels, including those for:
- Events
- Severities - Events
- User Types - Severities
- Stage Intent - User Types
- Stage Intent
It might make more sense to move these closer to where they're used, if their use is local to a It might make more sense to move these closer to where they're used, if their use is local to a
single interface or component. single interface or component.
- `./messages.ts` - `./messages.ts`
Contains one thing: a mapping of generic UI alert-level terms to specific classnames in the CSS. Contains one thing: a mapping of generic UI alert-level terms to specific classnames in the CSS.
- `./sentry.ts` - `./sentry.ts`
Sentry is an application monitoring package for finding code breakage. The Sentry configuration for Sentry is an application monitoring package for finding code breakage. The Sentry configuration for
all of our interfaces is kept here. all of our interfaces is kept here.
- `./users.ts` - `./users.ts`
Despite the plural name, this is entirely about getting the current user's configuration from the Despite the plural name, this is entirely about getting the current user's configuration from the
server. Used by all three major interfaces. Could probably be replaced by a context. (Possibly server. Used by all three major interfaces. Could probably be replaced by a context. (Possibly
already has been.) already has been.)
- `./utils.ts` - `./utils.ts`
The classic junk drawer of UI development. A few string functions, a few utilities from The classic junk drawer of UI development. A few string functions, a few utilities from
YouMightNotNeedLodash, a slugifier, some date handling utilities, that sort of thing. YouMightNotNeedLodash, a slugifier, some date handling utilities, that sort of thing.
- `./ws.ts` - `./ws.ts`
Sets up our web socket for receiving server-side events. Used by all three major interfaces. Sets up our web socket for receiving server-side events. Used by all three major interfaces.

View File

@ -12,6 +12,8 @@ export default [
{ {
ignores: [ ignores: [
"dist/", "dist/",
".wireit/",
"packages/common/.wireit/",
// don't ever lint node_modules // don't ever lint node_modules
"node_modules/", "node_modules/",
".storybook/*", ".storybook/*",

View File

@ -1,23 +1,11 @@
{ {
"name": "@goauthentik/common", "name": "@goauthentik/common",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
"./*": "./dist/*"
},
"files": [
"./dist/**/*"
],
"dependencies": { "dependencies": {
"@sentry/browser": "^8.23.0", "@sentry/browser": "^8.23.0",
"@types/webappsec-credential-management": "^0.6.8",
"base64-js": "^1.5.1" "base64-js": "^1.5.1"
}, },
"peerDependencies": {
"@lit/localize": "^0.12.2",
"lit": "^3.2.0"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.8.0", "@eslint/js": "^9.8.0",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
@ -39,6 +27,18 @@
"typesync": "^0.13.0", "typesync": "^0.13.0",
"wireit": "^0.14.4" "wireit": "^0.14.4"
}, },
"exports": {
"./*": "./dist/*"
},
"files": [
"./dist/**/*"
],
"license": "MIT",
"peerDependencies": {
"@lit/localize": "^0.12.2",
"lit": "^3.2.0"
},
"private": true,
"scripts": { "scripts": {
"build": "wireit", "build": "wireit",
"build:types": "wireit", "build:types": "wireit",
@ -47,11 +47,12 @@
"lint:lockfile": "wireit", "lint:lockfile": "wireit",
"lint:nightmare": "wireit", "lint:nightmare": "wireit",
"lint:package": "wireit", "lint:package": "wireit",
"lint:precommit": "wireit",
"lint:spelling": "wireit", "lint:spelling": "wireit",
"lint:types": "wireit", "lint:types": "wireit",
"precommit": "wireit",
"prettier": "wireit" "prettier": "wireit"
}, },
"type": "module",
"wireit": { "wireit": {
"build": { "build": {
"command": "${NODE_RUNNER} build.mjs", "command": "${NODE_RUNNER} build.mjs",
@ -88,16 +89,13 @@
] ]
}, },
"lint": { "lint": {
"command": "eslint --max-warnings 0 --fix", "command": "eslint --max-warnings 0 --fix --config ./eslint.config.mjs",
"env": { "env": {
"NODE_OPTIONS": "--max_old_space_size=65536" "NODE_OPTIONS": "--max_old_space_size=65536"
} }
}, },
"lint:types": { "lint:types": {
"command": "tsc --noEmit -p .", "command": "tsc --noEmit -p ."
"dependencies": [
"build-locales"
]
}, },
"lint:lockfile": { "lint:lockfile": {
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https" "command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
@ -105,6 +103,15 @@
"lint:package": { "lint:package": {
"command": "syncpack format -i ' '" "command": "syncpack format -i ' '"
}, },
"lint:precommit": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --precommit",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"lint:nightmare": { "lint:nightmare": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --nightmare", "command": "${NODE_RUNNER} ./scripts/eslint.mjs --nightmare",
"env": { "env": {
@ -114,8 +121,15 @@
} }
} }
}, },
"lint:precommit": { "precommit": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --precommit", "command": "prettier --write .",
"dependencies": [
"lint:types",
"lint:spelling",
"lint:lockfile",
"lint:package",
"lint:precommit"
],
"env": { "env": {
"NODE_RUNNER": { "NODE_RUNNER": {
"external": true, "external": true,

View File

@ -1,48 +0,0 @@
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import path from "path";
import process from "process";
// Code assumes this script is in the './web/scripts' folder.
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
process.chdir(path.join(projectRoot, "./web"));
const eslintConfig = {
overrideConfig: {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { avoidEscape: true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
},
},
};
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(["./src/**/*", "./build.mjs", "./scripts/*.mjs"]);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -13,6 +13,7 @@ export default [
{ {
ignores: [ ignores: [
"dist/", "dist/",
".wireit/",
// don't ever lint node_modules // don't ever lint node_modules
"node_modules/", "node_modules/",
".storybook/*", ".storybook/*",

View File

@ -32,7 +32,6 @@ export async function popupCenterScreen(
w: number, w: number,
h: number, h: number,
): Promise<Window | null> { ): Promise<Window | null> {
// eslint-disable-next-line no-magic-numbers
const [top, left] = [(screen.height - h) / 4, (screen.width - w) / 2]; const [top, left] = [(screen.height - h) / 4, (screen.width - w) / 2];
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {

View File

@ -3,7 +3,7 @@ import * as base64js from "base64-js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
export function b64enc(buf: Uint8Array): string { export function b64enc(buf: Uint8Array): string {
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
} }
export function b64RawEnc(buf: Uint8Array): string { export function b64RawEnc(buf: Uint8Array): string {

View File

@ -15,6 +15,17 @@ import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api";
export const TAG_SENTRY_COMPONENT = "authentik.component"; export const TAG_SENTRY_COMPONENT = "authentik.component";
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
const MIN_PATH_LENGTH = 2;
// Get the interface name from URL
export function currentInterface(): string {
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
let knownInterface = "unknown";
if (pathMatches && pathMatches.length >= MIN_PATH_LENGTH) {
knownInterface = pathMatches[1];
}
return knownInterface.toLowerCase();
}
export async function configureSentry(canDoPpi = false): Promise<Config> { export async function configureSentry(canDoPpi = false): Promise<Config> {
const cfg = await config(); const cfg = await config();
@ -81,13 +92,3 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
} }
return cfg; return cfg;
} }
// Get the interface name from URL
export function currentInterface(): string {
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
let currentInterface = "unknown";
if (pathMatches && pathMatches.length >= 2) {
currentInterface = pathMatches[1];
}
return currentInterface.toLowerCase();
}

View File

@ -5,10 +5,8 @@ import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
let globalMePromise: Promise<SessionUser> | undefined; let globalMePromise: Promise<SessionUser> | undefined;
export function refreshMe(): Promise<SessionUser> { const HTTP_FORBIDDEN = 403;
globalMePromise = undefined; const HTTP_UNAUTHORIZED = 401;
return me();
}
export function me(): Promise<SessionUser> { export function me(): Promise<SessionUser> {
if (!globalMePromise) { if (!globalMePromise) {
@ -48,7 +46,10 @@ export function me(): Promise<SessionUser> {
systemPermissions: [], systemPermissions: [],
}, },
}; };
if (ex.response?.status === 401 || ex.response?.status === 403) { if (
ex.response?.status === HTTP_UNAUTHORIZED ||
ex.response?.status === HTTP_FORBIDDEN
) {
const relativeUrl = window.location const relativeUrl = window.location
.toString() .toString()
.substring(window.location.origin.length); .substring(window.location.origin.length);
@ -61,3 +62,8 @@ export function me(): Promise<SessionUser> {
} }
return globalMePromise; return globalMePromise;
} }
export function refreshMe(): Promise<SessionUser> {
globalMePromise = undefined;
return me();
}

View File

@ -9,7 +9,7 @@ export function getCookie(name: string): string {
for (let i = 0; i < cookies.length; i++) { for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim(); const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want? // Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + "=") { if (cookie.substring(0, name.length + 1) === `${name}=`) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break; break;
} }
@ -25,21 +25,21 @@ export function convertToSlug(text: string): string {
.replace(/[^\w-]+/g, ""); .replace(/[^\w-]+/g, "");
} }
const WORD_COUNT_TRUNCATION_DEFAULT = 10;
/** /**
* Truncate a string based on maximum word count * Truncate a string based on maximum word count
*/ */
export function truncateWords(string: string, length = 10): string { export function truncateWords(input: string, length = WORD_COUNT_TRUNCATION_DEFAULT): string {
string = string || ""; const words = (input ?? "").trim().split(" ");
const array = string.trim().split(" "); const ellipsis = words.length > length ? "..." : "";
const ellipsis = array.length > length ? "..." : ""; return words.slice(0, length).join(" ") + ellipsis;
return array.slice(0, length).join(" ") + ellipsis;
} }
const CHAR_COUNT_TRUNCATION_DEFAULT = 10;
/** /**
* Truncate a string based on character count * Truncate a string based on character count
*/ */
export function truncate(string: string, length = 10): string { export function truncate(string: string, length = CHAR_COUNT_TRUNCATION_DEFAULT): string {
return string.length > length ? `${string.substring(0, length)}...` : string; return string.length > length ? `${string.substring(0, length)}...` : string;
} }
@ -83,20 +83,24 @@ export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const ascii_letters = ascii_lowercase + ascii_uppercase; export const ascii_letters = ascii_lowercase + ascii_uppercase;
export const digits = "0123456789"; export const digits = "0123456789";
export const hexdigits = digits + "abcdef" + "ABCDEF"; export const hexdigits = `${digits}abcdefABCDEF}`;
export const octdigits = "01234567"; export const octdigits = "01234567";
export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
const BYTE_SIZE = 256;
export function randomString(len: number, charset: string): string { export function randomString(len: number, charset: string): string {
const chars = []; const chars = [];
const array = new Uint8Array(len); const array = new Uint8Array(len);
self.crypto.getRandomValues(array); self.crypto.getRandomValues(array);
for (let index = 0; index < len; index++) { for (let index = 0; index < len; index++) {
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]); chars.push(charset[Math.floor(charset.length * (array[index] / BYTE_SIZE))]);
} }
return chars.join(""); return chars.join("");
} }
const TIMEZONE_OFFSET = 60000; // milliseconds
export function dateTimeLocal(date: Date): string { export function dateTimeLocal(date: Date): string {
// So for some reason, the datetime-local input field requires ISO Datetime as value // So for some reason, the datetime-local input field requires ISO Datetime as value
// But the standard javascript date.toISOString() returns everything with seconds and // But the standard javascript date.toISOString() returns everything with seconds and
@ -105,7 +109,7 @@ export function dateTimeLocal(date: Date): string {
// figure. // figure.
// Additionally, toISOString always returns the date without timezone, which we would like // Additionally, toISOString always returns the date without timezone, which we would like
// to include for better usability // to include for better usability
const tzOffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds const tzOffset = new Date().getTimezoneOffset() * TIMEZONE_OFFSET;
const localISOTime = new Date(date.getTime() - tzOffset).toISOString().slice(0, -1); const localISOTime = new Date(date.getTime() - tzOffset).toISOString().slice(0, -1);
const parts = localISOTime.split(":"); const parts = localISOTime.split(":");
return `${parts[0]}:${parts[1]}`; return `${parts[0]}:${parts[1]}`;
@ -122,7 +126,7 @@ export function dateToUTC(date: Date): Date {
// then subtract the timezone offset to create an "invalid" date (correct time&date) // then subtract the timezone offset to create an "invalid" date (correct time&date)
// but it still "thinks" it's in local TZ // but it still "thinks" it's in local TZ
const timestamp = date.getTime(); const timestamp = date.getTime();
const offset = -1 * (new Date().getTimezoneOffset() * 60000); const offset = -1 * (new Date().getTimezoneOffset() * TIMEZONE_OFFSET);
return new Date(timestamp - offset); return new Date(timestamp - offset);
} }
@ -151,13 +155,26 @@ export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): Ad
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet); return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
} }
const SECONDS_IN_A_MINUTE = 60;
const MINUTES_IN_AN_HOUR = 60;
const HOURS_IN_A_DAY = 24;
const DAYS_IN_A_YEAR = 365;
const MONTHS_IN_A_YEAR = 12;
const MILLISECONDS_IN_A_SECOND = 1000;
const MILLISECONDS_IN_A_MINUTE = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
const MILLISECONDS_IN_AN_HOUR = MILLISECONDS_IN_A_MINUTE * MINUTES_IN_AN_HOUR;
const MILLISECONDS_IN_A_DAY = MILLISECONDS_IN_AN_HOUR * HOURS_IN_A_DAY;
const MILLISECONDS_IN_A_YEAR = MILLISECONDS_IN_A_DAY * DAYS_IN_A_YEAR;
const MILLISECONDS_IN_A_MONTH = MILLISECONDS_IN_A_YEAR / MONTHS_IN_A_YEAR;
const _timeUnits = new Map<Intl.RelativeTimeFormatUnit, number>([ const _timeUnits = new Map<Intl.RelativeTimeFormatUnit, number>([
["year", 24 * 60 * 60 * 1000 * 365], ["year", MILLISECONDS_IN_A_YEAR],
["month", (24 * 60 * 60 * 1000 * 365) / 12], ["month", MILLISECONDS_IN_A_MONTH],
["day", 24 * 60 * 60 * 1000], ["day", MILLISECONDS_IN_A_DAY],
["hour", 60 * 60 * 1000], ["hour", MILLISECONDS_IN_AN_HOUR],
["minute", 60 * 1000], ["minute", MILLISECONDS_IN_A_MINUTE],
["second", 1000], ["second", MILLISECONDS_IN_A_SECOND],
]); ]);
export function getRelativeTime(d1: Date, d2: Date = new Date()): string { export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
@ -166,9 +183,9 @@ export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
// "Math.abs" accounts for both "past" & "future" scenarios // "Math.abs" accounts for both "past" & "future" scenarios
for (const [key, value] of _timeUnits) { for (const [key, value] of _timeUnits) {
if (Math.abs(elapsed) > value || key == "second") { if (Math.abs(elapsed) > value || key === "second") {
return rtf.format(Math.round(elapsed / value), key); return rtf.format(Math.round(elapsed / value), key);
} }
} }
return rtf.format(Math.round(elapsed / 1000), "second"); return rtf.format(Math.round(elapsed / MILLISECONDS_IN_A_SECOND), "second");
} }

View File

@ -7,9 +7,14 @@ export interface WSMessage {
message_type: string; message_type: string;
} }
const MESSAGE_RETRY_DELAY = 200; // milliseconds
const CLOSE_RETRY_DELAY = 6000; // milliseconds
const OPEN_RETRY_DELAY = 200; // milliseconds
const RETRY_BACKOFF = 2;
export class WebsocketClient { export class WebsocketClient {
messageSocket?: WebSocket; messageSocket?: WebSocket;
retryDelay = 200; retryDelay = MESSAGE_RETRY_DELAY;
constructor() { constructor() {
try { try {
@ -20,18 +25,20 @@ export class WebsocketClient {
} }
connect(): void { connect(): void {
if (navigator.webdriver) return; if (navigator.webdriver) {
return;
}
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${ const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host window.location.host
}/ws/client/`; }/ws/client/`;
this.messageSocket = new WebSocket(wsUrl); this.messageSocket = new WebSocket(wsUrl);
this.messageSocket.addEventListener("open", () => { this.messageSocket.addEventListener("open", () => {
console.debug(`authentik/ws: connected to ${wsUrl}`); console.debug(`authentik/ws: connected to ${wsUrl}`);
this.retryDelay = 200; this.retryDelay = OPEN_RETRY_DELAY;
}); });
this.messageSocket.addEventListener("close", (e) => { this.messageSocket.addEventListener("close", (e) => {
console.debug("authentik/ws: closed ws connection", e); console.debug("authentik/ws: closed ws connection", e);
if (this.retryDelay > 6000) { if (this.retryDelay > CLOSE_RETRY_DELAY) {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_MESSAGE, { new CustomEvent(EVENT_MESSAGE, {
bubbles: true, bubbles: true,
@ -47,7 +54,7 @@ export class WebsocketClient {
console.debug(`authentik/ws: reconnecting ws in ${this.retryDelay}ms`); console.debug(`authentik/ws: reconnecting ws in ${this.retryDelay}ms`);
this.connect(); this.connect();
}, this.retryDelay); }, this.retryDelay);
this.retryDelay = this.retryDelay * 2; this.retryDelay = this.retryDelay * RETRY_BACKOFF;
}); });
this.messageSocket.addEventListener("message", (e) => { this.messageSocket.addEventListener("message", (e) => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
@ -60,7 +67,7 @@ export class WebsocketClient {
); );
}); });
this.messageSocket.addEventListener("error", () => { this.messageSocket.addEventListener("error", () => {
this.retryDelay = this.retryDelay * 2; this.retryDelay = this.retryDelay * RETRY_BACKOFF;
}); });
} }
} }

View File

@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"outDir": "./dist/", "outDir": "./dist/",
"types": ["webauthn"],
"paths": { "paths": {
"@goauthentik/elements/*": ["./src/*"], "@goauthentik/elements/*": ["./src/*"],
"@goauthentik/locales/*": ["src/locales/*"] "@goauthentik/locales/*": ["src/locales/*"]