web: re-format with prettier

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-08-03 17:52:21 +02:00
parent 77ed25ae34
commit 2c60ec50be
218 changed files with 11696 additions and 8225 deletions

View File

@ -1,8 +1,5 @@
{
"presets": [
"@babel/env",
"@babel/typescript"
],
"presets": ["@babel/env", "@babel/typescript"],
"plugins": [
["@babel/plugin-proposal-private-methods", { "loose": true }],
[

View File

@ -14,11 +14,7 @@
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"lit",
"custom-elements"
],
"plugins": ["@typescript-eslint", "lit", "custom-elements"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],

9
web/.prettierignore Normal file
View File

@ -0,0 +1,9 @@
# 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
# don't lint generated code
api/
azure-pipelines.yml

19
web/.prettierrc.json Normal file
View File

@ -0,0 +1,19 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

View File

@ -49,6 +49,28 @@ stages:
command: 'custom'
workingDir: 'web/'
customCommand: 'run lint'
- job: prettier
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '16.x'
displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_api_client'
path: "web/api/"
- task: Npm@1
inputs:
command: 'install'
workingDir: 'web/'
- task: Npm@1
inputs:
command: 'custom'
workingDir: 'web/'
customCommand: 'run prettier-check'
- job: lit_analyse
pool:
vmImage: 'ubuntu-latest'

25
web/package-lock.json generated
View File

@ -47,6 +47,7 @@
"lit-element": "^2.5.1",
"lit-html": "^1.4.1",
"moment": "^2.29.1",
"prettier": "^2.3.2",
"rapidoc": "^9.0.0",
"rollup": "^2.55.1",
"rollup-plugin-commonjs": "^10.1.0",
@ -66,8 +67,8 @@
},
"api": {
"name": "authentik-api",
"version": "0.0.1",
"dependencies": {
"version": "1.0.0",
"devDependencies": {
"typescript": "^3.9.5"
}
},
@ -75,6 +76,7 @@
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -6405,6 +6407,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/pretty-format": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
@ -10217,7 +10230,8 @@
"typescript": {
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w=="
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true
}
}
},
@ -12932,6 +12946,11 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
},
"prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
},
"pretty-format": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",

View File

@ -8,7 +8,9 @@
"build": "lingui compile && rollup -c ./rollup.config.js",
"watch": "lingui compile && rollup -c -w",
"lint": "eslint . --max-warnings 0 --fix",
"lit-analyse": "lit-analyzer src"
"lit-analyse": "lit-analyzer src",
"prettier-check": "prettier --check .",
"prettier": "prettier --write ."
},
"lingui": {
"sourceLocale": "en",
@ -76,6 +78,7 @@
"lit-element": "^2.5.1",
"lit-html": "^1.4.1",
"moment": "^2.29.1",
"prettier": "^2.3.2",
"rapidoc": "^9.0.0",
"rollup": "^2.55.1",
"rollup-plugin-commonjs": "^10.1.0",

View File

@ -8,21 +8,37 @@ import copy from "rollup-plugin-copy";
import babel from "@rollup/plugin-babel";
import replace from "@rollup/plugin-replace";
const extensions = [
".js", ".jsx", ".ts", ".tsx",
];
const extensions = [".js", ".jsx", ".ts", ".tsx"];
const resources = [
{ src: "node_modules/rapidoc/dist/rapidoc-min.js", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/components/Page/page.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/components/EmptyState/empty-state.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/components/Spinner/spinner.css", dest: "dist/" },
{
src: "node_modules/@patternfly/patternfly/patternfly.min.css",
dest: "dist/",
},
{
src: "node_modules/@patternfly/patternfly/patternfly-base.css",
dest: "dist/",
},
{
src: "node_modules/@patternfly/patternfly/components/Page/page.css",
dest: "dist/",
},
{
src: "node_modules/@patternfly/patternfly/components/EmptyState/empty-state.css",
dest: "dist/",
},
{
src: "node_modules/@patternfly/patternfly/components/Spinner/spinner.css",
dest: "dist/",
},
{ src: "src/authentik.css", dest: "dist/" },
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
{
src: "node_modules/@patternfly/patternfly/assets/*",
dest: "dist/assets/",
},
{ src: "src/assets/*", dest: "dist/assets" },
{ src: "./icons/*", dest: "dist/assets/icons" },
];
@ -60,15 +76,15 @@ export default [
],
plugins: [
typescript({
"declaration": true,
"outDir": "./api/dist/",
declaration: true,
outDir: "./api/dist/",
}),
isProdBuild && terser(),
copy({
targets: [...resources],
copyOnce: false,
}),
].filter(p => p),
].filter((p) => p),
watch: {
clearScreen: false,
},
@ -81,14 +97,14 @@ export default [
format: "iife",
file: "dist/poly.js",
sourcemap: true,
}
},
],
plugins: [
cssimport(),
resolve({ browser: true }),
commonjs(),
isProdBuild && terser(),
].filter(p => p),
].filter((p) => p),
watch: {
clearScreen: false,
},
@ -102,7 +118,7 @@ export default [
dir: "dist",
sourcemap: true,
manualChunks: manualChunks,
chunkFileNames: "admin-[name].js"
chunkFileNames: "admin-[name].js",
},
],
plugins: [
@ -116,11 +132,11 @@ export default [
}),
replace({
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
preventAssignment: true
"preventAssignment": true,
}),
sourcemaps(),
isProdBuild && terser(),
].filter(p => p),
].filter((p) => p),
watch: {
clearScreen: false,
},
@ -134,7 +150,7 @@ export default [
dir: "dist",
sourcemap: true,
manualChunks: manualChunks,
chunkFileNames: "flow-[name].js"
chunkFileNames: "flow-[name].js",
},
],
plugins: [
@ -148,11 +164,11 @@ export default [
}),
replace({
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
preventAssignment: true
"preventAssignment": true,
}),
sourcemaps(),
isProdBuild && terser(),
].filter(p => p),
].filter((p) => p),
watch: {
clearScreen: false,
},

View File

@ -169,7 +169,9 @@ body {
color: var(--ak-dark-foreground) !important;
}
.pf-c-table__expandable-row.pf-m-expanded {
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(--ak-dark-background-lighter);
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(
--ak-dark-background-lighter
);
}
/* tabs */
.pf-c-tabs {
@ -214,7 +216,8 @@ body {
border-bottom: 0;
}
/* inputs */
optgroup, option {
optgroup,
option {
color: var(--ak-dark-foreground);
}
.pf-c-input-group {
@ -235,7 +238,10 @@ body {
background-color: var(--ak-dark-background-light);
}
.pf-c-button.pf-m-control {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter) var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor) var(--ak-dark-background-lighter);
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter)
var(--ak-dark-background-lighter)
var(--pf-c-button--m-control--after--BorderBottomColor)
var(--ak-dark-background-lighter);
background-color: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}

View File

@ -8,7 +8,6 @@ export interface WSMessage {
}
export class WebsocketClient {
messageSocket?: WebSocket;
retryDelay = 200;
@ -22,8 +21,9 @@ export class WebsocketClient {
connect(): void {
if (navigator.webdriver) return;
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${window.location.host
}/ws/client/`;
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host
}/ws/client/`;
this.messageSocket = new WebSocket(wsUrl);
this.messageSocket.addEventListener("open", () => {
console.debug(`authentik/ws: connected to ${wsUrl}`);
@ -34,7 +34,7 @@ export class WebsocketClient {
if (this.retryDelay > 3000) {
showMessage({
level: MessageLevel.error,
message: t`Connection error, reconnecting...`
message: t`Connection error, reconnecting...`,
});
}
setTimeout(() => {
@ -50,12 +50,11 @@ export class WebsocketClient {
bubbles: true,
composed: true,
detail: data as WSMessage,
})
}),
);
});
this.messageSocket.addEventListener("error", () => {
this.retryDelay = this.retryDelay * 2;
});
}
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import CodeMirror from "codemirror";
import "codemirror/addon/display/autorefresh";
@ -13,13 +21,13 @@ import "codemirror/mode/python/python.js";
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
import CodeMirrorTheme from "codemirror/theme/monokai.css";
import CodeMirrorDialogStyle from "codemirror/addon/dialog/dialog.css";
import CodeMirrorShowHintStyle from "codemirror/addon/hint/show-hint.css";
import CodeMirrorShowHintStyle from "codemirror/addon/hint/show-hint.css";
import { ifDefined } from "lit-html/directives/if-defined";
import YAML from "yaml";
@customElement("ak-codemirror")
export class CodeMirrorTextarea extends LitElement {
@property({type: Boolean})
@property({ type: Boolean })
readOnly = false;
@property()
@ -83,11 +91,17 @@ export class CodeMirrorTextarea extends LitElement {
}
static get styles(): CSSResult[] {
return [CodeMirrorStyle, CodeMirrorTheme, CodeMirrorDialogStyle, CodeMirrorShowHintStyle, css`
.CodeMirror-wrap pre {
word-break: break-word !important;
}
`];
return [
CodeMirrorStyle,
CodeMirrorTheme,
CodeMirrorDialogStyle,
CodeMirrorShowHintStyle,
css`
.CodeMirror-wrap pre {
word-break: break-word !important;
}
`,
];
}
firstUpdated(): void {
@ -102,7 +116,7 @@ export class CodeMirrorTextarea extends LitElement {
readOnly: this.readOnly,
autoRefresh: true,
lineWrapping: true,
value: this._value
value: this._value,
});
this.editor.on("blur", () => {
this.editor?.save();

View File

@ -4,34 +4,36 @@ import AKGlobal from "../authentik.css";
@customElement("ak-divider")
export class Divider extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, AKGlobal, css`
.separator {
display: flex;
align-items: center;
text-align: center;
}
return [
PFBase,
AKGlobal,
css`
.separator {
display: flex;
align-items: center;
text-align: center;
}
.separator::before,
.separator::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--pf-global--Color--100);
}
.separator::before,
.separator::after {
content: "";
flex: 1;
border-bottom: 1px solid var(--pf-global--Color--100);
}
.separator:not(:empty)::before {
margin-right: .25em;
}
.separator:not(:empty)::before {
margin-right: 0.25em;
}
.separator:not(:empty)::after {
margin-left: .25em;
}
`];
.separator:not(:empty)::after {
margin-left: 0.25em;
}
`,
];
}
render(): TemplateResult {
return html`<div class="separator"><slot></slot></div>`;
}
}

View File

@ -8,14 +8,13 @@ import { PFSize } from "./Spinner";
@customElement("ak-empty-state")
export class EmptyState extends LitElement {
@property({type: String})
@property({ type: String })
icon = "";
@property({type: Boolean})
@property({ type: Boolean })
loading = false;
@property({type: Boolean})
@property({ type: Boolean })
fullHeight = false;
@property()
@ -28,14 +27,16 @@ export class EmptyState extends LitElement {
render(): TemplateResult {
return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}">
<div class="pf-c-empty-state__content">
${this.loading ?
html`<div class="pf-c-empty-state__icon">
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
</div>`:
html`<i class="pf-icon fa ${this.icon || "fa-question-circle"} pf-c-empty-state__icon" aria-hidden="true"></i>`}
<h1 class="pf-c-title pf-m-lg">
${this.header}
</h1>
${this.loading
? html`<div class="pf-c-empty-state__icon">
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
</div>`
: html`<i
class="pf-icon fa ${this.icon ||
"fa-question-circle"} pf-c-empty-state__icon"
aria-hidden="true"
></i>`}
<h1 class="pf-c-title pf-m-lg">${this.header}</h1>
<div class="pf-c-empty-state__body">
<slot name="body"></slot>
</div>
@ -45,5 +46,4 @@ export class EmptyState extends LitElement {
</div>
</div>`;
}
}

View File

@ -4,7 +4,6 @@ import PFExpandableSection from "../../node_modules/@patternfly/patternfly/compo
@customElement("ak-expand")
export class Expand extends LitElement {
@property({ type: Boolean })
expanded = false;
@ -20,16 +19,22 @@ export class Expand extends LitElement {
render(): TemplateResult {
return html`<div class="pf-c-expandable-section ${this.expanded ? "pf-m-expanded" : ""}">
<button type="button" class="pf-c-expandable-section__toggle" aria-expanded="${this.expanded}" @click=${() => {
this.expanded = !this.expanded;
}}>
<button
type="button"
class="pf-c-expandable-section__toggle"
aria-expanded="${this.expanded}"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-expandable-section__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
<span class="pf-c-expandable-section__toggle-text">${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span>
<span class="pf-c-expandable-section__toggle-text"
>${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span
>
</button>
<slot ?hidden=${!this.expanded} class="pf-c-expandable-section__content"></slot>
</div>`;
}
}

View File

@ -12,7 +12,6 @@ export enum PFColor {
@customElement("ak-label")
export class Label extends LitElement {
@property()
color: PFColor = PFColor.Grey;
@ -45,11 +44,14 @@ export class Label extends LitElement {
return html`<span class="pf-c-label ${this.color}">
<span class="pf-c-label__content">
<span class="pf-c-label__icon">
<i class="fas ${this.text ? "fa-fw" : ""} ${this.icon || this.getDefaultIcon()}" aria-hidden="true"></i>
<i
class="fas ${this.text ? "fa-fw" : ""} ${this.icon ||
this.getDefaultIcon()}"
aria-hidden="true"
></i>
</span>
${this.text || ""}
</span>
</span>`;
}
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import AKGlobal from "../authentik.css";
@ -10,19 +18,18 @@ import { EventsApi } from "../../api/dist";
@customElement("ak-page-header")
export class PageHeader extends LitElement {
@property()
icon?: string;
@property({type: Boolean})
iconImage = false
@property({ type: Boolean })
iconImage = false;
@property({type: Boolean})
@property({ type: Boolean })
hasNotifications = false;
@property()
set header(value: string) {
tenant().then(tenant => {
tenant().then((tenant) => {
if (value !== "") {
document.title = `${value} - ${tenant.brandingTitle}`;
} else {
@ -42,33 +49,40 @@ export class PageHeader extends LitElement {
_header = "";
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFPage, PFContent, AKGlobal, css`
:host {
display: flex;
flex-direction: row;
min-height: 114px;
}
.pf-c-button.pf-m-plain {
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
border-radius: 0px;
}
.pf-c-page__main-section {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
img.pf-icon {
max-height: 24px;
}
.sidebar-trigger,
.notification-trigger {
font-size: 24px;
}
.notification-trigger.has-notifications {
color: #2B9AF3;
}
`];
return [
PFBase,
PFButton,
PFPage,
PFContent,
AKGlobal,
css`
:host {
display: flex;
flex-direction: row;
min-height: 114px;
}
.pf-c-button.pf-m-plain {
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
border-radius: 0px;
}
.pf-c-page__main-section {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
img.pf-icon {
max-height: 24px;
}
.sidebar-trigger,
.notification-trigger {
font-size: 24px;
}
.notification-trigger.has-notifications {
color: #2b9af3;
}
`,
];
}
renderIcon(): TemplateResult {
@ -82,50 +96,51 @@ export class PageHeader extends LitElement {
}
firstUpdated(): void {
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false,
ordering: "-created",
pageSize: 1,
}).then(r => {
this.hasNotifications = r.pagination.count > 0;
});
new EventsApi(DEFAULT_CONFIG)
.eventsNotificationsList({
seen: false,
ordering: "-created",
pageSize: 1,
})
.then((r) => {
this.hasNotifications = r.pagination.count > 0;
});
}
render(): TemplateResult {
return html`<button
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
})
);
}}>
<i class="fas fa-bars"></i>
</button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
${this.renderIcon()}
${this.header}
</h1>
${this.description ?
html`<p>${this.description}</p>` : html``}
</div>
</section>
<button
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications ? "has-notifications" : ""}"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
bubbles: true,
composed: true,
})
);
}}>
<i class="fas fa-bell"></i>
</button>`;
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bars"></i>
</button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>${this.renderIcon()} ${this.header}</h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>
</section>
<button
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications
? "has-notifications"
: ""}"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bell"></i>
</button>`;
}
}

View File

@ -20,13 +20,13 @@ export class Spinner extends LitElement {
render(): TemplateResult {
return html`<span
class="pf-c-spinner ${this.size.toString()}"
role="progressbar"
aria-valuetext="${t`Loading...`}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>`;
class="pf-c-spinner ${this.size.toString()}"
role="progressbar"
aria-valuetext="${t`Loading...`}"
>
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>`;
}
}

View File

@ -1,4 +1,12 @@
import { LitElement, html, customElement, property, CSSResult, TemplateResult, css } from "lit-element";
import {
LitElement,
html,
customElement,
property,
CSSResult,
TemplateResult,
css,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
@ -11,24 +19,29 @@ export class Tabs extends LitElement {
@property()
currentPage?: string;
@property({type: Boolean})
@property({ type: Boolean })
vertical = false;
static get styles(): CSSResult[] {
return [PFGlobal, PFTabs, AKGlobal, css`
::slotted(*) {
flex-grow: 2;
}
:host([vertical]) {
display: flex;
}
:host([vertical]) .pf-c-tabs {
width: auto !important;
}
:host([vertical]) .pf-c-tabs__list {
height: 100%;
}
`];
return [
PFGlobal,
PFTabs,
AKGlobal,
css`
::slotted(*) {
flex-grow: 2;
}
:host([vertical]) {
display: flex;
}
:host([vertical]) .pf-c-tabs {
width: auto !important;
}
:host([vertical]) .pf-c-tabs__list {
height: 100%;
}
`,
];
}
observer: MutationObserver;
@ -42,7 +55,11 @@ export class Tabs extends LitElement {
connectedCallback(): void {
super.connectedCallback();
this.observer.observe(this, { attributes: true, childList: true, subtree: true });
this.observer.observe(this, {
attributes: true,
childList: true,
subtree: true,
});
}
disconnectedCallback(): void {
@ -61,9 +78,7 @@ export class Tabs extends LitElement {
const slot = page.attributes.getNamedItem("slot")?.value;
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
<span class="pf-c-tabs__item-text">
${page.getAttribute("data-tab-title")}
</span>
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
</button>
</li>`;
}

View File

@ -5,10 +5,11 @@ import { MessageLevel } from "../messages/Message";
@customElement("ak-action-button")
export class ActionButton extends SpinnerButton {
@property({attribute: false})
@property({ attribute: false })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apiRequest: () => Promise<any> = () => { throw new Error(); };
apiRequest: () => Promise<any> = () => {
throw new Error();
};
callAction = (): Promise<void> => {
this.setLoading();
@ -16,13 +17,13 @@ export class ActionButton extends SpinnerButton {
if (e instanceof Error) {
showMessage({
level: MessageLevel.error,
message: e.toString()
message: e.toString(),
});
} else {
e.text().then(t => {
e.text().then((t) => {
showMessage({
level: MessageLevel.error,
message: t
message: t,
});
});
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css";
@ -35,11 +43,25 @@ export class ModalButton extends LitElement {
@property()
size: PFSize = PFSize.Large;
@property({type: Boolean})
@property({ type: Boolean })
open = false;
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFModalBox, PFForm, PFTitle, PFFormControl, PFBullseye, PFBackdrop, PFPage, PFCard, PFContent, AKGlobal, MODAL_BUTTON_STYLES];
return [
PFBase,
PFButton,
PFModalBox,
PFForm,
PFTitle,
PFFormControl,
PFBullseye,
PFBackdrop,
PFPage,
PFCard,
PFContent,
AKGlobal,
MODAL_BUTTON_STYLES,
];
}
constructor() {
@ -53,7 +75,7 @@ export class ModalButton extends LitElement {
}
resetForms(): void {
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
if ("resetForm" in form) {
form?.resetForm();
}
@ -62,7 +84,7 @@ export class ModalButton extends LitElement {
onClick(): void {
this.open = true;
this.querySelectorAll("*").forEach(child => {
this.querySelectorAll("*").forEach((child) => {
if ("requestUpdate" in child) {
(child as LitElement).requestUpdate();
}
@ -70,17 +92,13 @@ export class ModalButton extends LitElement {
}
renderModalInner(): TemplateResult {
return html`<slot name='modal'></slot>`;
return html`<slot name="modal"></slot>`;
}
renderModal(): TemplateResult {
return html`<div class="pf-c-backdrop">
<div class="pf-l-bullseye">
<div
class="pf-c-modal-box ${this.size}"
role="dialog"
aria-modal="true"
>
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
<button
@click=${() => (this.open = false)}
class="pf-c-button pf-m-plain"
@ -99,5 +117,4 @@ export class ModalButton extends LitElement {
return html` <slot name="trigger" @click=${() => this.onClick()}></slot>
${this.open ? this.renderModal() : ""}`;
}
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
@ -8,7 +16,7 @@ import { ERROR_CLASS, PRIMARY_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "../..
@customElement("ak-spinner-button")
export class SpinnerButton extends LitElement {
@property({type: Boolean})
@property({ type: Boolean })
isRunning = false;
@property()
@ -60,17 +68,20 @@ export class SpinnerButton extends LitElement {
}
this.setLoading();
if (this.callAction) {
this.callAction().then(() => {
this.setDone(SUCCESS_CLASS);
}).catch(() => {
this.setDone(ERROR_CLASS);
});
this.callAction()
.then(() => {
this.setDone(SUCCESS_CLASS);
})
.catch(() => {
this.setDone(ERROR_CLASS);
});
}
}}>
}}
>
${this.isRunning
? html` <span class="pf-c-button__progress">
<ak-spinner size=${PFSize.Medium}></ak-spinner>
</span>`
<ak-spinner size=${PFSize.Medium}></ak-spinner>
</span>`
: ""}
<slot></slot>
</button>`;

View File

@ -17,23 +17,25 @@ export class TokenCopyButton extends ActionButton {
if (!this.identifier) {
return Promise.reject();
}
return new CoreApi(DEFAULT_CONFIG).coreTokensViewKeyRetrieve({
identifier: this.identifier
}).then((token) => {
if (!token.key) {
return Promise.reject();
}
return navigator.clipboard.writeText(token.key).then(() => {
this.buttonClass = SUCCESS_CLASS;
setTimeout(() => {
this.buttonClass = PRIMARY_CLASS;
}, 1500);
return new CoreApi(DEFAULT_CONFIG)
.coreTokensViewKeyRetrieve({
identifier: this.identifier,
})
.then((token) => {
if (!token.key) {
return Promise.reject();
}
return navigator.clipboard.writeText(token.key).then(() => {
this.buttonClass = SUCCESS_CLASS;
setTimeout(() => {
this.buttonClass = PRIMARY_CLASS;
}, 1500);
});
})
.catch((err: Response | undefined) => {
return err?.json().then((errResp) => {
throw new Error(errResp["detail"]);
});
});
}).catch((err: Response | undefined) => {
return err?.json().then(errResp => {
throw new Error(errResp["detail"]);
});
});
}
};
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -17,22 +25,24 @@ export class AggregateCard extends LitElement {
headerLink?: string;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFFlex, AKGlobal].concat([css`
.pf-c-card.pf-c-card-aggregate {
height: 100%;
}
.pf-c-card__header {
flex-wrap: nowrap;
}
.center-value {
font-size: var(--pf-global--icon--FontSize--lg);
text-align: center;
color: var(--pf-global--Color--100);
}
.subtext {
font-size: var(--pf-global--FontSize--sm);
}
`]);
return [PFBase, PFCard, PFFlex, AKGlobal].concat([
css`
.pf-c-card.pf-c-card-aggregate {
height: 100%;
}
.pf-c-card__header {
flex-wrap: nowrap;
}
.center-value {
font-size: var(--pf-global--icon--FontSize--lg);
text-align: center;
color: var(--pf-global--Color--100);
}
.subtext {
font-size: var(--pf-global--FontSize--sm);
}
`,
]);
}
renderInner(): TemplateResult {
@ -40,9 +50,11 @@ export class AggregateCard extends LitElement {
}
renderHeaderLink(): TemplateResult {
return html`${this.headerLink ? html`<a href="${this.headerLink}">
<i class="fa fa-link"> </i>
</a>` : ""}`;
return html`${this.headerLink
? html`<a href="${this.headerLink}">
<i class="fa fa-link"> </i>
</a>`
: ""}`;
}
render(): TemplateResult {
@ -53,10 +65,7 @@ export class AggregateCard extends LitElement {
</div>
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body center-value">
${this.renderInner()}
</div>
<div class="pf-c-card__body center-value">${this.renderInner()}</div>
</div>`;
}
}

View File

@ -6,14 +6,14 @@ import { PFSize } from "../Spinner";
@customElement("ak-aggregate-card-promise")
export class AggregatePromiseCard extends AggregateCard {
@property({attribute: false})
@property({ attribute: false })
promise?: Promise<Record<string, unknown>>;
promiseProxy(): Promise<TemplateResult> {
if (!this.promise) {
return new Promise<TemplateResult>(() => html``);
}
return this.promise.then(s => {
return this.promise.then((s) => {
return html`<i class="fa fa-check-circle"></i>&nbsp;${s.toString()}`;
});
}
@ -23,5 +23,4 @@ export class AggregatePromiseCard extends AggregateCard {
${until(this.promiseProxy(), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
</p>`;
}
}

View File

@ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-charts-admin-login")
export class AdminLoginsChart extends AKChart<LoginMetrics> {
apiRequest(): Promise<LoginMetrics> {
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
}
@ -18,26 +17,27 @@ export class AdminLoginsChart extends AKChart<LoginMetrics> {
label: "Failed Logins",
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data: data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
{
label: "Successful Logins",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
]
],
};
}
}

View File

@ -6,12 +6,13 @@ import { ChartData } from "chart.js";
@customElement("ak-charts-application-authorize")
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
@property()
applicationSlug!: string;
apiRequest(): Promise<Coordinate[]> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({ slug: this.applicationSlug });
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({
slug: this.applicationSlug,
});
}
getChartData(data: Coordinate[]): ChartData {
@ -21,15 +22,15 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
label: "Authorizations",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
]
],
};
}
}

View File

@ -6,7 +6,7 @@ import { ArcElement, BarElement } from "chart.js";
import { TimeScale, LinearScale } from "chart.js";
import "chartjs-adapter-moment";
import { FONT_COLOUR_DARK_MODE, FONT_COLOUR_LIGHT_MODE } from "../../pages/flows/FlowDiagram";
import {EVENT_REFRESH} from "../../constants";
import { EVENT_REFRESH } from "../../constants";
Chart.register(Legend, Tooltip);
Chart.register(LineController, BarController, DoughnutController);
@ -14,7 +14,6 @@ Chart.register(ArcElement, BarElement);
Chart.register(TimeScale, LinearScale);
export abstract class AKChart<T> extends LitElement {
abstract apiRequest(): Promise<T>;
abstract getChartData(data: T): ChartData;
@ -26,15 +25,17 @@ export abstract class AKChart<T> extends LitElement {
fontColour = FONT_COLOUR_LIGHT_MODE;
static get styles(): CSSResult[] {
return [css`
.container {
height: 100%;
}
canvas {
width: 100px;
height: 100px;
}
`];
return [
css`
.container {
height: 100%;
}
canvas {
width: 100px;
height: 100px;
}
`,
];
}
constructor() {
@ -99,12 +100,14 @@ export abstract class AKChart<T> extends LitElement {
chart.ctx.textBaseline = "middle";
chart.ctx.fillStyle = this.fontColour;
const textX = Math.round((width - chart.ctx.measureText(this.centerText).width) / 2);
const textX = Math.round(
(width - chart.ctx.measureText(this.centerText).width) / 2,
);
const textY = height / 2;
chart.ctx.fillText(this.centerText, textX, textY);
}
}
},
},
];
}
@ -116,8 +119,12 @@ export abstract class AKChart<T> extends LitElement {
type: "time",
display: true,
ticks: {
callback: function (tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = (ticks[index]);
callback: function (
tickValue: string | number,
index: number,
ticks: Tick[],
): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return `${ago} Hours ago`;
@ -129,7 +136,7 @@ export abstract class AKChart<T> extends LitElement {
grid: {
color: "rgba(0, 0, 0, 0)",
},
offset: true
offset: true,
},
y: {
type: "linear",
@ -138,7 +145,7 @@ export abstract class AKChart<T> extends LitElement {
grid: {
color: "rgba(0, 0, 0, 0)",
},
}
},
},
} as ChartOptions;
}

View File

@ -6,8 +6,7 @@ import { ChartData } from "chart.js";
@customElement("ak-charts-user")
export class UserChart extends AKChart<UserMetrics> {
@property({type: Number})
@property({ type: Number })
userId?: number;
apiRequest(): Promise<UserMetrics> {
@ -23,37 +22,39 @@ export class UserChart extends AKChart<UserMetrics> {
label: "Failed Logins",
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data: data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
{
label: "Successful Logins",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
{
label: "Application authorizations",
backgroundColor: "rgba(43, 154, 243, .5)",
spanGaps: true,
data: data.authorizationsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
data:
data.authorizationsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
]
],
};
}
}

View File

@ -7,11 +7,10 @@ import AKGlobal from "../../authentik.css";
@customElement("ak-chip")
export class Chip extends LitElement {
@property()
value?: number | string;
@property({type: Boolean})
@property({ type: Boolean })
removable = false;
static get styles(): CSSResult[] {
@ -24,16 +23,23 @@ export class Chip extends LitElement {
<span class="pf-c-chip__text">
<slot></slot>
</span>
${this.removable ? html`<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
this.dispatchEvent(new CustomEvent("remove", {
bubbles: true,
composed: true,
}));
}}>
<i class="fas fa-times" aria-hidden="true"></i>
</button>` : html``}
${this.removable
? html`<button
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
this.dispatchEvent(
new CustomEvent("remove", {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>`
: html``}
</div>
</li>`;
}
}

View File

@ -9,7 +9,6 @@ import { Chip } from "./Chip";
@customElement("ak-chip-group")
export class ChipGroup extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFChip, PFChipGroup, PFButton, AKGlobal];
}
@ -20,7 +19,7 @@ export class ChipGroup extends LitElement {
get value(): (string | number | undefined)[] {
const values: (string | number | undefined)[] = [];
this.querySelectorAll<Chip>("ak-chip").forEach(chip => {
this.querySelectorAll<Chip>("ak-chip").forEach((chip) => {
values.push(chip.value);
});
return values;
@ -28,12 +27,11 @@ export class ChipGroup extends LitElement {
render(): TemplateResult {
return html`<div class="pf-c-chip-group">
<div class="pf-c-chip-group__main">
<ul class="pf-c-chip-group__list" role="list">
<slot></slot>
</ul>
</div>
</div>`;
<div class="pf-c-chip-group__main">
<ul class="pf-c-chip-group__list" role="list">
<slot></slot>
</ul>
</div>
</div>`;
}
}

View File

@ -55,32 +55,28 @@ export class ObjectChangelog extends Table<Event> {
return [
html`${item.action}`,
html`<div>${item.user?.username}</div>
${item.user.on_behalf_of ? html`<small>
${t`On behalf of ${item.user.on_behalf_of.username}`}
</small>` : html``}`,
${item.user.on_behalf_of
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
: html``}`,
html`<span>${item.created?.toLocaleString()}</span>`,
html`<span>${item.clientIp || "-"}</span>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
return html` <td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
<div slot="body">
${t`No matching events could be found.`}
</div>
<div slot="body">${t`No matching events could be found.`}</div>
</ak-empty-state>`);
}
}

View File

@ -29,7 +29,7 @@ export class ObjectChangelog extends Table<Event> {
page: page,
ordering: this.order,
pageSize: PAGE_SIZE / 2,
username: this.targetUser
username: this.targetUser,
});
}
@ -46,32 +46,28 @@ export class ObjectChangelog extends Table<Event> {
return [
html`${item.action}`,
html`<div>${item.user?.username}</div>
${item.user.on_behalf_of ? html`<small>
${t`On behalf of ${item.user.on_behalf_of.username}`}
</small>` : html``}`,
${item.user.on_behalf_of
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
: html``}`,
html`<span>${item.created?.toLocaleString()}</span>`,
html`<span>${item.clientIp || "-"}</span>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
return html` <td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
<div slot="body">
${t`No matching events could be found.`}
</div>
<div slot="body">${t`No matching events could be found.`}</div>
</ak-empty-state>`);
}
}

View File

@ -8,7 +8,6 @@ import { showMessage } from "../messages/MessageContainer";
@customElement("ak-forms-confirm")
export class ConfirmationForm extends ModalButton {
@property()
successMessage!: string;
@property()
@ -17,23 +16,25 @@ export class ConfirmationForm extends ModalButton {
@property()
action!: string;
@property({attribute: false})
@property({ attribute: false })
onConfirm!: () => Promise<unknown>;
confirm(): Promise<void> {
return this.onConfirm().then(() => {
this.onSuccess();
this.open = false;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}).catch((e) => {
this.onError(e);
throw e;
});
return this.onConfirm()
.then(() => {
this.onSuccess();
this.open = false;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
})
.catch((e) => {
this.onError(e);
throw e;
});
}
onSuccess(): void {
@ -52,33 +53,34 @@ export class ConfirmationForm extends ModalButton {
renderModalInner(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">
<slot name="header"></slot>
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<form class="pf-c-form pf-m-horizontal">
<slot class="pf-c-content" name="body"></slot>
</form>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-danger">
${this.action}
</ak-spinner-button>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary">
${t`Cancel`}
</ak-spinner-button>
</footer>`;
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">
<slot name="header"></slot>
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<form class="pf-c-form pf-m-horizontal">
<slot class="pf-c-content" name="body"></slot>
</form>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-danger"
>
${this.action} </ak-spinner-button
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -11,42 +11,43 @@ import { until } from "lit-html/directives/until";
@customElement("ak-forms-delete")
export class DeleteForm extends ModalButton {
static get styles(): CSSResult[] {
return super.styles.concat(PFList);
}
@property({attribute: false})
@property({ attribute: false })
obj?: Record<string, unknown>;
@property()
objectLabel?: string;
@property({attribute: false})
@property({ attribute: false })
usedBy?: () => Promise<UsedBy[]>;
@property({attribute: false})
@property({ attribute: false })
delete!: () => Promise<unknown>;
confirm(): Promise<void> {
return this.delete().then(() => {
this.onSuccess();
this.open = false;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}).catch((e) => {
this.onError(e);
throw e;
});
return this.delete()
.then(() => {
this.onSuccess();
this.open = false;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
})
.catch((e) => {
this.onError(e);
throw e;
});
}
onSuccess(): void {
showMessage({
message: t`Successfully deleted ${this.objectLabel} ${ this.obj?.name }`,
message: t`Successfully deleted ${this.objectLabel} ${this.obj?.name}`,
level: MessageLevel.success,
});
}
@ -66,69 +67,70 @@ export class DeleteForm extends ModalButton {
objName = "";
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">
${t`Delete ${this.objectLabel}`}
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<form class="pf-c-form pf-m-horizontal">
<p>
${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}
</p>
</form>
</section>
${this.usedBy ? until(this.usedBy().then(usedBy => {
if (usedBy.length < 1) {
return html``;
}
return html`
<section class="pf-c-page__main-section">
<form class="pf-c-form pf-m-horizontal">
<p>
${t`The following objects use ${objName} `}
</p>
<ul class="pf-c-list">
${usedBy.map(ub => {
let consequence = "";
switch (ub.action) {
case UsedByActionEnum.Cascade:
consequence = t`object will be DELETED`;
break;
case UsedByActionEnum.CascadeMany:
consequence = t`connecting object will be deleted`;
break;
case UsedByActionEnum.SetDefault:
consequence = t`reference will be reset to default value`;
break;
case UsedByActionEnum.SetNull:
consequence = t`reference will be set to an empty value`;
break;
}
return html`<li>${t`${ub.name} (${consequence})`}</li>`;
})}
</ul>
</form>
</section>
`;
})) : html``}
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-danger">
${t`Delete`}
</ak-spinner-button>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary">
${t`Cancel`}
</ak-spinner-button>
</footer>`;
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">${t`Delete ${this.objectLabel}`}</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<form class="pf-c-form pf-m-horizontal">
<p>${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}</p>
</form>
</section>
${this.usedBy
? until(
this.usedBy().then((usedBy) => {
if (usedBy.length < 1) {
return html``;
}
return html`
<section class="pf-c-page__main-section">
<form class="pf-c-form pf-m-horizontal">
<p>${t`The following objects use ${objName} `}</p>
<ul class="pf-c-list">
${usedBy.map((ub) => {
let consequence = "";
switch (ub.action) {
case UsedByActionEnum.Cascade:
consequence = t`object will be DELETED`;
break;
case UsedByActionEnum.CascadeMany:
consequence = t`connecting object will be deleted`;
break;
case UsedByActionEnum.SetDefault:
consequence = t`reference will be reset to default value`;
break;
case UsedByActionEnum.SetNull:
consequence = t`reference will be set to an empty value`;
break;
}
return html`<li>
${t`${ub.name} (${consequence})`}
</li>`;
})}
</ul>
</form>
</section>
`;
}),
)
: html``}
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-danger"
>
${t`Delete`} </ak-spinner-button
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -2,7 +2,15 @@ import "@polymer/paper-input/paper-input";
import "@polymer/iron-form/iron-form";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { showMessage } from "../../elements/messages/MessageContainer";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -18,31 +26,38 @@ import { ValidationError } from "authentik-api";
import { EVENT_REFRESH } from "../../constants";
export class APIError extends Error {
constructor(public response: ValidationError) {
super();
}
}
@customElement("ak-form")
export class Form<T> extends LitElement {
@property()
successMessage = "";
@property()
send!: (data: T) => Promise<unknown>;
@property({attribute: false})
@property({ attribute: false })
nonFieldErrors?: string[];
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFInputGroup, PFFormControl, AKGlobal, css`
select[multiple] {
height: 15em;
}
`];
return [
PFBase,
PFCard,
PFButton,
PFForm,
PFAlert,
PFInputGroup,
PFFormControl,
AKGlobal,
css`
select[multiple] {
height: 15em;
}
`,
];
}
get isInViewport(): boolean {
@ -55,25 +70,27 @@ export class Form<T> extends LitElement {
}
updated(): void {
this.shadowRoot?.querySelectorAll<HTMLInputElement>("input[name=name]").forEach(nameInput => {
const form = nameInput.closest("form");
if (form === null) {
return;
}
const slugField = form.querySelector<HTMLInputElement>("input[name=slug]");
if (!slugField) {
return;
}
// Only attach handler if the slug is already equal to the name
// if not, they are probably completely different and shouldn't update
// each other
if (convertToSlug(nameInput.value) !== slugField.value) {
return;
}
nameInput.addEventListener("input", () => {
slugField.value = convertToSlug(nameInput.value);
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[name=name]")
.forEach((nameInput) => {
const form = nameInput.closest("form");
if (form === null) {
return;
}
const slugField = form.querySelector<HTMLInputElement>("input[name=slug]");
if (!slugField) {
return;
}
// Only attach handler if the slug is already equal to the name
// if not, they are probably completely different and shouldn't update
// each other
if (convertToSlug(nameInput.value) !== slugField.value) {
return;
}
nameInput.addEventListener("input", () => {
slugField.value = convertToSlug(nameInput.value);
});
});
});
}
/**
@ -110,7 +127,7 @@ export class Form<T> extends LitElement {
serializeForm(form: IronFormElement): T {
const elements: HTMLInputElement[] = form._getSubmittableElements();
const json: { [key: string]: unknown } = {};
elements.forEach(element => {
elements.forEach((element) => {
const values = form._serializeElementValues(element);
if (element.hidden) {
return;
@ -138,54 +155,58 @@ export class Form<T> extends LitElement {
return;
}
const data = this.serializeForm(ironForm);
return this.send(data).then((r) => {
showMessage({
level: MessageLevel.success,
message: this.getSuccessMessage()
});
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
return r;
}).catch((ex: Response | Error) => {
if (ex instanceof Error) {
throw ex;
}
if (ex.status > 399 && ex.status < 500) {
return ex.json().then((errorMessage: ValidationError) => {
if (!errorMessage) return errorMessage;
if (errorMessage instanceof Error) {
throw errorMessage;
}
// assign all input-related errors to their elements
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
elements.forEach((element) => {
const elementName = element.name;
if (!elementName) return;
if (camelToSnake(elementName) in errorMessage) {
element.errorMessage = errorMessage[camelToSnake(elementName)].join(", ");
element.invalid = true;
}
});
if ("non_field_errors" in errorMessage) {
this.nonFieldErrors = errorMessage["non_field_errors"];
}
throw new APIError(errorMessage);
return this.send(data)
.then((r) => {
showMessage({
level: MessageLevel.success,
message: this.getSuccessMessage(),
});
}
throw ex;
}).catch((ex: Error) => {
// error is local or not from rest_framework
showMessage({
message: ex.toString(),
level: MessageLevel.error,
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
return r;
})
.catch((ex: Response | Error) => {
if (ex instanceof Error) {
throw ex;
}
if (ex.status > 399 && ex.status < 500) {
return ex.json().then((errorMessage: ValidationError) => {
if (!errorMessage) return errorMessage;
if (errorMessage instanceof Error) {
throw errorMessage;
}
// assign all input-related errors to their elements
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
elements.forEach((element) => {
const elementName = element.name;
if (!elementName) return;
if (camelToSnake(elementName) in errorMessage) {
element.errorMessage =
errorMessage[camelToSnake(elementName)].join(", ");
element.invalid = true;
}
});
if ("non_field_errors" in errorMessage) {
this.nonFieldErrors = errorMessage["non_field_errors"];
}
throw new APIError(errorMessage);
});
}
throw ex;
})
.catch((ex: Error) => {
// error is local or not from rest_framework
showMessage({
message: ex.toString(),
level: MessageLevel.error,
});
// rethrow the error so the form doesn't close
throw ex;
});
// rethrow the error so the form doesn't close
throw ex;
});
}
renderForm(): TemplateResult {
@ -197,24 +218,24 @@ export class Form<T> extends LitElement {
return html``;
}
return html`<div class="pf-c-form__alert">
${this.nonFieldErrors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err}
</h4>
</div>`;
})}
${this.nonFieldErrors.map((err) => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">${err}</h4>
</div>`;
})}
</div>`;
}
renderVisible(): TemplateResult {
return html`<iron-form
@iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}>
${this.renderNonFieldErrors()}
${this.renderForm()}
@iron-form-presubmit=${(ev: Event) => {
this.submit(ev);
}}
>
${this.renderNonFieldErrors()} ${this.renderForm()}
</iron-form>`;
}
@ -224,5 +245,4 @@ export class Form<T> extends LitElement {
}
return this.renderVisible();
}
}

View File

@ -6,16 +6,19 @@ import { ErrorDetail } from "authentik-api";
@customElement("ak-form-element")
export class FormElement extends LitElement {
static get styles(): CSSResult[] {
return [PFForm, PFFormControl, css`
slot {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
`];
return [
PFForm,
PFFormControl,
css`
slot {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
`,
];
}
@property()
@ -28,22 +31,23 @@ export class FormElement extends LitElement {
errors?: ErrorDetail[];
updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
input.focus();
});
}
render(): TemplateResult {
return html`<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``}
</label>
<slot></slot>
${(this.errors || []).map((error) => {
return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`;
})}
</div>`;
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
: html``}
</label>
<slot></slot>
${(this.errors || []).map((error) => {
return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`;
})}
</div>`;
}
}

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -7,25 +15,37 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-form-group")
export class FormGroup extends LitElement {
@property({ type: Boolean })
expanded = false;
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFFormControl, AKGlobal, css`
slot[name=body][hidden] {
display: none !important;
}
`];
return [
PFBase,
PFForm,
PFButton,
PFFormControl,
AKGlobal,
css`
slot[name="body"][hidden] {
display: none !important;
}
`,
];
}
render(): TemplateResult {
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
<div class="pf-c-form__field-group-toggle">
<div class="pf-c-form__field-group-toggle-button">
<button class="pf-c-button pf-m-plain" type="button" aria-expanded="${this.expanded}" aria-label="Details" @click=${() => {
this.expanded = !this.expanded;
}}>
<button
class="pf-c-button pf-m-plain"
type="button"
aria-expanded="${this.expanded}"
aria-label="Details"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-form__field-group-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
@ -47,5 +67,4 @@ export class FormGroup extends LitElement {
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
</div>`;
}
}

View File

@ -8,17 +8,24 @@ import { t } from "@lingui/macro";
@customElement("ak-form-element-horizontal")
export class HorizontalFormElement extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFFormControl, AKGlobal, css`
.pf-c-form__group {
display: grid;
grid-template-columns: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
}
.pf-c-form__group-label {
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
}
`];
return [
PFBase,
PFForm,
PFFormControl,
AKGlobal,
css`
.pf-c-form__group {
display: grid;
grid-template-columns:
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
}
.pf-c-form__group-label {
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
}
`,
];
}
@property()
@ -43,7 +50,7 @@ export class HorizontalFormElement extends LitElement {
name = "";
updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
input.focus();
});
this.querySelectorAll("*").forEach((input) => {
@ -59,7 +66,7 @@ export class HorizontalFormElement extends LitElement {
return;
}
if (this.writeOnly && !this.writeOnlyActivated) {
const i = (input as HTMLInputElement);
const i = input as HTMLInputElement;
i.setAttribute("hidden", "true");
const handler = () => {
i.removeAttribute("hidden");
@ -76,24 +83,36 @@ export class HorizontalFormElement extends LitElement {
<div class="pf-c-form__group-label">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``}
${this.required
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
: html``}
</label>
</div>
<div class="pf-c-form__group-control">
${this.writeOnly && !this.writeOnlyActivated ?
html`<div class="pf-c-form__horizontal-group">
<input class="pf-c-form-control" type="password" disabled value="**************">
</div>` :
html``}
${this.writeOnly && !this.writeOnlyActivated
? html`<div class="pf-c-form__horizontal-group">
<input
class="pf-c-form-control"
type="password"
disabled
value="**************"
/>
</div>`
: html``}
<slot class="pf-c-form__horizontal-group"></slot>
<div class="pf-c-form__horizontal-group">
${this.writeOnly ? html`<p class="pf-c-form__helper-text" aria-live="polite">${
t`Click to change value`
}</p>` : html``}
${this.invalid ? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">${this.errorMessage}</p>` : html``}
${this.writeOnly
? html`<p class="pf-c-form__helper-text" aria-live="polite">
${t`Click to change value`}
</p>`
: html``}
${this.invalid
? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
${this.errorMessage}
</p>`
: html``}
</div>
</div>
</div>`;
}
}

View File

@ -7,11 +7,10 @@ import "../buttons/SpinnerButton";
@customElement("ak-forms-modal")
export class ModalForm extends ModalButton {
@property({ type: Boolean })
closeAfterSuccessfulSubmit = true;
confirm(): Promise<void> {
confirm(): Promise<void> {
const form = this.querySelector<Form<unknown>>("[slot=form]");
if (!form) {
return Promise.reject(t`No form found`);
@ -29,39 +28,40 @@ export class ModalForm extends ModalButton {
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
}),
);
});
}
renderModalInner(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">
<slot name="header"></slot>
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<slot name="form"></slot>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-primary">
<slot name="submit"></slot>
</ak-spinner-button>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.resetForms();
this.open = false;
}}
class="pf-m-secondary">
${t`Cancel`}
</ak-spinner-button>
</footer>`;
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">
<slot name="header"></slot>
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-light">
<slot name="form"></slot>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-primary"
>
<slot name="submit"></slot> </ak-spinner-button
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.resetForms();
this.open = false;
}}
class="pf-m-secondary"
>
${t`Cancel`}
</ak-spinner-button>
</footer>`;
}
}

View File

@ -3,14 +3,13 @@ import { EVENT_REFRESH } from "../../constants";
import { Form } from "./Form";
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
abstract loadInstance(pk: PKT): Promise<T>;
@property({attribute: false})
@property({ attribute: false })
set instancePk(value: PKT) {
this._instancePk = value;
if (this.isInViewport) {
this.loadInstance(value).then(instance => {
this.loadInstance(value).then((instance) => {
this.instance = instance;
this.requestUpdate();
});
@ -32,7 +31,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this._instancePk) return;
this.loadInstance(this._instancePk).then(instance => {
this.loadInstance(this._instancePk).then((instance) => {
this.instance = instance;
});
});
@ -51,5 +50,4 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
}
return super.render();
}
}

View File

@ -3,14 +3,13 @@ import { Form } from "./Form";
@customElement("ak-proxy-form")
export class ProxyForm extends Form<unknown> {
@property()
type!: string;
@property({attribute: false})
@property({ attribute: false })
args: Record<string, unknown> = {};
@property({attribute: false})
@property({ attribute: false })
typeMap: Record<string, string> = {};
submit(ev: Event): Promise<unknown> | undefined {
@ -43,5 +42,4 @@ export class ProxyForm extends Form<unknown> {
}
return html`${el}`;
}
}

View File

@ -9,7 +9,7 @@ export enum MessageLevel {
error = "error",
warning = "warning",
success = "success",
info = "info"
info = "info",
}
export interface APIMessage {
level: MessageLevel;
@ -27,14 +27,13 @@ const LEVEL_ICON_MAP: { [key: string]: string } = {
@customElement("ak-message")
export class Message extends LitElement {
@property({attribute: false})
@property({ attribute: false })
message?: APIMessage;
@property({type: Number})
@property({ type: Number })
removeAfter = 8000;
@property({attribute: false})
@property({ attribute: false })
onRemove?: (m: APIMessage) => void;
static get styles(): CSSResult[] {
@ -51,27 +50,34 @@ export class Message extends LitElement {
render(): TemplateResult {
return html`<li class="pf-c-alert-group__item">
<div class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level === MessageLevel.error ? "pf-m-danger" : ""}">
<div
class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level ===
MessageLevel.error
? "pf-m-danger"
: ""}"
>
<div class="pf-c-alert__icon">
<i class="${this.message ? LEVEL_ICON_MAP[this.message.level] : ""}"></i>
</div>
<p class="pf-c-alert__title">
${this.message?.message}
</p>
${this.message?.description && html`<div class="pf-c-alert__description">
<p class="pf-c-alert__title">${this.message?.message}</p>
${this.message?.description &&
html`<div class="pf-c-alert__description">
<p>${this.message.description}</p>
</div>`}
<div class="pf-c-alert__action">
<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
if (!this.message) return;
if (!this.onRemove) return;
this.onRemove(this.message);
}}>
<button
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
if (!this.message) return;
if (!this.onRemove) return;
this.onRemove(this.message);
}}
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
</div>
</li>`;
}
}

View File

@ -1,4 +1,12 @@
import { LitElement, html, customElement, TemplateResult, property, CSSResult, css } from "lit-element";
import {
LitElement,
html,
customElement,
TemplateResult,
property,
CSSResult,
css,
} from "lit-element";
import "./Message";
import { APIMessage } from "./Message";
import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css";
@ -17,17 +25,20 @@ export function showMessage(message: APIMessage): void {
@customElement("ak-message-container")
export class MessageContainer extends LitElement {
@property({attribute: false})
@property({ attribute: false })
messages: APIMessage[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFAlertGroup, css`
/* Fix spacing between messages */
ak-message {
display: block;
}
`];
return [
PFBase,
PFAlertGroup,
css`
/* Fix spacing between messages */
ak-message {
display: block;
}
`,
];
}
constructor() {
@ -40,7 +51,7 @@ export class MessageContainer extends LitElement {
// add a new message, but only if the message isn't currently shown.
addMessage(message: APIMessage): void {
const matchingMessages = this.messages.filter(m => m.message == message.message);
const matchingMessages = this.messages.filter((m) => m.message == message.message);
if (matchingMessages.length < 1) {
this.messages.push(message);
}
@ -54,9 +65,10 @@ export class MessageContainer extends LitElement {
.onRemove=${(m: APIMessage) => {
this.messages = this.messages.filter((v) => v !== m);
this.requestUpdate();
}}>
</ak-message>`;
})}
}}
>
</ak-message>`;
})}
</ul>`;
}
}

View File

@ -4,13 +4,12 @@ import { MessageLevel } from "./Message";
import { showMessage } from "./MessageContainer";
export class MessageMiddleware implements Middleware {
post(context: ResponseContext): Promise<Response | void> {
if (context.response.status >= 500) {
showMessage({
level: MessageLevel.error,
message: t`API request failed`,
description: `${context.init.method} ${context.url}: ${context.response.status}`
description: `${context.init.method} ${context.url}: ${context.response.status}`,
});
}
return Promise.resolve(context.response);

View File

@ -32,7 +32,7 @@ export class APIMiddleware implements Middleware {
new CustomEvent(EVENT_API_DRAWER_REFRESH, {
bubbles: true,
composed: true,
})
}),
);
return Promise.resolve(context.response);
}
@ -43,7 +43,6 @@ export const API_DRAWER_MIDDLEWARE = new APIMiddleware();
@customElement("ak-api-drawer")
export class APIDrawer extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal];
}
@ -58,9 +57,7 @@ export class APIDrawer extends LitElement {
renderItem(item: RequestInfo): TemplateResult {
return html`<li class="pf-c-notification-drawer__list-item pf-m-read">
<div class="pf-c-notification-drawer__list-item-header">
<h2 class="pf-c-notification-drawer__list-item-header-title">
${item.method}
</h2>
<h2 class="pf-c-notification-drawer__list-item-header-title">${item.method}</h2>
</div>
<p class="pf-c-notification-drawer__list-item-description">${item.path}</p>
</li>`;
@ -70,17 +67,14 @@ export class APIDrawer extends LitElement {
return html`<div class="pf-c-drawer__body pf-m-no-padding">
<div class="pf-c-notification-drawer">
<div class="pf-c-notification-drawer__header pf-c-content">
<h1>
${t`API Requests`}
</h1>
<h1>${t`API Requests`}</h1>
</div>
<div class="pf-c-notification-drawer__body">
<ul class="pf-c-notification-drawer__list">
${API_DRAWER_MIDDLEWARE.requests.map(n => this.renderItem(n))}
${API_DRAWER_MIDDLEWARE.requests.map((n) => this.renderItem(n))}
</ul>
</div>
</div>
</div>`;
}
}

View File

@ -1,5 +1,13 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { EventsApi, Notification } from "authentik-api";
import { AKResponse } from "../../api/Client";
import { DEFAULT_CONFIG } from "../../api/Config";
@ -14,11 +22,10 @@ import { ActionToLabel } from "../../pages/events/utils";
@customElement("ak-notification-drawer")
export class NotificationDrawer extends LitElement {
@property({attribute: false})
@property({ attribute: false })
notifications?: AKResponse<Notification>;
@property({type: Number})
@property({ type: Number })
unread = 0;
static get styles(): CSSResult[] {
@ -36,34 +43,36 @@ export class NotificationDrawer extends LitElement {
.pf-c-notification-drawer__list-item-description {
white-space: pre-wrap;
}
`
`,
);
}
firstUpdated(): void {
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false,
ordering: "-created",
}).then(r => {
this.notifications = r;
this.unread = r.results.length;
});
new EventsApi(DEFAULT_CONFIG)
.eventsNotificationsList({
seen: false,
ordering: "-created",
})
.then((r) => {
this.notifications = r;
this.unread = r.results.length;
});
}
renderItem(item: Notification): TemplateResult {
let level = "";
switch (item.severity) {
case "notice":
level = "pf-m-info";
break;
case "warning":
level = "pf-m-warning";
break;
case "alert":
level = "pf-m-danger";
break;
default:
break;
case "notice":
level = "pf-m-info";
break;
case "warning":
level = "pf-m-warning";
break;
case "alert":
level = "pf-m-danger";
break;
default:
break;
}
return html`<li class="pf-c-notification-drawer__list-item">
<div class="pf-c-notification-drawer__list-item-header">
@ -75,26 +84,38 @@ export class NotificationDrawer extends LitElement {
</h2>
</div>
<div class="pf-c-notification-drawer__list-item-action">
${item.event && html`
<a class="pf-c-dropdown__toggle pf-m-plain" href="#/events/log/${item.event?.pk}">
${item.event &&
html`
<a
class="pf-c-dropdown__toggle pf-m-plain"
href="#/events/log/${item.event?.pk}"
>
<i class="fas fas fa-share-square"></i>
</a>
`}
<button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => {
new EventsApi(DEFAULT_CONFIG).eventsNotificationsPartialUpdate({
uuid: item.pk || "",
patchedNotificationRequest: {
seen: true,
}
}).then(() => {
this.firstUpdated();
});
}}>
<button
class="pf-c-dropdown__toggle pf-m-plain"
type="button"
@click=${() => {
new EventsApi(DEFAULT_CONFIG)
.eventsNotificationsPartialUpdate({
uuid: item.pk || "",
patchedNotificationRequest: {
seen: true,
},
})
.then(() => {
this.firstUpdated();
});
}}
>
<i class="fas fa-times"></i>
</button>
</div>
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
<small class="pf-c-notification-drawer__list-item-timestamp">${item.created?.toLocaleString()}</small>
<small class="pf-c-notification-drawer__list-item-timestamp"
>${item.created?.toLocaleString()}</small
>
</li>`;
}
@ -106,12 +127,8 @@ export class NotificationDrawer extends LitElement {
<div class="pf-c-notification-drawer">
<div class="pf-c-notification-drawer__header">
<div class="text">
<h1 class="pf-c-notification-drawer__header-title">
${t`Notifications`}
</h1>
<span>
${t`${this.unread} unread`}
</span>
<h1 class="pf-c-notification-drawer__header-title">${t`Notifications`}</h1>
<span> ${t`${this.unread} unread`} </span>
</div>
<div class="pf-c-notification-drawer__header-action">
<div class="pf-c-notification-drawer__header-action-close">
@ -121,12 +138,13 @@ export class NotificationDrawer extends LitElement {
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
bubbles: true,
composed: true,
})
}),
);
}}
class="pf-c-button pf-m-plain"
type="button"
aria-label="Close">
aria-label="Close"
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
@ -134,11 +152,10 @@ export class NotificationDrawer extends LitElement {
</div>
<div class="pf-c-notification-drawer__body">
<ul class="pf-c-notification-drawer__list">
${this.notifications.results.map(n => this.renderItem(n))}
${this.notifications.results.map((n) => this.renderItem(n))}
</ul>
</div>
</div>
</div>`;
}
}

View File

@ -35,30 +35,27 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> {
row(item: ExpiringBaseGrantModel): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.provider?.pk}">
${item.provider?.name}
</a>`,
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
html`${item.expires?.toLocaleString()}`,
html`${item.scope.join(", ")}`,
html`
<ak-forms-delete
html` <ak-forms-delete
.obj=${item}
objectLabel=${t`Authorization Code`}
.usedBy=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({
id: item.pk
id: item.pk,
});
}}
.delete=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({
id: item.pk,
});
}}>
}}
>
<button slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete Authorization Code`}
</button>
</ak-forms-delete>`,
];
}
}

View File

@ -42,49 +42,45 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
}
renderExpanded(item: RefreshTokenModel): TemplateResult {
return html`
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`ID Token`}</h3>
<pre>${item.idToken}</pre>
return html` <td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${t`ID Token`}</h3>
<pre>${item.idToken}</pre>
</div>
</div>
</div>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
</td>
<td></td>
<td></td>
<td></td>`;
}
row(item: RefreshTokenModel): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.provider?.pk}">
${item.provider?.name}
</a>`,
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
html`${item.revoked ? t`Yes` : t`No`}`,
html`${item.expires?.toLocaleString()}`,
html`${item.scope.join(", ")}`,
html`
<ak-forms-delete
html` <ak-forms-delete
.obj=${item}
objectLabel=${t`Refresh Code`}
.usedBy=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
id: item.pk
id: item.pk,
});
}}
.delete=${() => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
id: item.pk,
});
}}>
}}
>
<button slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete Refresh Code`}
</button>
</ak-forms-delete>`,
];
}
}

View File

@ -3,7 +3,7 @@ import { Route } from "./Route";
export class RouteMatch {
route: Route;
arguments: { [key: string]: string; };
arguments: { [key: string]: string };
fullUrl?: string;
constructor(route: Route) {
@ -16,6 +16,8 @@ export class RouteMatch {
}
toString(): string {
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(this.arguments)}>`;
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(
this.arguments,
)}>`;
}
}

View File

@ -6,7 +6,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-router-404")
export class Router404 extends LitElement {
@property()
url = "";
@ -19,9 +18,7 @@ export class Router404 extends LitElement {
<div class="pf-c-empty-state__content">
<i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">${t`Not found`}</h1>
<div class="pf-c-empty-state__body">
${t`The URL "${this.url}" was not found.`}
</div>
<div class="pf-c-empty-state__body">${t`The URL "${this.url}" was not found.`}</div>
<a href="#/" class="pf-c-button pf-m-primary" type="button">${t`Return home`}</a>
</div>
</div>`;

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { Route } from "./Route";
import { ROUTES } from "../../routes";
import { RouteMatch } from "./RouteMatch";
@ -10,26 +18,36 @@ import { ROUTE_SEPARATOR } from "../../constants";
// Poliyfill for hashchange.newURL,
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
window.addEventListener("load", () => {
if (!window.HashChangeEvent) (function () {
let lastURL = document.URL;
window.addEventListener("hashchange", function (event) {
Object.defineProperty(event, "oldURL", { enumerable: true, configurable: true, value: lastURL });
Object.defineProperty(event, "newURL", { enumerable: true, configurable: true, value: document.URL });
lastURL = document.URL;
});
}());
if (!window.HashChangeEvent)
(function () {
let lastURL = document.URL;
window.addEventListener("hashchange", function (event) {
Object.defineProperty(event, "oldURL", {
enumerable: true,
configurable: true,
value: lastURL,
});
Object.defineProperty(event, "newURL", {
enumerable: true,
configurable: true,
value: document.URL,
});
lastURL = document.URL;
});
})();
});
@customElement("ak-router-outlet")
export class RouterOutlet extends LitElement {
@property({attribute: false})
@property({ attribute: false })
current?: RouteMatch;
@property()
defaultUrl?: string;
static get styles(): CSSResult[] {
return [AKGlobal,
return [
AKGlobal,
css`
:host {
height: 100vh;
@ -88,7 +106,7 @@ export class RouterOutlet extends LitElement {
RegExp(""),
html`<div class="pf-c-page__main">
<ak-router-404 url=${activeUrl}></ak-router-404>
</div>`
</div>`,
);
matchedRoute = new RouteMatch(route);
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};

View File

@ -9,7 +9,6 @@ import "./SidebarUser";
@customElement("ak-sidebar")
export class Sidebar extends LitElement {
static get styles(): CSSResult[] {
return [
PFBase,

View File

@ -1,4 +1,12 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -25,7 +33,7 @@ export const DefaultTenant: CurrentTenant = {
@customElement("ak-sidebar-brand")
export class SidebarBrand extends LitElement {
@property({attribute: false})
@property({ attribute: false })
tenant: CurrentTenant = DefaultTenant;
static get styles(): CSSResult[] {
@ -45,7 +53,7 @@ export class SidebarBrand extends LitElement {
}
.pf-c-brand img {
width: 100%;
padding: 0 .5rem;
padding: 0 0.5rem;
height: 42px;
}
button.pf-c-button.sidebar-trigger {
@ -67,28 +75,34 @@ export class SidebarBrand extends LitElement {
firstUpdated(): void {
configureSentry(true);
tenant().then(tenant => this.tenant = tenant);
tenant().then((tenant) => (this.tenant = tenant));
}
render(): TemplateResult {
return html`
${window.innerWidth <= MIN_WIDTH ? html`
<button
class="sidebar-trigger pf-c-button"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
})
);
}}>
<i class="fas fa-bars"></i>
</button>
` : html``}
return html` ${window.innerWidth <= MIN_WIDTH
? html`
<button
class="sidebar-trigger pf-c-button"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bars"></i>
</button>
`
: html``}
<a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img src="${ifDefined(this.tenant.brandingLogo)}" alt="authentik icon" loading="lazy" />
<img
src="${ifDefined(this.tenant.brandingLogo)}"
alt="authentik icon"
loading="lazy"
/>
</div>
</a>`;
}

View File

@ -9,7 +9,6 @@ import { ROUTE_SEPARATOR } from "../../constants";
@customElement("ak-sidebar-item")
export class SidebarItem extends LitElement {
static get styles(): CSSResult[] {
return [
PFBase,
@ -25,7 +24,7 @@ export class SidebarItem extends LitElement {
background-color: var(--ak-accent);
margin: 16px;
}
:host([highlight]) .pf-c-nav__item .pf-c-nav__link {
:host([highlight]) .pf-c-nav__item .pf-c-nav__link {
padding-left: 0.5rem;
}
.pf-c-nav__link.pf-m-current::after,
@ -92,13 +91,13 @@ export class SidebarItem extends LitElement {
get childItems(): SidebarItem[] {
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
children.forEach(child => child.parent = this);
children.forEach((child) => (child.parent = this));
return children;
}
@property({attribute: false})
@property({ attribute: false })
set activeWhen(regexp: string[]) {
regexp.forEach(r => {
regexp.forEach((r) => {
this.activeMatchers.push(new RegExp(r));
});
}
@ -110,7 +109,7 @@ export class SidebarItem extends LitElement {
onHashChange(): void {
const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
this.childItems.forEach(item => {
this.childItems.forEach((item) => {
this.expandParentRecursive(activePath, item);
});
this.isActive = this.matchesPath(activePath);
@ -125,7 +124,7 @@ export class SidebarItem extends LitElement {
return true;
}
}
return this.activeMatchers.some(v => {
return this.activeMatchers.some((v) => {
const match = v.exec(path);
if (match !== null) {
return true;
@ -138,7 +137,7 @@ export class SidebarItem extends LitElement {
item.parent.expanded = true;
this.requestUpdate();
}
item.childItems.forEach(i => this.expandParentRecursive(activePath, i));
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
}
render(): TemplateResult {
@ -153,10 +152,16 @@ export class SidebarItem extends LitElement {
}
}
if (this.childItems.length > 0) {
return html`<li class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}">
<button class="pf-c-nav__link" aria-expanded="true" @click=${() => {
this.expanded = !this.expanded;
}}>
return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
>
<button
class="pf-c-nav__link"
aria-expanded="true"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<slot name="label"></slot>
<span class="pf-c-nav__toggle">
<span class="pf-c-nav__toggle-icon">
@ -172,15 +177,20 @@ export class SidebarItem extends LitElement {
</li>`;
}
return html`<li class="pf-c-nav__item">
${this.path ? html`
<a href="${this.isAbsoluteLink ? "" : "#"}${this.path}" class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}">
<slot name="label"></slot>
</a>
` : html`
<span class="pf-c-nav__link">
<slot name="label"></slot>
</span>
`}
${this.path
? html`
<a
href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
>
<slot name="label"></slot>
</a>
`
: html`
<span class="pf-c-nav__link">
<slot name="label"></slot>
</span>
`}
</li>`;
}
}

View File

@ -9,7 +9,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-sidebar-user")
export class SidebarUser extends LitElement {
static get styles(): CSSResult[] {
return [
PFBase,
@ -34,9 +33,16 @@ export class SidebarUser extends LitElement {
render(): TemplateResult {
return html`
<a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings">
${until(me().then((u) => {
return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`;
}), html``)}
${until(
me().then((u) => {
return html`<img
class="pf-c-avatar"
src="${ifDefined(u.user.avatar)}"
alt=""
/>`;
}),
html``,
)}
</a>
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>

View File

@ -20,7 +20,6 @@ import { EVENT_REFRESH } from "../../constants";
import { ifDefined } from "lit-html/directives/if-defined";
export class TableColumn {
title: string;
orderBy?: string;
@ -45,25 +44,27 @@ export class TableColumn {
private getSortIndicator(table: Table<unknown>): string {
switch (table.order) {
case this.orderBy:
return "fa-long-arrow-alt-down";
case `-${this.orderBy}`:
return "fa-long-arrow-alt-up";
default:
return "fa-arrows-alt-v";
case this.orderBy:
return "fa-long-arrow-alt-down";
case `-${this.orderBy}`:
return "fa-long-arrow-alt-up";
default:
return "fa-arrows-alt-v";
}
}
renderSortable(table: Table<unknown>): TemplateResult {
return html`
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
<div class="pf-c-table__button-content">
<span class="pf-c-table__text">${this.title}</span>
<span class="pf-c-table__sort-indicator">
<i class="fas ${this.getSortIndicator(table)}"></i>
</span>
</div>
</button>`;
return html` <button
class="pf-c-table__button"
@click=${() => this.headerClickHandler(table)}
>
<div class="pf-c-table__button-content">
<span class="pf-c-table__text">${this.title}</span>
<span class="pf-c-table__sort-indicator">
<i class="fas ${this.getSortIndicator(table)}"></i>
</span>
</div>
</button>`;
}
render(table: Table<unknown>): TemplateResult {
@ -72,12 +73,14 @@ export class TableColumn {
scope="col"
class="
${this.orderBy ? "pf-c-table__sort " : " "}
${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""}
">
${table.order === this.orderBy || table.order === `-${this.orderBy}`
? "pf-m-selected "
: ""}
"
>
${this.orderBy ? this.renderSortable(table) : html`${this.title}`}
</th>`;
}
}
export abstract class Table<T> extends LitElement {
@ -99,32 +102,41 @@ export abstract class Table<T> extends LitElement {
return html``;
}
@property({attribute: false})
@property({ attribute: false })
data?: AKResponse<T>;
@property({type: Number})
@property({ type: Number })
page = 1;
@property({type: String})
@property({ type: String })
order?: string;
@property({type: String})
@property({ type: String })
search?: string;
@property({type: Boolean})
@property({ type: Boolean })
checkbox = false;
@property({attribute: false})
@property({ attribute: false })
selectedElements: T[] = [];
@property({type: Boolean})
@property({ type: Boolean })
expandable = false;
@property({attribute: false})
@property({ attribute: false })
expandedRows: boolean[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFTable, PFBullseye, PFButton, PFToolbar, PFDropdown, PFPagination, AKGlobal];
return [
PFBase,
PFTable,
PFBullseye,
PFButton,
PFToolbar,
PFDropdown,
PFPagination,
AKGlobal,
];
}
constructor() {
@ -139,24 +151,23 @@ export abstract class Table<T> extends LitElement {
return;
}
this.isLoading = true;
this.apiEndpoint(this.page).then((r) => {
this.data = r;
this.page = r.pagination.current;
this.expandedRows = [];
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
});
this.apiEndpoint(this.page)
.then((r) => {
this.data = r;
this.page = r.pagination.current;
this.expandedRows = [];
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
});
}
private renderLoading(): TemplateResult {
return html`<tr role="row">
<td role="cell" colspan="25">
<div class="pf-l-bullseye">
<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>
<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>
</div>
</td>
</tr>`;
@ -167,7 +178,11 @@ export abstract class Table<T> extends LitElement {
<tr role="row">
<td role="cell" colspan="8">
<div class="pf-l-bullseye">
${inner ? inner : html`<ak-empty-state header="${t`No objects found.`}"></ak-empty-state>`}
${inner
? inner
: html`<ak-empty-state
header="${t`No objects found.`}"
></ak-empty-state>`}
</div>
</td>
</tr>
@ -182,40 +197,62 @@ export abstract class Table<T> extends LitElement {
return [this.renderEmpty()];
}
return this.data.results.map((item: T, idx: number) => {
if ((this.expandedRows.length - 1) < idx) {
if (this.expandedRows.length - 1 < idx) {
this.expandedRows[idx] = false;
}
return html`<tbody role="rowgroup" class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}">
return html`<tbody
role="rowgroup"
class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}"
>
<tr role="row">
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
<input type="checkbox"
?checked=${this.selectedElements.indexOf(item) >= 0}
@input=${(ev: InputEvent) => {
if ((ev.target as HTMLInputElement).checked) {
// Add item to selected
this.selectedElements.push(item);
} else {
// Get index of item and remove if selected
const index = this.selectedElements.indexOf(item);
if (index <= -1) return;
this.selectedElements.splice(index, 1);
}
this.requestUpdate();
}} />
</td>` : html``}
${this.expandable ? html`<td class="pf-c-table__toggle" role="cell">
<button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => {
this.expandedRows[idx] = !this.expandedRows[idx];
this.requestUpdate();
}}>
<div class="pf-c-table__toggle-icon">&nbsp;<i class="fas fa-angle-down" aria-hidden="true"></i>&nbsp;</div>
</button>
</td>` : html``}
${this.checkbox
? html`<td class="pf-c-table__check" role="cell">
<input
type="checkbox"
?checked=${this.selectedElements.indexOf(item) >= 0}
@input=${(ev: InputEvent) => {
if ((ev.target as HTMLInputElement).checked) {
// Add item to selected
this.selectedElements.push(item);
} else {
// Get index of item and remove if selected
const index = this.selectedElements.indexOf(item);
if (index <= -1) return;
this.selectedElements.splice(index, 1);
}
this.requestUpdate();
}}
/>
</td>`
: html``}
${this.expandable
? html`<td class="pf-c-table__toggle" role="cell">
<button
class="pf-c-button pf-m-plain ${this.expandedRows[idx]
? "pf-m-expanded"
: ""}"
@click=${() => {
this.expandedRows[idx] = !this.expandedRows[idx];
this.requestUpdate();
}}
>
<div class="pf-c-table__toggle-icon">
&nbsp;<i class="fas fa-angle-down" aria-hidden="true"></i
>&nbsp;
</div>
</button>
</td>`
: html``}
${this.row(item).map((col) => {
return html`<td role="cell">${col}</td>`;
})}
</tr>
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
<tr
class="pf-c-table__expandable-row ${this.expandedRows[idx]
? "pf-m-expanded"
: ""}"
role="row"
>
<td></td>
${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
</tr>
@ -230,10 +267,11 @@ export abstract class Table<T> extends LitElement {
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
}),
);
}}
class="pf-c-button pf-m-primary">
class="pf-c-button pf-m-primary"
>
${t`Refresh`}
</button>`;
}
@ -246,16 +284,20 @@ export abstract class Table<T> extends LitElement {
if (!this.searchEnabled()) {
return html``;
}
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
this.search = value;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
);
}}>
</ak-table-search>&nbsp;`;
return html`<ak-table-search
value=${ifDefined(this.search)}
.onSearch=${(value: string) => {
this.search = value;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}}
>
</ak-table-search
>&nbsp;`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -268,20 +310,17 @@ export abstract class Table<T> extends LitElement {
}
renderTable(): TemplateResult {
return html`
${this.checkbox ?
html`<ak-chip-group>
${this.selectedElements.map(el => {
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
})}
</ak-chip-group>`:
html``}
return html` ${this.checkbox
? html`<ak-chip-group>
${this.selectedElements.map((el) => {
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
})}
</ak-chip-group>`
: html``}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
${this.renderSearch()}
<div class="pf-c-toolbar__bulk-select">
${this.renderToolbar()}
</div>
<div class="pf-c-toolbar__bulk-select">${this.renderToolbar()}</div>
${this.renderToolbarAfter()}
<ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination"
@ -292,29 +331,36 @@ export abstract class Table<T> extends LitElement {
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
}),
);
}}>
}}
>
</ak-table-pagination>
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
<thead>
<tr role="row">
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
<input type="checkbox" aria-label=${t`Select all rows`} @input=${(ev: InputEvent) => {
if ((ev.target as HTMLInputElement).checked) {
this.selectedElements = this.data?.results || [];
} else {
this.selectedElements = [];
}
}} />
</td>` : html``}
${this.checkbox
? html`<td class="pf-c-table__check" role="cell">
<input
type="checkbox"
aria-label=${t`Select all rows`}
@input=${(ev: InputEvent) => {
if ((ev.target as HTMLInputElement).checked) {
this.selectedElements = this.data?.results || [];
} else {
this.selectedElements = [];
}
}}
/>
</td>`
: html``}
${this.expandable ? html`<td role="cell"></td>` : html``}
${this.columns().map((col) => col.render(this))}
</tr>
</thead>
${(this.isLoading || !this.data) ? this.renderLoading() : this.renderRows()}
${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
</table>
<div class="pf-c-pagination pf-m-bottom">
<ak-table-pagination
@ -326,9 +372,10 @@ export abstract class Table<T> extends LitElement {
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
})
}),
);
}}>
}}
>
</ak-table-pagination>
</div>`;
}

View File

@ -19,7 +19,16 @@ export abstract class TableModal<T> extends Table<T> {
open = false;
static get styles(): CSSResult[] {
return super.styles.concat(PFModalBox, PFBullseye, PFContent, PFBackdrop, PFPage, PFStack, AKGlobal, MODAL_BUTTON_STYLES);
return super.styles.concat(
PFModalBox,
PFBullseye,
PFContent,
PFBackdrop,
PFPage,
PFStack,
AKGlobal,
MODAL_BUTTON_STYLES,
);
}
constructor() {
@ -33,7 +42,7 @@ export abstract class TableModal<T> extends Table<T> {
}
resetForms(): void {
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
if ("resetForm" in form) {
form?.resetForm();
}
@ -42,7 +51,7 @@ export abstract class TableModal<T> extends Table<T> {
onClick(): void {
this.open = true;
this.querySelectorAll("*").forEach(child => {
this.querySelectorAll("*").forEach((child) => {
if ("requestUpdate" in child) {
(child as LitElement).requestUpdate();
}
@ -56,11 +65,7 @@ export abstract class TableModal<T> extends Table<T> {
renderModal(): TemplateResult {
return html`<div class="pf-c-backdrop">
<div class="pf-l-bullseye">
<div
class="pf-c-modal-box ${this.size}"
role="dialog"
aria-modal="true"
>
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
<button
@click=${() => (this.open = false)}
class="pf-c-button pf-m-plain"

View File

@ -19,7 +19,8 @@ export abstract class TablePage<T> extends Table<T> {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}
description=${ifDefined(this.pageDescription())}>
description=${ifDefined(this.pageDescription())}
>
</ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">${this.renderTable()}</div>

View File

@ -8,12 +8,12 @@ import AKGlobal from "../../authentik.css";
@customElement("ak-table-pagination")
export class TablePagination extends LitElement {
@property({attribute: false})
@property({ attribute: false })
pages?: AKPagination;
@property({attribute: false})
@property({ attribute: false })
// eslint-disable-next-line
pageChangeHandler: (page: number) => void = (page: number) => {}
pageChangeHandler: (page: number) => void = (page: number) => {};
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFPagination, AKGlobal];
@ -33,7 +33,9 @@ export class TablePagination extends LitElement {
<div class="pf-c-pagination__nav-control pf-m-prev">
<button
class="pf-c-button pf-m-plain"
@click=${() => { this.pageChangeHandler(this.pages?.previous || 0); }}
@click=${() => {
this.pageChangeHandler(this.pages?.previous || 0);
}}
?disabled="${(this.pages?.previous || 0) < 1}"
aria-label="${t`Go to previous page`}"
>
@ -43,7 +45,9 @@ export class TablePagination extends LitElement {
<div class="pf-c-pagination__nav-control pf-m-next">
<button
class="pf-c-button pf-m-plain"
@click=${() => { this.pageChangeHandler(this.pages?.next || 0); }}
@click=${() => {
this.pageChangeHandler(this.pages?.next || 0);
}}
?disabled="${(this.pages?.next || 0) <= 0}"
aria-label="${t`Go to next page`}"
>

View File

@ -10,7 +10,6 @@ import { t } from "@lingui/macro";
@customElement("ak-table-search")
export class TableSearch extends LitElement {
@property()
value?: string;
@ -23,25 +22,36 @@ export class TableSearch extends LitElement {
render(): TemplateResult {
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
<div class="pf-c-toolbar__item pf-m-search-filter">
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
<div class="pf-c-toolbar__item pf-m-search-filter">
<form
class="pf-c-input-group"
method="GET"
@submit=${(e: Event) => {
e.preventDefault();
if (!this.onSearch) return;
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
const el =
this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
if (!el) return;
if (el.value === "") return;
this.onSearch(el?.value);
}}>
<input class="pf-c-form-control" name="search" type="search" placeholder=${t`Search...`} value="${ifDefined(this.value)}" @search=${(ev: Event) => {
}}
>
<input
class="pf-c-form-control"
name="search"
type="search"
placeholder=${t`Search...`}
value="${ifDefined(this.value)}"
@search=${(ev: Event) => {
if (!this.onSearch) return;
this.onSearch((ev.target as HTMLInputElement).value);
}}>
<button class="pf-c-button pf-m-control" type="submit">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</form>
</div>
</div>`;
}}
/>
<button class="pf-c-button pf-m-control" type="submit">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</form>
</div>
</div>`;
}
}

View File

@ -10,7 +10,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-user-session-list")
export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
@property()
targetUser!: string;
@ -41,8 +40,7 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
html`${item.userAgent.userAgent?.family}`,
html`${item.userAgent.os?.family}`,
html`${item.expires?.toLocaleString()}`,
html`
<ak-forms-delete
html` <ak-forms-delete
.obj=${item}
objectLabel=${t`Session`}
.usedBy=${() => {
@ -54,12 +52,10 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
uuid: item.uuid || "",
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete Session`}
</button>
}}
>
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Session`}</button>
</ak-forms-delete>`,
];
}
}

View File

@ -36,25 +36,22 @@ export class UserConsentList extends Table<UserConsent> {
return [
html`${item.application.name}`,
html`${item.expires?.toLocaleString()}`,
html`
<ak-forms-delete
html` <ak-forms-delete
.obj=${item}
objectLabel=${t`Consent`}
.usedBy=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({
id: item.pk
id: item.pk,
});
}}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({
id: item.pk,
});
}}>
<button slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete Consent`}
</button>
}}
>
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Consent`}</button>
</ak-forms-delete>`,
];
}
}

View File

@ -1,5 +1,13 @@
import { t } from "@lingui/macro";
import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element";
import {
LitElement,
html,
customElement,
property,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -26,7 +34,15 @@ import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit";
import { StageHost } from "./stages/base";
import { ChallengeChoices, CurrentTenant, ChallengeTypes, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
import {
ChallengeChoices,
CurrentTenant,
ChallengeTypes,
FlowChallengeResponseRequest,
FlowsApi,
RedirectChallenge,
ShellChallenge,
} from "authentik-api";
import { DEFAULT_CONFIG, tenant } from "../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
@ -37,13 +53,12 @@ import { WebsocketClient } from "../common/ws";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
flowSlug: string;
@property({attribute: false})
@property({ attribute: false })
challenge?: ChallengeTypes;
@property({type: Boolean})
@property({ type: Boolean })
loading = false;
@property({ attribute: false })
@ -84,13 +99,15 @@ export class FlowExecutor extends LitElement implements StageHost {
}
setBackground(url: string): void {
this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
});
this.shadowRoot
?.querySelectorAll<HTMLDivElement>(".pf-c-background-image")
.forEach((bg) => {
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
});
}
private postUpdate(): void {
tenant().then(tenant => {
tenant().then((tenant) => {
if (this.challenge?.flowInfo?.title) {
document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
} else {
@ -105,40 +122,48 @@ export class FlowExecutor extends LitElement implements StageHost {
// @ts-ignore
payload.component = this.challenge.component;
this.loading = true;
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest: payload,
}).then((data) => {
this.challenge = data;
this.postUpdate();
}).catch((e: Error | Response) => {
this.errorMessage(e);
}).finally(() => {
this.loading = false;
});
return new FlowsApi(DEFAULT_CONFIG)
.flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest: payload,
})
.then((data) => {
this.challenge = data;
this.postUpdate();
})
.catch((e: Error | Response) => {
this.errorMessage(e);
})
.finally(() => {
this.loading = false;
});
}
firstUpdated(): void {
configureSentry();
tenant().then(tenant => this.tenant = tenant);
tenant().then((tenant) => (this.tenant = tenant));
this.loading = true;
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
}).then((challenge) => {
this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution
if (this.challenge?.flowInfo?.background) {
this.setBackground(this.challenge.flowInfo.background);
}
this.postUpdate();
}).catch((e: Error | Response) => {
// Catch JSON or Update errors
this.errorMessage(e);
}).finally(() => {
this.loading = false;
});
new FlowsApi(DEFAULT_CONFIG)
.flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
})
.then((challenge) => {
this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution
if (this.challenge?.flowInfo?.background) {
this.setBackground(this.challenge.flowInfo.background);
}
this.postUpdate();
})
.catch((e: Error | Response) => {
// Catch JSON or Update errors
this.errorMessage(e);
})
.finally(() => {
this.loading = false;
});
}
async errorMessage(error: Error | Response): Promise<void> {
@ -167,7 +192,7 @@ export class FlowExecutor extends LitElement implements StageHost {
</a>
</li>
</ul>
</footer>`
</footer>`,
} as ChallengeTypes;
}
@ -183,46 +208,92 @@ export class FlowExecutor extends LitElement implements StageHost {
}
switch (this.challenge.type) {
case ChallengeChoices.Redirect:
console.debug("authentik/flows: redirecting to url from server", (this.challenge as RedirectChallenge).to);
console.debug(
"authentik/flows: redirecting to url from server",
(this.challenge as RedirectChallenge).to,
);
window.location.assign((this.challenge as RedirectChallenge).to);
return html`<ak-empty-state
?loading=${true}
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
</ak-empty-state>`;
case ChallengeChoices.Shell:
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
case ChallengeChoices.Native:
switch (this.challenge.component) {
case "ak-stage-access-denied":
return html`<ak-stage-access-denied .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-access-denied>`;
return html`<ak-stage-access-denied
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-access-denied>`;
case "ak-stage-identification":
return html`<ak-stage-identification .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-identification>`;
return html`<ak-stage-identification
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-identification>`;
case "ak-stage-password":
return html`<ak-stage-password .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-password>`;
return html`<ak-stage-password
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-password>`;
case "ak-stage-captcha":
return html`<ak-stage-captcha .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-captcha>`;
return html`<ak-stage-captcha
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-captcha>`;
case "ak-stage-consent":
return html`<ak-stage-consent .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-consent>`;
return html`<ak-stage-consent
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-consent>`;
case "ak-stage-dummy":
return html`<ak-stage-dummy .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-dummy>`;
return html`<ak-stage-dummy
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-dummy>`;
case "ak-stage-email":
return html`<ak-stage-email .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-email>`;
return html`<ak-stage-email
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-email>`;
case "ak-stage-autosubmit":
return html`<ak-stage-autosubmit .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-autosubmit>`;
return html`<ak-stage-autosubmit
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-autosubmit>`;
case "ak-stage-prompt":
return html`<ak-stage-prompt .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-prompt>`;
return html`<ak-stage-prompt
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-prompt>`;
case "ak-stage-authenticator-totp":
return html`<ak-stage-authenticator-totp .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-totp>`;
return html`<ak-stage-authenticator-totp
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-totp>`;
case "ak-stage-authenticator-duo":
return html`<ak-stage-authenticator-duo .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-duo>`;
return html`<ak-stage-authenticator-duo
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-duo>`;
case "ak-stage-authenticator-static":
return html`<ak-stage-authenticator-static .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-static>`;
return html`<ak-stage-authenticator-static
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-static>`;
case "ak-stage-authenticator-webauthn":
return html`<ak-stage-authenticator-webauthn .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-webauthn>`;
return html`<ak-stage-authenticator-webauthn
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-validate":
return html`<ak-stage-authenticator-validate .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-validate>`;
return html`<ak-stage-authenticator-validate
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-validate>`;
case "ak-flow-sources-plex":
return html`<ak-flow-sources-plex .host=${this as StageHost} .challenge=${this.challenge}></ak-flow-sources-plex>`;
return html`<ak-flow-sources-plex
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-plex>`;
default:
break;
}
@ -236,59 +307,85 @@ export class FlowExecutor extends LitElement implements StageHost {
renderChallengeWrapper(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading=${true}
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
}
return html`
${this.loading ? this.renderLoading() : html``}
${this.renderChallenge()}
`;
return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `;
}
render(): TemplateResult {
return html`<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" />
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-login">
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
</div>
</header>
<div class="pf-c-login__main">
${this.renderChallengeWrapper()}
</div>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}">${link.name}</a>
</li>`;
}))}
${this.tenant?.brandingTitle != "authentik" ? html`
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
` : html``}
${this.challenge?.flowInfo?.background?.startsWith("/static") ? html`
<li><a href="https://unsplash.com/@ventiviews">${t`Background image`}</a></li>
` : html``}
</ul>
</footer>
<svg
xmlns="http://www.w3.org/2000/svg"
class="pf-c-background-image__filter"
width="0"
height="0"
>
<filter id="image_overlay">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0"
/>
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncR>
<feFuncG
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncG>
<feFuncB
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
</div>`;
<div class="pf-c-login">
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img
src="${ifDefined(this.tenant?.brandingLogo)}"
alt="authentik icon"
/>
</div>
</header>
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(
this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}">${link.name}</a>
</li>`;
}),
)}
${this.tenant?.brandingTitle != "authentik"
? html`
<li>
<a href="https://goauthentik.io"
>${t`Powered by authentik`}</a
>
</li>
`
: html``}
${this.challenge?.flowInfo?.background?.startsWith("/static")
? html`
<li>
<a href="https://unsplash.com/@ventiviews"
>${t`Background image`}</a
>
</li>
`
: html``}
</ul>
</footer>
</div>
</div>`;
}
}

View File

@ -1,11 +1,18 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-form-static")
export class FormStatic extends LitElement {
@property()
userAvatar?: string;
@ -13,39 +20,45 @@ export class FormStatic extends LitElement {
user = "";
static get styles(): CSSResult[] {
return [PFAvatar, css`
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
}
`];
return [
PFAvatar,
css`
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
}
`,
];
}
render(): TemplateResult {
return html`
<div class="form-control-static">
<div class="avatar">
<img class="pf-c-avatar" src="${ifDefined(this.userAvatar)}" alt="${t`User's avatar`}">
<img
class="pf-c-avatar"
src="${ifDefined(this.userAvatar)}"
alt="${t`User's avatar`}"
/>
${this.user}
</div>
<slot name="link"></slot>
</div>
`;
}
}

View File

@ -13,23 +13,20 @@ import { t } from "@lingui/macro";
import "../../elements/EmptyState";
@customElement("ak-stage-access-denied")
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
export class FlowAccessDenied extends BaseStage<
AccessDeniedChallenge,
FlowChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form method="POST" class="pf-c-form">
@ -39,15 +36,13 @@ export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChall
${t`Request has been denied.`}
</p>
${this.challenge?.errorMessage &&
html`<hr>
html`<hr />
<p>${this.challenge.errorMessage}</p>`}
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -23,40 +23,54 @@ export const DEFAULT_HEADERS = {
};
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
const top = (screen.height - h) / 4,
left = (screen.width - w) / 2;
const popup = window.open(
url,
title,
`scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`,
);
return popup;
}
export class PlexAPIClient {
token: string;
constructor(token: string) {
this.token = token;
}
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
static async getPin(
clientIdentifier: string,
): Promise<{ authUrl: string; pin: PlexPinResponse }> {
const headers = {
...DEFAULT_HEADERS,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
method: "POST",
headers: headers
headers: headers,
});
const pin: PlexPinResponse = await pinResponse.json();
return {
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
pin: pin
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(
clientIdentifier,
)}&code=${pin.code}`,
pin: pin,
};
}
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
const headers = {
...DEFAULT_HEADERS,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
headers: headers
headers: headers,
});
const pin: PlexPinResponse = await pinResponse.json();
return pin.authToken || "";
@ -65,7 +79,7 @@ export class PlexAPIClient {
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
const executePoll = async (
resolve: (authToken: string) => void,
reject: (e: Error) => void
reject: (e: Error) => void,
) => {
try {
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
@ -84,13 +98,15 @@ export class PlexAPIClient {
}
async getServers(): Promise<PlexResource[]> {
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
headers: DEFAULT_HEADERS
});
const resourcesResponse = await fetch(
`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`,
{
headers: DEFAULT_HEADERS,
},
);
const resources: PlexResource[] = await resourcesResponse.json();
return resources.filter(r => {
return resources.filter((r) => {
return r.provides.toLowerCase().includes("server") && r.owned;
});
}
}

View File

@ -1,5 +1,8 @@
import { t } from "@lingui/macro";
import { PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest } from "authentik-api";
import {
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest,
} from "authentik-api";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -16,10 +19,11 @@ import { SourcesApi } from "authentik-api";
import { showMessage } from "../../../elements/messages/MessageContainer";
import { MessageLevel } from "../../../elements/messages/Message";
@customElement("ak-flow-sources-plex")
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
export class PlexLoginInit extends BaseStage<
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
@ -27,46 +31,43 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
async firstUpdated(): Promise<void> {
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => {
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then((token) => {
authWindow?.close();
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
plexTokenRedeemRequest: {
plexToken: token,
},
slug: this.challenge?.slug || "",
}).then((r) => {
window.location.assign(r.to);
}).catch((r: Response) => {
r.json().then((body: {detail: string}) => {
showMessage({
level: MessageLevel.error,
message: body.detail
new SourcesApi(DEFAULT_CONFIG)
.sourcesPlexRedeemTokenCreate({
plexTokenRedeemRequest: {
plexToken: token,
},
slug: this.challenge?.slug || "",
})
.then((r) => {
window.location.assign(r.to);
})
.catch((r: Response) => {
r.json().then((body: { detail: string }) => {
showMessage({
level: MessageLevel.error,
message: body.detail,
});
setTimeout(() => {
window.location.assign("/");
}, 5000);
});
setTimeout(() => {
window.location.assign("/");
}, 5000);
});
});
});
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${t`Authenticating with Plex...`}
</h1>
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Plex...`}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-empty-state
?loading="${true}">
</ak-empty-state>
<ak-empty-state ?loading="${true}"> </ak-empty-state>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,13 +11,19 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, StagesApi } from "authentik-api";
import {
AuthenticatorDuoChallenge,
AuthenticatorDuoChallengeResponseRequest,
StagesApi,
} from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-duo")
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
export class AuthenticatorDuoStage extends BaseStage<
AuthenticatorDuoChallenge,
AuthenticatorDuoChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -31,35 +37,41 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
}
checkEnrollStatus(): Promise<void> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stageUuid || "",
}).then(() => {
this.host?.submit({});
}).catch(() => {
console.debug("authentik/flows/duo: Waiting for auth status");
});
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stageUuid || "",
})
.then(() => {
this.host?.submit({});
})
.catch(() => {
console.debug("authentik/flows/duo: Waiting for auth status");
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<img src=${this.challenge.activationBarcode} />
@ -69,18 +81,20 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
<div class="pf-c-form__group pf-m-action">
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.checkEnrollStatus();
}}>
<button
type="button"
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.checkEnrollStatus();
}}
>
${t`Check status`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,58 +11,75 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest } from "authentik-api";
import {
AuthenticatorStaticChallenge,
AuthenticatorStaticChallengeResponseRequest,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
export const STATIC_TOKEN_STYLE = css`
/* Static OTP Tokens */
.ak-otp-tokens {
list-style: circle;
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
margin-left: var(--pf-global--spacer--xs);
}
.ak-otp-tokens li {
font-size: var(--pf-global--FontSize--2xl);
font-family: monospace;
}
/* Static OTP Tokens */
.ak-otp-tokens {
list-style: circle;
columns: 2;
-webkit-columns: 2;
-moz-columns: 2;
margin-left: var(--pf-global--spacer--xs);
}
.ak-otp-tokens li {
font-size: var(--pf-global--FontSize--2xl);
font-family: monospace;
}
`;
@customElement("ak-stage-authenticator-static")
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
export class AuthenticatorStaticStage extends BaseStage<
AuthenticatorStaticChallenge,
AuthenticatorStaticChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal, STATIC_TOKEN_STYLE];
return [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
STATIC_TOKEN_STYLE,
];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<ak-form-element
label="${t`Tokens`}"
?required="${true}"
class="pf-c-form__group">
class="pf-c-form__group"
>
<ul class="ak-otp-tokens">
${this.challenge.codes.map((token) => {
return html`<li>${token}</li>`;
@ -78,9 +95,7 @@ export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChall
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -14,53 +14,66 @@ import { showMessage } from "../../../elements/messages/MessageContainer";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { MessageLevel } from "../../../elements/messages/Message";
import { AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest } from "authentik-api";
import {
AuthenticatorTOTPChallenge,
AuthenticatorTOTPChallengeResponseRequest,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-totp")
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
export class AuthenticatorTOTPStage extends BaseStage<
AuthenticatorTOTPChallenge,
AuthenticatorTOTPChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
<ak-form-element>
<!-- @ts-ignore -->
<qr-code data="${this.challenge.configUrl}"></qr-code>
<button type="button" class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress" @click=${(e: Event) => {
e.preventDefault();
if (!this.challenge?.configUrl) return;
navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
showMessage({
level: MessageLevel.success,
message: t`Successfully copied TOTP Config.`
});
});
}}>
<button
type="button"
class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress"
@click=${(e: Event) => {
e.preventDefault();
if (!this.challenge?.configUrl) return;
navigator.clipboard
.writeText(this.challenge?.configUrl)
.then(() => {
showMessage({
level: MessageLevel.success,
message: t`Successfully copied TOTP Config.`,
});
});
}}
>
<span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
${t`Copy`}
</button>
@ -69,9 +82,11 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}>
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input type="text"
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
@ -79,7 +94,8 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
required>
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
@ -90,9 +106,7 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -12,7 +12,11 @@ import "./AuthenticatorValidateStageWebAuthn";
import "./AuthenticatorValidateStageCode";
import "./AuthenticatorValidateStageDuo";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
export enum DeviceClasses {
STATIC = "static",
@ -22,9 +26,14 @@ export enum DeviceClasses {
}
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
@property({attribute: false})
export class AuthenticatorValidateStage
extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
>
implements StageHost
{
@property({ attribute: false })
selectedDeviceChallenge?: DeviceChallenge;
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
@ -65,7 +74,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
return html`<i class="fas fa-mobile-alt"></i>
<div class="right">
<p>${t`Duo push-notifications`}</p>
<small>${t`Receive a push notification on your phone to prove your identity.`}</small>
<small
>${t`Receive a push notification on your phone to prove your identity.`}</small
>
</div>`;
case DeviceClasses.WEBAUTHN:
return html`<i class="fas fa-mobile-alt"></i>
@ -78,7 +89,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
// and we have a pre-filled value from the password manager,
// directly set the the TOTP device Challenge as active.
if (PasswordManagerPrefill.totp) {
console.debug("authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge");
console.debug(
"authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
);
this.selectedDeviceChallenge = deviceChallenge;
// Delay the update as a re-render isn't triggered from here
setTimeout(() => {
@ -103,13 +116,16 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
}
renderDevicePicker(): TemplateResult {
return html`
<ul>
return html` <ul>
${this.challenge?.deviceChallenges.map((challenges) => {
return html`<li>
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
this.selectedDeviceChallenge = challenges;
}}>
<button
class="pf-c-button authenticator-button"
type="button"
@click=${() => {
this.selectedDeviceChallenge = challenges;
}}
>
${this.renderDevicePickerSingle(challenges)}
</button>
</li>`;
@ -122,60 +138,56 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
return html``;
}
switch (this.selectedDeviceChallenge?.deviceClass) {
case DeviceClasses.STATIC:
case DeviceClasses.TOTP:
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-webauthn>`;
case DeviceClasses.DUO:
return html`<ak-stage-authenticator-validate-duo
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-duo>`;
case DeviceClasses.STATIC:
case DeviceClasses.TOTP:
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-webauthn>`;
case DeviceClasses.DUO:
return html`<ak-stage-authenticator-validate-duo
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-duo>`;
}
return html``;
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
// User only has a single device class, so we don't show a picker
if (this.challenge?.deviceChallenges.length === 1) {
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
${t`Select an identification method.`}
</p>`}
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
${this.selectedDeviceChallenge
? ""
: html`<p class="pf-c-login__main-header-desc">
${t`Select an identification method.`}
</p>`}
</header>
${this.selectedDeviceChallenge ?
this.renderDeviceChallenge() :
html`<div class="pf-c-login__main-body">
${this.renderDevicePicker()}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`}`;
${this.selectedDeviceChallenge
? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body">${this.renderDevicePicker()}</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`}`;
}
}

View File

@ -13,12 +13,18 @@ import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import "../../FormStatic";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
export class AuthenticatorValidateStageWebCode extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@ -31,60 +37,72 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
</div>
</ak-form-static>
<ak-form-element
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}>
<!-- @ts-ignore -->
<input type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${t`Please enter your TOTP Code`}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required>
</ak-form-element>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<ak-form-element
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${t`Please enter your TOTP Code`}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}>
${t`Return to device picker`}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</li>`:
html``}
</ul>
</footer>`;
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -12,12 +12,18 @@ import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-validate-duo")
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
export class AuthenticatorValidateStageWebDuo extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@ -30,49 +36,58 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
firstUpdated(): void {
this.host?.submit({
"duo": this.deviceChallenge?.deviceUid
duo: this.deviceChallenge?.deviceUid,
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
</div>
</ak-form-static>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}>
${t`Return to device picker`}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</li>`:
html``}
</ul>
</footer>`;
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -8,15 +8,24 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
import {
transformAssertionForServer,
transformCredentialRequestOptions,
} from "../authenticator_webauthn/utils";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
@customElement("ak-stage-authenticator-validate-webauthn")
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
@property({attribute: false})
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
@ -25,7 +34,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
@property()
authenticateMessage = "";
@property({type: Boolean})
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
@ -35,8 +44,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
async authenticate(): Promise<void> {
// convert certain members of the PublicKeyCredentialRequestOptions into
// byte arrays as expected by the spec.
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>(
this.deviceChallenge?.challenge
);
const transformedCredentialRequestOptions =
transformCredentialRequestOptions(credentialRequestOptions);
// request the authenticator to create an assertion signature using the
// credential private key
@ -54,12 +66,14 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = transformAssertionForServer(<PublicKeyCredential>assertion);
const transformedAssertionForServer = transformAssertionForServer(
<PublicKeyCredential>assertion,
);
// post the assertion to the server for verification.
try {
await this.host?.submit({
webauthn: transformedAssertionForServer
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(t`Error when validating assertion on server: ${err}`);
@ -75,48 +89,56 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
return;
}
this.authenticateRunning = true;
this.authenticate().catch((e) => {
console.error(e);
this.authenticateMessage = e.toString();
}).finally(() => {
this.authenticateRunning = false;
});
this.authenticate()
.catch((e) => {
console.error(e);
this.authenticateMessage = e.toString();
})
.finally(() => {
this.authenticateRunning = false;
});
}
render(): TemplateResult {
return html`<div class="pf-c-login__main-body">
${this.authenticateRunning ?
html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`:
html`
<div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">${this.authenticateMessage}</p>
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.authenticateWrapper();
}}>
${t`Retry authentication`}
</button>
</div>`}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}>
${t`Return to device picker`}
</button>
</li>`:
html``}
</ul>
</footer>`;
${this.authenticateRunning
? html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`
: html` <div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">${this.authenticateMessage}</p>
<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.authenticateWrapper();
}}
>
${t`Retry authentication`}
</button>
</div>`}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -9,17 +9,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { BaseStage } from "../base";
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
import {
Assertion,
transformCredentialCreateOptions,
transformNewAssertionForServer,
} from "./utils";
import {
AuthenticatorWebAuthnChallenge,
AuthenticatorWebAuthnChallengeResponseRequest,
} from "authentik-api";
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
response: Assertion;
}
@customElement("ak-stage-authenticator-webauthn")
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
@property({type: Boolean})
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
AuthenticatorWebAuthnChallenge,
AuthenticatorWebAuthnChallengeResponseRequest
> {
@property({ type: Boolean })
registerRunning = false;
@property()
@ -35,13 +44,15 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
}
// convert certain members of the PublicKeyCredentialCreateOptions into
// byte arrays as expected by the spec.
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration as PublicKeyCredentialCreationOptions);
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
this.challenge?.registration as PublicKeyCredentialCreationOptions,
);
// request the authenticator(s) to create a new credential keypair.
let credential;
try {
credential = <PublicKeyCredential> await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
credential = <PublicKeyCredential>await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions,
});
if (!credential) {
throw new Error("Credential is empty");
@ -58,7 +69,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
// and storing the public key
try {
await this.host?.submit({
response: newAssertionForServer
response: newAssertionForServer,
});
} catch (err) {
throw new Error(t`Server validation of credential failed: ${err}`);
@ -70,12 +81,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
return;
}
this.registerRunning = true;
this.register().catch((e) => {
console.error(e);
this.registerMessage = e.toString();
}).finally(() => {
this.registerRunning = false;
});
this.register()
.catch((e) => {
console.error(e);
this.registerMessage = e.toString();
})
.finally(() => {
this.registerRunning = false;
});
}
firstUpdated(): void {
@ -89,26 +102,32 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
</h1>
</header>
<div class="pf-c-login__main-body">
${this.registerRunning ?
html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`:
html`
<div class="pf-c-form__group pf-m-action">
${this.challenge?.responseErrors ?
html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
html``}
<p class="pf-m-block">${this.registerMessage}</p>
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.registerWrapper();
}}>
${t`Register device`}
</button>
</div>`}
${
this.registerRunning
? html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`
: html` <div class="pf-c-form__group pf-m-action">
${this.challenge?.responseErrors
? html`<p class="pf-m-block">
${this.challenge.responseErrors["response"][0].string}
</p>`
: html``}
<p class="pf-m-block">${this.registerMessage}</p>
<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.registerWrapper();
}}
>
${t`Register device`}
</button>
</div>`
}
</div>
</div>
<footer class="pf-c-login__main-footer">
@ -116,5 +135,4 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
</ul>
</footer>`;
}
}

View File

@ -2,30 +2,28 @@ import * as base64js from "base64-js";
import { hexEncode } from "../../../utils";
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 {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_");
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
export function transformCredentialCreateOptions(credentialCreateOptions: PublicKeyCredentialCreationOptions): PublicKeyCredentialCreationOptions {
export function transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
const challenge = u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptions,
{ challenge, user });
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
challenge,
user,
});
return transformedCredentialCreateOptions;
}
@ -46,11 +44,10 @@ export interface Assertion {
*/
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
const attObj = new Uint8Array(
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject);
const clientDataJSON = new Uint8Array(
newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(
newAssertion.rawId);
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
@ -59,26 +56,32 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
type: newAssertion.type,
attObj: b64enc(attObj),
clientData: b64enc(clientDataJSON),
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
};
}
function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
export function transformCredentialRequestOptions(credentialRequestOptions: PublicKeyCredentialRequestOptions): PublicKeyCredentialRequestOptions {
export function transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {
const challenge = u8arr(credentialRequestOptions.challenge.toString());
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(credentialDescriptor => {
const id = u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
});
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
(credentialDescriptor) => {
const id = u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
},
);
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptions,
{ challenge, allowCredentials });
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
}
@ -97,8 +100,8 @@ export interface AuthAssertion {
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion{
const response = <AuthenticatorAssertionResponse> newAssertion.response;
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
const response = <AuthenticatorAssertionResponse>newAssertion.response;
const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
@ -112,6 +115,6 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
authData: b64RawEnc(authData),
clientData: b64RawEnc(clientDataJSON),
signature: hexEncode(sig),
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
};
}

View File

@ -12,36 +12,37 @@ import "../../../elements/EmptyState";
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-autosubmit")
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
export class AutosubmitStage extends BaseStage<
AutosubmitChallenge,
AutoSubmitChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
updated(): void {
this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit();});
this.shadowRoot?.querySelectorAll("form").forEach((form) => {
form.submit();
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
${Object.entries(this.challenge.attrs).map(([key, value]) => {
return html`<input
type="hidden"
name="${key as string}"
value="${value as string}"
/>`;
})}
<ak-empty-state
?loading="${true}">
</ak-empty-state>
<ak-empty-state ?loading="${true}"> </ak-empty-state>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -50,9 +51,7 @@ export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitCh
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -7,7 +7,6 @@ export interface StageHost {
}
export class BaseStage<Tin, Tout> extends LitElement {
host!: StageHost;
@property({ attribute: false })
@ -19,7 +18,7 @@ export class BaseStage<Tin, Tout> extends LitElement {
[key: string]: unknown;
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
form.forEach((value, key) => object[key] = value);
form.forEach((value, key) => (object[key] = value));
this.host?.submit(object as unknown as Tout);
}
@ -28,18 +27,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
${errors.map((err) => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
<h4 class="pf-c-alert__title">${err.string}</h4>
</div>`;
})}
</div>`;
}
}

View File

@ -17,7 +17,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -38,7 +37,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
sitekey: this.challenge.siteKey,
callback: (token) => {
this.host?.submit({
"token": token,
token: token,
});
},
size: "invisible",
@ -51,24 +50,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="ak-loading">
@ -77,9 +74,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -15,48 +15,63 @@ import "../../FormStatic";
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-consent")
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
return [
PFBase,
PFLogin,
PFList,
PFForm,
PFSpacing,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<p id="header-text" class="pf-u-mb-xl">
${this.challenge.headerText}
</p>
${this.challenge.permissions.length > 0 ? html`
<p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
<ul class="pf-c-list" id="permmissions">
${this.challenge.permissions.map((permission) => {
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
})}
</ul>
` : html``}
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
${this.challenge.permissions.length > 0
? html`
<p class="pf-u-mb-sm">
${t`Application requires following permissions:`}
</p>
<ul class="pf-c-list" id="permmissions">
${this.challenge.permissions.map((permission) => {
return html`<li data-permission-code="${permission.id}">
${permission.name}
</li>`;
})}
</ul>
`
: html``}
</div>
<div class="pf-c-form__group pf-m-action">
@ -67,9 +82,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -14,25 +14,24 @@ import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-dummy")
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -41,9 +40,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -13,29 +13,26 @@ import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-email")
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<div class="pf-c-form__group">
<p>
${t`Check your Emails for a password reset link.`}
</p>
<p>${t`Check your Emails for a password reset link.`}</p>
</div>
<div class="pf-c-form__group pf-m-action">
@ -46,9 +43,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,7 +11,12 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import AKGlobal from "../../../authentik.css";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api";
import {
IdentificationChallenge,
IdentificationChallengeResponseRequest,
LoginSource,
UserFieldsEnum,
} from "authentik-api";
export const PasswordManagerPrefill: {
password: string | undefined;
@ -21,13 +26,27 @@ export const PasswordManagerPrefill: {
totp: undefined,
};
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", { style: "short", type: "disjunction" });
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", {
style: "short",
type: "disjunction",
});
@customElement("ak-stage-identification")
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
export class IdentificationStage extends BaseStage<
IdentificationChallenge,
IdentificationChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
return [
PFBase,
PFAlert,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
].concat(
css`
/* login page's icons */
.pf-c-login__main-footer-links-item button {
@ -41,7 +60,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`
`,
);
}
@ -56,12 +75,14 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
username.setAttribute("autocomplete", "username");
username.onkeyup = (ev: Event) => {
const el = ev.target as HTMLInputElement;
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(username);
const password = document.createElement("input");
@ -79,11 +100,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(password);
const totp = document.createElement("input");
@ -101,11 +124,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(totp);
}
@ -113,16 +138,19 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
renderSource(source: LoginSource): TemplateResult {
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
if (source.iconUrl) {
icon = html`<img src="${source.iconUrl}" alt="${source.name}">`;
icon = html`<img src="${source.iconUrl}" alt="${source.name}" />`;
}
return html`<li class="pf-c-login__main-footer-links-item">
<button type="button" @click=${() => {
<button
type="button"
@click=${() => {
if (!this.host) return;
this.host.challenge = source.challenge;
}}>
${icon}
</button>
</li>`;
}}
>
${icon}
</button>
</li>`;
}
renderFooter(): TemplateResult {
@ -130,24 +158,26 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
return html``;
}
return html`<div class="pf-c-login__main-footer-band">
${this.challenge.enrollUrl ? html`
<p class="pf-c-login__main-footer-band-item">
${t`Need an account?`}
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
</p>` : html``}
${this.challenge.recoveryUrl ? html`
<p class="pf-c-login__main-footer-band-item">
<a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
</p>` : html``}
</div>`;
${this.challenge.enrollUrl
? html` <p class="pf-c-login__main-footer-band-item">
${t`Need an account?`}
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
</p>`
: html``}
${this.challenge.recoveryUrl
? html` <p class="pf-c-login__main-footer-band-item">
<a id="recovery" href="${this.challenge.recoveryUrl}"
>${t`Forgot username or password?`}</a
>
</p>`
: html``}
</div>`;
}
renderInput(): TemplateResult {
let type = "text";
if (!this.challenge?.userFields) {
return html`<p>
${t`Select one of the sources below to login.`}
</p>`;
return html`<p>${t`Select one of the sources below to login.`}</p>`;
}
const fields = (this.challenge?.userFields || []).sort();
// Check if the field should be *only* email to set the input type
@ -159,40 +189,48 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
[UserFieldsEnum.Email]: t`Email`,
[UserFieldsEnum.Upn]: t`UPN`,
};
const label = OR_LIST_FORMATTERS.format(fields.map(f => uiFields[f]));
const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
return html`<ak-form-element
label=${label}
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["uid_field"]}>
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
>
<!-- @ts-ignore -->
<input type=${type}
<input
type=${type}
name="uidField"
placeholder=${label}
autofocus=""
autocomplete="username"
class="pf-c-form-control"
required>
required
/>
</ak-form-element>
${this.challenge.passwordFields ? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}>
<input type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element>
`: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
html``}
${this.challenge.passwordFields
? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}
>
<input
type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}
/>
</ak-form-element>
`
: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {})
? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
: html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction}
@ -202,23 +240,21 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
${this.challenge.applicationPre ?
html`<p>
${t`Login to continue to ${this.challenge.applicationPre}.`}
</p>`:
html``}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.challenge.applicationPre
? html`<p>${t`Login to continue to ${this.challenge.applicationPre}.`}</p>`
: html``}
${this.renderInput()}
</form>
</div>
@ -231,5 +267,4 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
${this.renderFooter()}
</footer>`;
}
}

View File

@ -17,52 +17,62 @@ import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-password")
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pendingUser}">
<input
name="username"
autocomplete="username"
type="hidden"
value="${this.challenge.pendingUser}"
/>
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["password"]}>
<input type="password"
.errors=${(this.challenge?.responseErrors || {})["password"]}
>
<input
type="password"
name="password"
placeholder="${t`Please enter your password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
value=${PasswordManagerPrefill.password || ""}
/>
</ak-form-element>
${this.challenge.recoveryUrl ?
html`<a href="${this.challenge.recoveryUrl}">
${t`Forgot password?`}</a>` : ""}
${this.challenge.recoveryUrl
? html`<a href="${this.challenge.recoveryUrl}"> ${t`Forgot password?`}</a>`
: ""}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
@ -72,9 +82,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -15,10 +15,8 @@ import "../../../elements/EmptyState";
import "../../../elements/Divider";
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
@customElement("ak-stage-prompt")
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -104,34 +102,41 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.challenge.fields.map((prompt) => {
// Special types that aren't rendered in a wrapper
if (prompt.type === "static" || prompt.type === "hidden" || prompt.type === "separator") {
if (
prompt.type === "static" ||
prompt.type === "hidden" ||
prompt.type === "separator"
) {
return unsafeHTML(this.renderPromptInner(prompt));
}
return html`<ak-form-element
label="${prompt.label}"
?required="${prompt.required}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
>
${unsafeHTML(this.renderPromptInner(prompt))}
</ak-form-element>`;
})}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
html``}
${"non_field_errors" in (this.challenge?.responseErrors || {})
? this.renderNonFieldErrors(
this.challenge?.responseErrors?.non_field_errors || [],
)
: html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -140,9 +145,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -1,5 +1,13 @@
import "../elements/messages/MessageContainer";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { me } from "../api/Users";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
import "./locale";
@ -21,24 +29,31 @@ import { AdminApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../api/Config";
import { WebsocketClient } from "../common/ws";
@customElement("ak-interface-admin")
export class AdminInterface extends LitElement {
@property({type: Boolean})
@property({ type: Boolean })
sidebarOpen = true;
@property({type: Boolean})
@property({ type: Boolean })
notificationOpen = false;
ws: WebsocketClient;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFButton, PFDrawer, AKGlobal, css`
.pf-c-page__main, .pf-c-drawer__content, .pf-c-page__drawer {
z-index: auto !important;
}
`];
return [
PFBase,
PFPage,
PFButton,
PFDrawer,
AKGlobal,
css`
.pf-c-page__main,
.pf-c-drawer__content,
.pf-c-page__drawer {
z-index: auto !important;
}
`,
];
}
constructor() {
@ -58,56 +73,78 @@ export class AdminInterface extends LitElement {
}
render(): TemplateResult {
return html`
<div class="pf-c-page">
<ak-sidebar class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
${this.renderSidebarItems()}
</ak-sidebar>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${this.notificationOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet role="main" class="pf-c-page__main" tabindex="-1" id="main-content" defaultUrl="/library">
</ak-router-outlet>
</main>
</div>
return html` <div class="pf-c-page">
<ak-sidebar
class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}"
>
${this.renderSidebarItems()}
</ak-sidebar>
<div class="pf-c-page__drawer">
<div
class="pf-c-drawer ${this.notificationOpen
? "pf-m-expanded"
: "pf-m-collapsed"}"
>
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/library"
>
</ak-router-outlet>
</main>
</div>
<ak-notification-drawer class="pf-c-drawer__panel pf-m-width-33">
</ak-notification-drawer>
</div>
<ak-notification-drawer class="pf-c-drawer__panel pf-m-width-33">
</ak-notification-drawer>
</div>
</div>
</div>`;
</div>
</div>`;
}
renderSidebarItems(): TemplateResult {
const superUserCondition = () => {
return me().then(u => u.user.isSuperuser || false);
return me().then((u) => u.user.isSuperuser || false);
};
return html`
${until(new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then(version => {
if (version.versionCurrent !== VERSION) {
return html`<ak-sidebar-item ?highlight=${true}>
<span slot="label">${t`A newer version of the frontend is available.`}</span>
</ak-sidebar-item>`;
}
return html``;
}))}
${until(me().then((u) => {
if (u.original) {
return html`<ak-sidebar-item ?highlight=${true} ?isAbsoluteLink=${true} path=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}>
<span slot="label">${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span>
</ak-sidebar-item>`;
}
return html``;
}))}
${until(
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
if (version.versionCurrent !== VERSION) {
return html`<ak-sidebar-item ?highlight=${true}>
<span slot="label"
>${t`A newer version of the frontend is available.`}</span
>
</ak-sidebar-item>`;
}
return html``;
}),
)}
${until(
me().then((u) => {
if (u.original) {
return html`<ak-sidebar-item
?highlight=${true}
?isAbsoluteLink=${true}
path=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}
>
<span slot="label"
>${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span
>
</ak-sidebar-item>`;
}
return html``;
}),
)}
<ak-sidebar-item path="/library">
<span slot="label">${t`Library`}</span>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Monitor`}</span>
<ak-sidebar-item path="/administration/overview">
<span slot="label">${t`Overview`}</span>
@ -116,24 +153,31 @@ export class AdminInterface extends LitElement {
<span slot="label">${t`System Tasks`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Resources`}</span>
<ak-sidebar-item path="/core/applications" .activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]}>
<ak-sidebar-item
path="/core/applications"
.activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Applications`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/core/sources" .activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}>
<ak-sidebar-item
path="/core/sources"
.activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Sources`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/core/providers" .activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}>
<ak-sidebar-item
path="/core/providers"
.activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}
>
<span slot="label">${t`Providers`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Outposts`}</span>
<ak-sidebar-item path="/outpost/outposts">
<span slot="label">${t`Outposts`}</span>
@ -142,10 +186,12 @@ export class AdminInterface extends LitElement {
<span slot="label">${t`Service Connections`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Events`}</span>
<ak-sidebar-item path="/events/log" .activeWhen=${[`^/events/log/(?<id>${UUID_REGEX})$`]}>
<ak-sidebar-item
path="/events/log"
.activeWhen=${[`^/events/log/(?<id>${UUID_REGEX})$`]}
>
<span slot="label">${t`Logs`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/events/rules">
@ -155,8 +201,7 @@ export class AdminInterface extends LitElement {
<span slot="label">${t`Notification Transports`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Customisation`}</span>
<ak-sidebar-item path="/policy/policies">
<span slot="label">${t`Policies`}</span>
@ -171,10 +216,12 @@ export class AdminInterface extends LitElement {
<span slot="label">${t`Property Mappings`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Flows`}</span>
<ak-sidebar-item path="/flow/flows" .activeWhen=${[`^/flow/flows/(?<slug>${SLUG_REGEX})$`]}>
<ak-sidebar-item
path="/flow/flows"
.activeWhen=${[`^/flow/flows/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Flows`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/flow/stages">
@ -187,10 +234,12 @@ export class AdminInterface extends LitElement {
<span slot="label">${t`Invitations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item
.condition=${superUserCondition}>
<ak-sidebar-item .condition=${superUserCondition}>
<span slot="label">${t`Identity & Cryptography`}</span>
<ak-sidebar-item path="/identity/users" .activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]}>
<ak-sidebar-item
path="/identity/users"
.activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]}
>
<span slot="label">${t`Users`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/identity/groups">
@ -205,5 +254,4 @@ export class AdminInterface extends LitElement {
</ak-sidebar-item>
`;
}
}

View File

@ -1,15 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/page.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
<script src="/static/dist/poly.js" type="module"></script>
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
<script>
window["polymerSkipLoadingFontRoboto"] = true;
</script>
<script src="/static/dist/AdminInterface.js" type="module"></script>
<title>authentik</title>
</head>
@ -17,9 +19,13 @@
<ak-message-container></ak-message-container>
<ak-interface-admin>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state" style="height: 100vh">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
<span
class="pf-c-spinner pf-m-xl pf-c-empty-state__icon"
role="progressbar"
aria-valuetext="Loading..."
>
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>

View File

@ -1,14 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
<script>ShadyDOM = { force: !navigator.webdriver }; window["polymerSkipLoadingFontRoboto"] = true;</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/page.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
<script>
ShadyDOM = { force: !navigator.webdriver };
window["polymerSkipLoadingFontRoboto"] = true;
</script>
<script src="/static/dist/poly.js" type="module"></script>
<script src="/static/dist/FlowInterface.js" type="module"></script>
<title>authentik</title>
@ -17,9 +20,13 @@
<ak-message-container></ak-message-container>
<ak-flow-executor>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state" style="height: 100vh">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
<span
class="pf-c-spinner pf-m-xl pf-c-empty-state__icon"
role="progressbar"
aria-valuetext="Loading..."
>
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>

View File

@ -1,5 +1,13 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
import { Application, CoreApi } from "authentik-api";
@ -20,11 +28,15 @@ import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
@customElement("ak-library-app")
export class LibraryApplication extends LitElement {
@property({attribute: false})
@property({ attribute: false })
application?: Application;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFAvatar, AKGlobal,
return [
PFBase,
PFCard,
PFAvatar,
AKGlobal,
css`
a {
height: 100%;
@ -48,7 +60,7 @@ export class LibraryApplication extends LitElement {
justify-content: center;
margin-right: 0.25em;
}
`
`,
];
}
@ -56,19 +68,28 @@ export class LibraryApplication extends LitElement {
if (!this.application) {
return html`<ak-spinner></ak-spinner>`;
}
return html` <a href="${ifDefined(this.application.launchUrl ?? "")}" class="pf-c-card pf-m-hoverable pf-m-compact">
return html` <a
href="${ifDefined(this.application.launchUrl ?? "")}"
class="pf-c-card pf-m-hoverable pf-m-compact"
>
<div class="pf-c-card__header">
${this.application.metaIcon
? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.metaIcon)}" alt="Application Icon"/>`
? html`<img
class="app-icon pf-c-avatar"
src="${ifDefined(this.application.metaIcon)}"
alt="Application Icon"
/>`
: html`<i class="fas fas fa-share-square"></i>`}
${until(me().then((u) => {
if (!u.user.isSuperuser) return html``;
return html`
<a href="#/core/applications/${this.application?.slug}">
<i class="fas fa-pencil-alt"></i>
</a>
`;
}))}
${until(
me().then((u) => {
if (!u.user.isSuperuser) return html``;
return html`
<a href="#/core/applications/${this.application?.slug}">
<i class="fas fa-pencil-alt"></i>
</a>
`;
}),
)}
</div>
<div class="pf-c-card__title">
<p id="card-1-check-label">${this.application.name}</p>
@ -76,15 +97,13 @@ export class LibraryApplication extends LitElement {
<small>${this.application.metaPublisher}</small>
</div>
</div>
<div class="pf-c-card__body">${truncate(this.application.metaDescription, 35)}</div>
</a>`;
}
}
@customElement("ak-library")
export class LibraryPage extends LitElement {
@property({attribute: false})
@property({ attribute: false })
apps?: AKResponse<Application>;
pageTitle(): string {
@ -120,20 +139,23 @@ export class LibraryPage extends LitElement {
renderApps(): TemplateResult {
return html`<div class="pf-l-gallery pf-m-gutter">
${this.apps?.results.map((app) => html`<ak-library-app .application=${app}></ak-library-app>`)}
${this.apps?.results.map(
(app) => html`<ak-library-app .application=${app}></ak-library-app>`,
)}
</div>`;
}
render(): TemplateResult {
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<ak-page-header
icon="pf-icon pf-icon-applications"
header=${t`Applications`}>
<ak-page-header icon="pf-icon pf-icon-applications" header=${t`Applications`}>
</ak-page-header>
<section class="pf-c-page__main-section">
${loading(this.apps, html`${(this.apps?.results.length || 0) > 0 ?
this.renderApps() :
this.renderEmptyState()}`)}
${loading(
this.apps,
html`${(this.apps?.results.length || 0) > 0
? this.renderApps()
: this.renderEmptyState()}`,
)}
</section>
</main>`;
}

View File

@ -26,101 +26,173 @@ import "../../elements/PageHeader";
@customElement("ak-admin-overview")
export class AdminOverviewPage extends LitElement {
static get styles(): CSSResult[] {
return [PFGrid, PFPage, PFContent, AKGlobal, css`
.row-divider {
margin-top: -4px;
margin-bottom: -4px;
}
.graph-container {
height: 20em;
}
.big-graph-container {
height: 35em;
}
.card-container {
max-height: 10em;
}
`];
return [
PFGrid,
PFPage,
PFContent,
AKGlobal,
css`
.row-divider {
margin-top: -4px;
margin-bottom: -4px;
}
.graph-container {
height: 20em;
}
.big-graph-container {
height: 35em;
}
.card-container {
max-height: 10em;
}
`,
];
}
render(): TemplateResult {
return html`
<ak-page-header
icon=""
header=${t`System Overview`}
description=${t`General system status`}>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
<!-- row 1 -->
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="pf-icon pf-icon-infrastructure" header=${t`Policies`} headerLink="#/policy/policies">
<ak-admin-status-chart-policy></ak-admin-status-chart-policy>
</ak-aggregate-card>
return html` <ak-page-header
icon=""
header=${t`System Overview`}
description=${t`General system status`}
>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
<!-- row 1 -->
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-infrastructure"
header=${t`Policies`}
headerLink="#/policy/policies"
>
<ak-admin-status-chart-policy></ak-admin-status-chart-policy>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-server"
header=${t`Flows`}
headerLink="#/flow/flows"
>
<ak-admin-status-chart-flow></ak-admin-status-chart-flow>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`Outpost status`}
headerLink="#/outpost/outposts"
>
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`Users`}
headerLink="#/identity/users"
>
<ak-admin-status-chart-user-count></ak-admin-status-chart-user-count>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`Groups`}
headerLink="#/identity/groups"
>
<ak-admin-status-chart-group-count></ak-admin-status-chart-group-count>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`LDAP Sync status`}
headerLink="#/core/sources"
>
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
</div>
<!-- row 2 -->
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-version
icon="pf-icon pf-icon-bundle"
header=${t`Version`}
headerLink="https://github.com/goauthentik/authentik/releases"
>
</ak-admin-status-version>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-2-col-on-md pf-m-2-col-on-xl card-container"
>
<ak-admin-status-card-backup
icon="fa fa-database"
header=${t`Backup status`}
headerLink="#/administration/system-tasks"
>
</ak-admin-status-card-backup>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container"
>
<ak-admin-status-card-workers
icon="pf-icon pf-icon-server"
header=${t`Workers`}
>
</ak-admin-status-card-workers>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container"
>
<ak-admin-status-system
icon="pf-icon pf-icon-server"
header=${t`System status`}
>
</ak-admin-status-system>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
</div>
<!-- row 3 -->
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-server"
header=${t`Logins over the last 24 hours`}
>
<ak-charts-admin-login></ak-charts-admin-login>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl big-graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-server"
header=${t`Apps with most usage`}
>
<ak-top-applications-table></ak-top-applications-table>
</ak-aggregate-card>
</div>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Flows`} headerLink="#/flow/flows">
<ak-admin-status-chart-flow></ak-admin-status-chart-flow>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Outpost status`} headerLink="#/outpost/outposts">
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Users`} headerLink="#/identity/users">
<ak-admin-status-chart-user-count></ak-admin-status-chart-user-count>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Groups`} headerLink="#/identity/groups">
<ak-admin-status-chart-group-count></ak-admin-status-chart-group-count>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`LDAP Sync status`} headerLink="#/core/sources">
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr>
</div>
<!-- row 2 -->
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container">
<ak-admin-status-version icon="pf-icon pf-icon-bundle" header=${t`Version`} headerLink="https://github.com/goauthentik/authentik/releases">
</ak-admin-status-version>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-2-col-on-md pf-m-2-col-on-xl card-container">
<ak-admin-status-card-backup icon="fa fa-database" header=${t`Backup status`} headerLink="#/administration/system-tasks">
</ak-admin-status-card-backup>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
<ak-admin-status-card-workers icon="pf-icon pf-icon-server" header=${t`Workers`}>
</ak-admin-status-card-workers>
</div>
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
<ak-admin-status-system icon="pf-icon pf-icon-server" header=${t`System status`}>
</ak-admin-status-system>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr>
</div>
<!-- row 3 -->
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container">
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Logins over the last 24 hours`}>
<ak-charts-admin-login></ak-charts-admin-login>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl big-graph-container">
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Apps with most usage`}>
<ak-top-applications-table></ak-top-applications-table>
</ak-aggregate-card>
</div>
</div>
</section>`;
</section>`;
}
}

View File

@ -9,8 +9,7 @@ import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-top-applications-table")
export class TopApplicationsTable extends LitElement {
@property({attribute: false})
@property({ attribute: false })
topN?: EventTopPerUser[];
static get styles(): CSSResult[] {
@ -18,41 +17,43 @@ export class TopApplicationsTable extends LitElement {
}
firstUpdated(): void {
new EventsApi(DEFAULT_CONFIG).eventsEventsTopPerUserList({
action: "authorize_application",
topN: 11,
}).then((events) => {
this.topN = events;
});
new EventsApi(DEFAULT_CONFIG)
.eventsEventsTopPerUserList({
action: "authorize_application",
topN: 11,
})
.then((events) => {
this.topN = events;
});
}
renderRow(event: EventTopPerUser): TemplateResult {
return html`<tr role="row">
<td role="cell">${event.application.name}</td>
<td role="cell">${event.countedEvents}</td>
<td role="cell">
${event.application.name}
</td>
<td role="cell">
${event.countedEvents}
</td>
<td role="cell">
<progress value="${event.countedEvents}" max="${this.topN ? this.topN[0].countedEvents : 0}"></progress>
<progress
value="${event.countedEvents}"
max="${this.topN ? this.topN[0].countedEvents : 0}"
></progress>
</td>
</tr>`;
}
render(): TemplateResult {
return html`<table class="pf-c-table pf-m-compact" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">${t`Application`}</th>
<th role="columnheader" scope="col">${t`Logins`}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
${this.topN ? this.topN.map((e) => this.renderRow(e)) : html`<ak-spinner></ak-spinner>`}
</tbody>
</table>`;
<thead>
<tr role="row">
<th role="columnheader" scope="col">${t`Application`}</th>
<th role="columnheader" scope="col">${t`Logins`}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
${this.topN
? this.topN.map((e) => this.renderRow(e))
: html`<ak-spinner></ak-spinner>`}
</tbody>
</table>`;
}
}

View File

@ -10,7 +10,6 @@ export interface AdminStatus {
}
export abstract class AdminStatusCard<T> extends AggregateCard {
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
@ -30,16 +29,20 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
renderInner(): TemplateResult {
return html`<p class="center-value">
${until(this.getPrimaryValue().then((v) => {
this.value = v;
return this.getStatus(v);
}).then((status) => {
return html`<p>
<i class="${status.icon}"></i>&nbsp;${this.renderValue()}
</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : html``}`;
}), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
${until(
this.getPrimaryValue()
.then((v) => {
this.value = v;
return this.getStatus(v);
})
.then((status) => {
return html`<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message
? html`<p class="subtext">${status.message}</p>`
: html``}`;
}),
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
)}
</p>`;
}
}

View File

@ -7,22 +7,24 @@ import { convertToTitle } from "../../../utils";
@customElement("ak-admin-status-card-backup")
export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
getPrimaryValue(): Promise<StatusEnum> {
return new AdminApi(DEFAULT_CONFIG).adminSystemTasksRetrieve({
id: "backup_database"
}).then((value) => {
return value.status;
}).catch(() => {
// On error (probably 404), check the config and see if the server
// can even backup
return config().then(c => {
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
return StatusEnum.Error;
}
return StatusEnum.Warning;
return new AdminApi(DEFAULT_CONFIG)
.adminSystemTasksRetrieve({
id: "backup_database",
})
.then((value) => {
return value.status;
})
.catch(() => {
// On error (probably 404), check the config and see if the server
// can even backup
return config().then((c) => {
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
return StatusEnum.Error;
}
return StatusEnum.Warning;
});
});
});
}
renderValue(): TemplateResult {
@ -43,9 +45,8 @@ export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
});
default:
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
icon: "fa fa-check-circle pf-m-success",
});
}
}
}

View File

@ -6,7 +6,6 @@ import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-system")
export class SystemStatusCard extends AdminStatusCard<System> {
now?: Date;
header = "OK";
@ -35,12 +34,11 @@ export class SystemStatusCard extends AdminStatusCard<System> {
}
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
message: t`Everything is ok.`
message: t`Everything is ok.`,
});
}
renderValue(): TemplateResult {
return html`${this.header}`;
}
}

View File

@ -6,7 +6,6 @@ import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-version")
export class VersionStatusCard extends AdminStatusCard<Version> {
getPrimaryValue(): Promise<Version> {
return new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
}
@ -26,12 +25,11 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
}
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
message: t`Up-to-date!`
message: t`Up-to-date!`,
});
}
renderValue(): TemplateResult {
return html`${this.value?.versionCurrent}`;
}
}

View File

@ -6,7 +6,6 @@ import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
@customElement("ak-admin-status-card-workers")
export class WorkersStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return new AdminApi(DEFAULT_CONFIG).adminWorkersRetrieve().then((workers) => {
return workers.count;
@ -21,9 +20,8 @@ export class WorkersStatusCard extends AdminStatusCard<number> {
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
icon: "fa fa-check-circle pf-m-success",
});
}
}
}

View File

@ -13,7 +13,6 @@ interface FlowMetrics {
@customElement("ak-admin-status-chart-flow")
export class PolicyStatusChart extends AKChart<FlowMetrics> {
getChartType(): string {
return "doughnut";
}
@ -32,9 +31,11 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
async apiRequest(): Promise<FlowMetrics> {
const api = new FlowsApi(DEFAULT_CONFIG);
const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0;
const count = (await api.flowsInstancesList({
pageSize: 1
})).pagination.count;
const count = (
await api.flowsInstancesList({
pageSize: 1,
})
).pagination.count;
this.centerText = count.toString();
return {
count: count - cached,
@ -44,24 +45,14 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
getChartData(data: FlowMetrics): ChartData {
return {
labels: [
t`Total flows`,
t`Cached flows`,
],
labels: [t`Total flows`, t`Cached flows`],
datasets: [
{
backgroundColor: [
"#2b9af3",
"#3e8635",
],
backgroundColor: ["#2b9af3", "#3e8635"],
spanGaps: true,
data: [
data.count,
data.cached,
],
data: [data.count, data.cached],
},
]
],
};
}
}

View File

@ -12,7 +12,6 @@ interface GroupMetrics {
@customElement("ak-admin-status-chart-group-count")
export class GroupCountStatusChart extends AKChart<GroupMetrics> {
getChartType(): string {
return "doughnut";
}
@ -30,12 +29,16 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
async apiRequest(): Promise<GroupMetrics> {
const api = new CoreApi(DEFAULT_CONFIG);
const count = (await api.coreGroupsList({
pageSize: 1
})).pagination.count;
const superusers = (await api.coreGroupsList({
isSuperuser: true
})).pagination.count;
const count = (
await api.coreGroupsList({
pageSize: 1,
})
).pagination.count;
const superusers = (
await api.coreGroupsList({
isSuperuser: true,
})
).pagination.count;
this.centerText = count.toString();
return {
count: count - superusers,
@ -45,24 +48,14 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
getChartData(data: GroupMetrics): ChartData {
return {
labels: [
t`Total groups`,
t`Superuser-groups`,
],
labels: [t`Total groups`, t`Superuser-groups`],
datasets: [
{
backgroundColor: [
"#2b9af3",
"#3e8635",
],
backgroundColor: ["#2b9af3", "#3e8635"],
spanGaps: true,
data: [
data.count,
data.superusers,
],
data: [data.count, data.superusers],
},
]
],
};
}
}

View File

@ -14,7 +14,6 @@ interface LDAPSyncStats {
@customElement("ak-admin-status-chart-ldap-sync")
export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
getChartType(): string {
return "doughnut";
}
@ -36,56 +35,45 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
let healthy = 0;
let failed = 0;
let unsynced = 0;
await Promise.all(sources.results.map(async (element) => {
try {
const health = await api.sourcesLdapSyncStatusRetrieve({
slug: element.slug,
});
if (health.status !== StatusEnum.Successful) {
failed += 1;
}
const now = new Date().getTime();
const maxDelta = 3600000; // 1 hour
if (!health || (now - health.taskFinishTimestamp.getTime()) > maxDelta) {
await Promise.all(
sources.results.map(async (element) => {
try {
const health = await api.sourcesLdapSyncStatusRetrieve({
slug: element.slug,
});
if (health.status !== StatusEnum.Successful) {
failed += 1;
}
const now = new Date().getTime();
const maxDelta = 3600000; // 1 hour
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
unsynced += 1;
} else {
healthy += 1;
}
} catch {
unsynced += 1;
} else {
healthy += 1;
}
} catch {
unsynced += 1;
}
}));
}),
);
this.centerText = sources.pagination.count.toString();
return {
healthy: sources.pagination.count === 0 ? -1 : healthy,
failed,
unsynced
unsynced,
};
}
getChartData(data: LDAPSyncStats): ChartData {
return {
labels: [
t`Healthy sources`,
t`Failed sources`,
t`Unsynced sources`,
],
labels: [t`Healthy sources`, t`Failed sources`, t`Unsynced sources`],
datasets: [
{
backgroundColor: [
"#3e8635",
"#C9190B",
"#2b9af3",
],
backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"],
spanGaps: true,
data: [
data.healthy,
data.failed,
data.unsynced
],
data: [data.healthy, data.failed, data.unsynced],
},
]
],
};
}
}

View File

@ -14,7 +14,6 @@ interface OutpostStats {
@customElement("ak-admin-status-chart-outpost")
export class OutpostStatusChart extends AKChart<OutpostStats> {
getChartType(): string {
return "doughnut";
}
@ -36,52 +35,41 @@ export class OutpostStatusChart extends AKChart<OutpostStats> {
let healthy = 0;
let outdated = 0;
let unhealthy = 0;
await Promise.all(outposts.results.map(async (element) => {
const health = await api.outpostsInstancesHealthList({
uuid: element.pk || "",
});
if (health.length === 0) {
unhealthy += 1;
}
health.forEach(h => {
if (h.versionOutdated) {
outdated += 1;
} else {
healthy += 1;
await Promise.all(
outposts.results.map(async (element) => {
const health = await api.outpostsInstancesHealthList({
uuid: element.pk || "",
});
if (health.length === 0) {
unhealthy += 1;
}
});
}));
health.forEach((h) => {
if (h.versionOutdated) {
outdated += 1;
} else {
healthy += 1;
}
});
}),
);
this.centerText = outposts.pagination.count.toString();
return {
healthy: outposts.pagination.count === 0 ? -1 : healthy,
outdated,
unhealthy
unhealthy,
};
}
getChartData(data: OutpostStats): ChartData {
return {
labels: [
t`Healthy outposts`,
t`Outdated outposts`,
t`Unhealthy outposts`,
],
labels: [t`Healthy outposts`, t`Outdated outposts`, t`Unhealthy outposts`],
datasets: [
{
backgroundColor: [
"#3e8635",
"#f0ab00",
"#C9190B",
],
backgroundColor: ["#3e8635", "#f0ab00", "#C9190B"],
spanGaps: true,
data: [
data.healthy,
data.outdated,
data.unhealthy
],
data: [data.healthy, data.outdated, data.unhealthy],
},
]
],
};
}
}

Some files were not shown because too many files have changed in this diff Show More