Merge branch 'master' into stage-challenge

# Conflicts:
#	authentik/api/v2/urls.py
This commit is contained in:
Jens Langhammer
2021-02-20 12:51:10 +01:00
120 changed files with 2865 additions and 2598 deletions

View File

@ -1,4 +1,4 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { DefaultClient, AKResponse, QueryArguments, BaseInheritanceModel } from "./Client";
import { TypeCreate } from "./Providers";
export enum FlowDesignation {
@ -40,8 +40,8 @@ export class Flow {
}
static cached(): Promise<number> {
return DefaultClient.fetch<AKResponse<Flow>>(["flows", "cached"]).then(r => {
return r.pagination.count;
return DefaultClient.fetch<{ count: number }>(["flows", "all", "cached"]).then(r => {
return r.count;
});
}
static adminUrl(rest: string): string {
@ -49,16 +49,26 @@ export class Flow {
}
}
export class Stage {
export class Stage implements BaseInheritanceModel {
pk: string;
name: string;
__type__: string;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
flow_set: Flow[];
constructor() {
throw Error();
}
static get(slug: string): Promise<Stage> {
return DefaultClient.fetch<Stage>(["stages", "all", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Stage>> {
return DefaultClient.fetch<AKResponse<Stage>>(["stages", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["stages", "all", "types"]);
}

View File

@ -1,15 +1,28 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { EventContext } from "./Events";
export class Group {
group_uuid: string;
pk: string;
name: string;
is_superuser: boolean;
attributes: EventContext;
parent?: Group;
users: number[];
constructor() {
throw Error();
}
static get(pk: string): Promise<Group> {
return DefaultClient.fetch<Group>(["core", "groups", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Group>> {
return DefaultClient.fetch<AKResponse<Group>>(["core", "groups"], filter);
}
static adminUrl(rest: string): string {
return `/administration/groups/${rest}`;
}
}

View File

@ -0,0 +1,27 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { EventContext } from "./Events";
import { User } from "./Users";
export class Invitation {
pk: string;
expires: number;
fixed_date: EventContext;
created_by: User;
constructor() {
throw Error();
}
static get(pk: string): Promise<Invitation> {
return DefaultClient.fetch<Invitation>(["stages", "invitation", "invitations", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Invitation>> {
return DefaultClient.fetch<AKResponse<Invitation>>(["stages", "invitation", "invitations"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/invitations/${rest}`;
}
}

View File

@ -1,5 +1,5 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Provider } from "./Providers";
import { Provider, TypeCreate } from "./Providers";
export interface OutpostHealth {
last_seen: number;
@ -38,3 +38,42 @@ export class Outpost {
return `/administration/outposts/${rest}`;
}
}
export interface OutpostServiceConnectionState {
version: string;
healthy: boolean;
}
export class OutpostServiceConnection {
pk: string;
name: string;
local: boolean;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<OutpostServiceConnection> {
return DefaultClient.fetch<OutpostServiceConnection>(["outposts", "service_connections", "all", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<OutpostServiceConnection>> {
return DefaultClient.fetch<AKResponse<OutpostServiceConnection>>(["outposts", "service_connections", "all"], filter);
}
static state(pk: string): Promise<OutpostServiceConnectionState> {
return DefaultClient.fetch<OutpostServiceConnectionState>(["outposts", "service_connections", "all", pk, "state"]);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["outposts", "service_connections", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/outpost_service_connections/${rest}`;
}
}

View File

@ -1,15 +1,18 @@
import { DefaultClient, BaseInheritanceModel, AKResponse, QueryArguments } from "./Client";
import { TypeCreate } from "./Providers";
export class Policy implements BaseInheritanceModel {
pk: string;
name: string;
execution_logging: boolean;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
bound_to: number;
constructor() {
throw Error();
}
object_type: string;
verbose_name: string;
verbose_name_plural: string;
static get(pk: string): Promise<Policy> {
return DefaultClient.fetch<Policy>(["policies", "all", pk]);
@ -20,8 +23,16 @@ export class Policy implements BaseInheritanceModel {
}
static cached(): Promise<number> {
return DefaultClient.fetch<AKResponse<Policy>>(["policies", "cached"]).then(r => {
return r.pagination.count;
return DefaultClient.fetch<{ count: number }>(["policies", "all", "cached"]).then(r => {
return r.count;
});
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["policies", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/policies/${rest}`;
}
}

30
web/src/api/Prompts.ts Normal file
View File

@ -0,0 +1,30 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { Stage } from "./Flows";
export class Prompt {
pk: string;
field_key: string;
label: string;
type: string;
required: boolean;
placeholder: string;
order: number;
promptstage_set: Stage[];
constructor() {
throw Error();
}
static get(pk: string): Promise<Prompt> {
return DefaultClient.fetch<Prompt>(["stages", "prompt", "prompts", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Prompt>> {
return DefaultClient.fetch<AKResponse<Prompt>>(["stages", "prompt", "prompts"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/prompts/${rest}`;
}
}

View File

@ -1,4 +1,5 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { TypeCreate } from "./Providers";
export class PropertyMapping {
pk: string;
@ -20,6 +21,10 @@ export class PropertyMapping {
return DefaultClient.fetch<AKResponse<PropertyMapping>>(["propertymappings", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["propertymappings", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/property-mappings/${rest}`;
}

33
web/src/api/SystemTask.ts Normal file
View File

@ -0,0 +1,33 @@
import { DefaultClient, QueryArguments } from "./Client";
export enum TaskStatus {
SUCCESSFUL = 1,
WARNING = 2,
ERROR = 4,
}
export class SystemTask {
task_name: string;
task_description: string;
task_finish_timestamp: number;
status: TaskStatus;
messages: string[];
constructor() {
throw Error();
}
static get(task_name: string): Promise<SystemTask> {
return DefaultClient.fetch<SystemTask>(["admin", "system_tasks", task_name]);
}
static list(filter?: QueryArguments): Promise<SystemTask[]> {
return DefaultClient.fetch<SystemTask[]>(["admin", "system_tasks"], filter);
}
static retry(task_name: string): string {
return DefaultClient.makeUrl(["admin", "system_tasks", task_name, "retry"]);
}
}

View File

@ -1,11 +1,47 @@
import { DefaultClient } from "./Client";
import { AKResponse, DefaultClient, QueryArguments } from "./Client";
import { User } from "./Users";
interface TokenResponse {
key: string;
export enum TokenIntent {
INTENT_VERIFICATION = "verification",
INTENT_API = "api",
INTENT_RECOVERY = "recovery",
}
export function tokenByIdentifier(identifier: string): Promise<string> {
return DefaultClient.fetch<TokenResponse>(["core", "tokens", identifier, "view_key"]).then(
(r) => r.key
);
export class Token {
pk: string;
identifier: string;
intent: TokenIntent;
user: User;
description: string;
expires: number;
expiring: boolean;
constructor() {
throw Error();
}
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "tokens", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Token>> {
return DefaultClient.fetch<AKResponse<Token>>(["core", "tokens"], filter);
}
static adminUrl(rest: string): string {
return `/administration/tokens/${rest}`;
}
static userUrl(rest: string): string {
return `/-/user/tokens/${rest}`;
}
static getKey(identifier: string): Promise<string> {
return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then(
(r) => r.key
);
}
}

View File

@ -1,4 +1,4 @@
import { DefaultClient, AKResponse } from "./Client";
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
let _globalMePromise: Promise<User>;
@ -9,11 +9,25 @@ export class User {
is_superuser: boolean;
email: boolean;
avatar: string;
is_active: boolean;
last_login: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "users", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<User>> {
return DefaultClient.fetch<AKResponse<User>>(["core", "users"], filter);
}
static adminUrl(rest: string): string {
return `/administration/users/${rest}`;
}
static me(): Promise<User> {
if (!_globalMePromise) {
_globalMePromise = DefaultClient.fetch<User>(["core", "users", "me"]);

View File

@ -85,10 +85,6 @@ select[multiple] {
z-index: auto !important;
}
.pf-c-page__main {
display: block;
}
@media (prefers-color-scheme: dark) {
:root {
--ak-dark-foreground: #fafafa;

View File

@ -70,18 +70,18 @@ export class SpinnerButton extends LitElement {
@click=${() => this.callAction()}
>
${this.isRunning
? html` <span class="pf-c-button__progress">
<span
class="pf-c-spinner pf-m-md"
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>
</span>
</span>`
: ""}
? html` <span class="pf-c-button__progress">
<span
class="pf-c-spinner pf-m-md"
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>
</span>
</span>`
: ""}
<slot></slot>
</button>`;
}

View File

@ -3,7 +3,7 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
import { tokenByIdentifier } from "../../api/Tokens";
import { Token } from "../../api/Tokens";
import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
@customElement("ak-token-copy-button")
@ -35,7 +35,7 @@ export class TokenCopyButton extends LitElement {
}, 1500);
return;
}
tokenByIdentifier(this.identifier).then((token) => {
Token.getKey(this.identifier).then((token) => {
navigator.clipboard.writeText(token).then(() => {
this.buttonClass = SUCCESS_CLASS;
setTimeout(() => {

View File

@ -47,6 +47,7 @@ export class MessageContainer extends LitElement {
this.messageSocket = new WebSocket(wsUrl);
this.messageSocket.addEventListener("open", () => {
console.debug(`authentik/messages: connected to ${wsUrl}`);
this.retryDelay = 200;
});
this.messageSocket.addEventListener("close", (e) => {
console.debug(`authentik/messages: closed ws connection: ${e}`);

View File

@ -1,8 +1,13 @@
import { customElement, html, LitElement, TemplateResult } from "lit-element";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-notification-trigger")
export class NotificationRule extends LitElement {
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
constructor() {
super();
this.addEventListener("click", () => {
@ -16,7 +21,8 @@ export class NotificationRule extends LitElement {
}
render(): TemplateResult {
return html`<slot></slot>`;
// TODO: Show icon with red dot when unread notifications exist
return html`<i class="fas fa-bell pf-c-dropdown__toggle-icon" aria-hidden="true"></i>`;
}
}

View File

@ -0,0 +1,27 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-router-404")
export class Router404 extends LitElement {
@property()
url = "";
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
return html`<div class="pf-c-empty-state pf-m-full-height">
<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">${gettext("Not found")}</h1>
<div class="pf-c-empty-state__body">
${gettext(`The url '${this.url}' was not found.`)}
</div>
<a href="#/" class="pf-c-button pf-m-primary" type="button">${gettext("Return home")}</a>
</div>
</div>`;
}
}

View File

@ -10,6 +10,7 @@ import { ROUTES } from "../../routes";
import { RouteMatch } from "./RouteMatch";
import "../../pages/generic/SiteShell";
import "./Router404";
@customElement("ak-router-outlet")
export class RouterOutlet extends LitElement {
@ -28,6 +29,11 @@ export class RouterOutlet extends LitElement {
:host {
height: 100vh;
}
*:first-child {
height: 100%;
display: flex;
flex-direction: column;
}
`,
].concat(...COMMON_STYLES);
}
@ -62,12 +68,12 @@ export class RouterOutlet extends LitElement {
}
});
if (!matchedRoute) {
console.debug(`authentik/router: route "${activeUrl}" not defined, defaulting to shell`);
console.debug(`authentik/router: route "${activeUrl}" not defined`);
const route = new Route(
RegExp(""),
html`<ak-site-shell url=${activeUrl}>
<div slot="body"></div>
</ak-site-shell>`
html`<div class="pf-c-page__main">
<ak-router-404 url=${activeUrl}></ak-router-404>
</div>`
);
matchedRoute = new RouteMatch(route);
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
@ -77,7 +83,6 @@ export class RouterOutlet extends LitElement {
}
render(): TemplateResult | undefined {
// TODO: Render 404 when current Route is empty
return this.current?.render();
}
}

View File

@ -29,7 +29,7 @@ export class SidebarItem {
this.condition = async () => true;
this.activeMatchers = [];
if (this.path) {
this.activeMatchers.push(new RegExp(`^${this.path}`));
this.activeMatchers.push(new RegExp(`^${this.path}$`));
}
}

View File

@ -37,11 +37,11 @@ export class SidebarUser extends LitElement {
render(): TemplateResult {
return html`
<a href="#/-/user/" class="pf-c-nav__link user-avatar" id="user-settings">
${until(User.me().then(u => {
return html`<img class="pf-c-avatar" src="${u.avatar}" alt="" />`;}), html``)}
${until(User.me().then((u) => {
return html`<img class="pf-c-avatar" src="${u.avatar}" alt="" />`;
}), html``)}
</a>
<ak-notification-trigger class="pf-c-nav__link user-notifications">
<i class="fas fa-bell pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</ak-notification-trigger>
<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

@ -43,7 +43,7 @@ export class TablePagination extends LitElement {
<button
class="pf-c-button pf-m-plain"
@click=${() => { this.pageChangeHandler(this.pages?.next || 0); }}
?disabled="${(this.pages?.next || 0) < 0}"
?disabled="${(this.pages?.next || 0) <= 0}"
aria-label="${gettext("Go to next page")}"
>
<i class="fas fa-angle-right" aria-hidden="true"></i>

View File

@ -8,7 +8,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Library", "/library"),
new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("System Tasks", "/administration/system-tasks"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
@ -20,37 +20,37 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Resources").children(
new SidebarItem("Applications", "/applications").activeWhen(
`^/applications/(?<slug>${SLUG_REGEX})$`
new SidebarItem("Applications", "/core/applications").activeWhen(
`^/core/applications/(?<slug>${SLUG_REGEX})$`
),
new SidebarItem("Sources", "/sources").activeWhen(
`^/sources/(?<slug>${SLUG_REGEX})$`,
new SidebarItem("Sources", "/core/sources").activeWhen(
`^/core/sources/(?<slug>${SLUG_REGEX})$`,
),
new SidebarItem("Providers", "/providers"),
new SidebarItem("Outposts", "/outposts"),
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
new SidebarItem("Providers", "/core/providers"),
new SidebarItem("Outposts", "/outpost/outposts"),
new SidebarItem("Outpost Service Connections", "/outpost/service-connections"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Customisation").children(
new SidebarItem("Policies", "/administration/policies/"),
new SidebarItem("Property Mappings", "/property-mappings"),
new SidebarItem("Policies", "/policy/policies"),
new SidebarItem("Property Mappings", "/core/property-mappings"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Flows").children(
new SidebarItem("Flows", "/flows").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})$`),
new SidebarItem("Stages", "/administration/stages/"),
new SidebarItem("Prompts", "/administration/stages_prompts/"),
new SidebarItem("Invitations", "/administration/stages/invitations/"),
new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?<slug>${SLUG_REGEX})$`),
new SidebarItem("Stages", "/flow/stages"),
new SidebarItem("Prompts", "/flow/stages/prompts"),
new SidebarItem("Invitations", "/flow/stages/invitations"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Identity & Cryptography").children(
new SidebarItem("User", "/administration/users/"),
new SidebarItem("Groups", "/administration/groups/"),
new SidebarItem("User", "/identity/users"),
new SidebarItem("Groups", "/identity/groups"),
new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/administration/tokens/"),
new SidebarItem("Tokens", "/core/tokens"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),

View File

@ -28,6 +28,7 @@ import "./pages/admin-overview/AdminOverviewPage";
import "./pages/admin-overview/TopApplicationsTable";
import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/tokens/UserTokenList";
import "./pages/LibraryPage";
import "./elements/stages/authenticator_webauthn/WebAuthnRegister";

View File

@ -14,6 +14,10 @@ export class LibraryApplication extends LitElement {
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
:host,
main {
height: 100%;
}
a {
height: 100%;
}

View File

@ -36,7 +36,7 @@ export class AdminOverviewPage extends LitElement {
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<ak-top-applications-table></ak-top-applications-table>
</ak-aggregate-card>
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/providers/">
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/core/providers/">
</ak-admin-status-card-provider>
<ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/">
</ak-admin-status-card-policy-unbound>

View File

@ -50,7 +50,7 @@ export class ApplicationListPage extends TablePage<Application> {
item.meta_icon ?
html`<img class="app-icon pf-c-avatar" src="${item.meta_icon}" alt="${gettext("Application Icon")}">` :
html`<i class="pf-icon pf-icon-arrow"></i>`,
html`<a href="#/applications/${item.slug}">
html`<a href="#/core/applications/${item.slug}">
<div>
${item.name}
</div>

View File

@ -38,7 +38,14 @@ export class ApplicationViewPage extends LitElement {
render(): TemplateResult {
if (!this.application) {
return html`<ak-loading-state></ak-loading-state>`;
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
${gettext("Loading...")}
</h1>
</div>
</section>
<ak-loading-state></ak-loading-state>`;
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
@ -80,7 +87,7 @@ export class ApplicationViewPage extends LitElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a href="#/providers/${this.application.provider.pk}">
<a href="#/core/providers/${this.application.provider.pk}">
${this.application.provider.name}
</a>
</div>

View File

@ -107,7 +107,7 @@ export class EventInfo extends LitElement {
<span>${until(Flow.list({
flow_uuid: this.event.context.flow as string,
}).then(resp => {
return html`<a href="#/flows/${resp.results[0].slug}">${resp.results[0].name}</a>`;
return html`<a href="#/flow/flows/${resp.results[0].slug}">${resp.results[0].name}</a>`;
}), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)}
</span>
</div>

View File

@ -47,7 +47,7 @@ export class FlowListPage extends TablePage<Flow> {
row(item: Flow): TemplateResult[] {
return [
html`<a href="#/flows/${item.slug}">
html`<a href="#/flow/flows/${item.slug}">
<code>${item.slug}</code>
</a>`,
html`${item.name}`,

View File

@ -0,0 +1,80 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Group } from "../../api/Groups";
@customElement("ak-group-list")
export class GroupListPage extends TablePage<Group> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Groups");
}
pageDescription(): string {
return gettext("Group users together and give them permissions based on the membership.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-users");
}
@property()
order = "slug";
apiEndpoint(page: number): Promise<AKResponse<Group>> {
return Group.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Parent", "parent"),
new TableColumn("Members"),
new TableColumn("Superuser privileges?"),
new TableColumn(""),
];
}
row(item: Group): TemplateResult[] {
return [
html`${item.name}`,
html`${item.parent || "-"}`,
html`${item.users.length}`,
html`${item.is_superuser ? "Yes" : "No"}`,
html`
<ak-modal-button href="${Group.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Group.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Group.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -48,7 +48,7 @@ export class OutpostListPage extends TablePage<Outpost> {
return [
html`${item.name}`,
html`<ul>${item.providers_obj.map((p) => {
return html`<li><a href="#/providers/${p.pk}">${p.name}</a></li>`;
return html`<li><a href="#/core/providers/${p.pk}">${p.name}</a></li>`;
})}</ul>`,
html`<ak-outpost-health outpostId=${item.pk}></ak-outpost-health>`,
html`

View File

@ -0,0 +1,102 @@
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { AKResponse } from "../../api/Client";
import { OutpostServiceConnection } from "../../api/Outposts";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import "./OutpostHealth";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import { until } from "lit-html/directives/until";
@customElement("ak-outpost-service-connection-list")
export class OutpostServiceConnectionListPage extends TablePage<OutpostServiceConnection> {
pageTitle(): string {
return "Outpost Service-Connections";
}
pageDescription(): string | undefined {
return "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts.";
}
pageIcon(): string {
return "pf-icon pf-icon-integration";
}
searchEnabled(): boolean {
return true;
}
apiEndpoint(page: number): Promise<AKResponse<OutpostServiceConnection>> {
return OutpostServiceConnection.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Type"),
new TableColumn("Local", "local"),
new TableColumn("State"),
new TableColumn(""),
];
}
@property()
order = "name";
row(item: OutpostServiceConnection): TemplateResult[] {
return [
html`${item.name}`,
html`${item.verbose_name}`,
html`${item.local ? "Yes" : "No"}`,
html`${until(OutpostServiceConnection.state(item.pk).then((state) => {
if (state.healthy) {
return html`<i class="fas fa-check pf-m-success"></i> ${state.version}`;
}
return html`<i class="fas fa-times pf-m-danger"></i> ${gettext("Unhealthy")}`;
}), html`<ak-spinner></ak-spinner>`)}`,
html`
<ak-modal-button href="${OutpostServiceConnection.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${OutpostServiceConnection.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext("Create")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
${until(OutpostServiceConnection.getTypes().then((types) => {
return types.map((type) => {
return html`<li>
<ak-modal-button href="${type.link}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
<small>${type.description}</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;
}
}

View File

@ -0,0 +1,108 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Policy } from "../../api/Policies";
import { until } from "lit-html/directives/until";
@customElement("ak-policy-list")
export class PolicyListPage extends TablePage<Policy> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Policies");
}
pageDescription(): string {
return gettext("Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-infrastructure");
}
@property()
order = "name";
apiEndpoint(page: number): Promise<AKResponse<Policy>> {
return Policy.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Type"),
new TableColumn(""),
];
}
row(item: Policy): TemplateResult[] {
return [
html`<div>
<div>${item.name}</div>
${item.bound_to > 0 ?
html`<i class="pf-icon pf-icon-ok"></i>
<small>
${gettext(`Assigned to ${item.bound_to} objects.`)}
</small>`:
html`<i class="pf-icon pf-icon-warning-triangle"></i>
<small>${gettext("Warning: Policy is not assigned.")}</small>`}
</div>`,
html`${item.verbose_name}`,
html`
<ak-modal-button href="${Policy.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Policy.adminUrl(`${item.pk}/test/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Test")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Policy.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext("Create")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
${until(Policy.getTypes().then((types) => {
return types.map((type) => {
return html`<li>
<ak-modal-button href="${type.link}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
<small>${type.description}</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;
}
}

View File

@ -8,6 +8,7 @@ import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { until } from "lit-html/directives/until";
@customElement("ak-property-mapping-list")
export class PropertyMappingListPage extends TablePage<PropertyMapping> {
@ -82,36 +83,18 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
<li>
<ak-modal-button href="${PropertyMapping.adminUrl("create/?type=LDAPPropertyMapping")}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("LDAP Property Mapping")}<br>
<small>
${gettext("Map LDAP Property to User or Group object attribute")}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
<li>
<ak-modal-button href="${PropertyMapping.adminUrl("create/?type=SAMLPropertyMapping")}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("SAML Property Mapping")}<br>
<small>
${gettext("Map User/Group attribute to SAML Attribute, which can be used by the Service Provider.")}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
<li>
<ak-modal-button href="${PropertyMapping.adminUrl("create/?type=ScopeMapping")}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("Scope Mapping")}<br>
<small>
${gettext("Map an OAuth Scope to users properties")}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
${until(PropertyMapping.getTypes().then((types) => {
return types.map((type) => {
return html`<li>
<ak-modal-button href="${type.link}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
<small>${type.description}</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;

View File

@ -47,13 +47,13 @@ export class ProviderListPage extends TablePage<Provider> {
row(item: Provider): TemplateResult[] {
return [
html`<a href="#/providers/${item.pk}">
html`<a href="#/core/providers/${item.pk}">
${item.name}
</a>`,
item.assigned_application_name ?
html`<i class="pf-icon pf-icon-ok"></i>
${gettext("Assigned to application ")}
<a href="#/applications/${item.assigned_application_slug}">${item.assigned_application_name}</a>` :
<a href="#/core/applications/${item.assigned_application_slug}">${item.assigned_application_name}</a>` :
html`<i class="pf-icon pf-icon-warning-triangle"></i>
${gettext("Warning: Provider not assigned to any application.")}`,
html`${item.verbose_name}`,

View File

@ -14,7 +14,7 @@ export class RelatedApplicationButton extends LitElement {
render(): TemplateResult {
if (this.provider?.assigned_application_slug) {
return html`<a href="#/applications/${this.provider.assigned_application_slug}">
return html`<a href="#/core/applications/${this.provider.assigned_application_slug}">
${this.provider.assigned_application_name}
</a>`;
}

View File

@ -46,7 +46,7 @@ export class SourceListPage extends TablePage<Source> {
row(item: Source): TemplateResult[] {
return [
html`<a href="#/sources/${item.slug}">
html`<a href="#/core/sources/${item.slug}">
<div>${item.name}</div>
${item.enabled ? html`` : html`<small>${gettext("Disabled")}</small>`}
</a>`,

View File

@ -0,0 +1,72 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Invitation } from "../../api/Invitations";
@customElement("ak-stage-invitation-list")
export class InvitationListPage extends TablePage<Invitation> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Invitations");
}
pageDescription(): string {
return gettext("Create Invitation Links to enroll Users, and optionally force specific attributes of their account.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-migration");
}
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Invitation>> {
return Invitation.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("ID", "pk"),
new TableColumn("Created by", "created_by"),
new TableColumn("Expiry"),
new TableColumn(""),
];
}
row(item: Invitation): TemplateResult[] {
return [
html`${item.pk}`,
html`${item.created_by.username}`,
html`${new Date(item.expires * 1000).toLocaleString()}`,
html`
<ak-modal-button href="${Invitation.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Invitation.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -0,0 +1,84 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Prompt } from "../../api/Prompts";
@customElement("ak-stage-prompt-list")
export class PromptListPage extends TablePage<Prompt> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Prompts");
}
pageDescription(): string {
return gettext("Single Prompts that can be used for Prompt Stages.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-plugged");
}
@property()
order = "order";
apiEndpoint(page: number): Promise<AKResponse<Prompt>> {
return Prompt.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Field", "field_key"),
new TableColumn("Label", "label"),
new TableColumn("Type", "type"),
new TableColumn("Order", "order"),
new TableColumn("Stages"),
new TableColumn(""),
];
}
row(item: Prompt): TemplateResult[] {
return [
html`${item.field_key}`,
html`${item.label}`,
html`${item.type}`,
html`${item.order}`,
html`${item.promptstage_set.map((stage) => {
return html`<li>${stage.name}</li>`;
})}`,
html`
<ak-modal-button href="${Prompt.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Prompt.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Prompt.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -0,0 +1,100 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown";
import { until } from "lit-html/directives/until";
import { Stage } from "../../api/Flows";
@customElement("ak-stage-list")
export class StageListPage extends TablePage<Stage> {
pageTitle(): string {
return "Stages";
}
pageDescription(): string | undefined {
return "Stages are single steps of a Flow that a user is guided through.";
}
pageIcon(): string {
return "pf-icon pf-icon-plugged";
}
searchEnabled(): boolean {
return true;
}
@property()
order = "name";
apiEndpoint(page: number): Promise<AKResponse<Stage>> {
return Stage.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Flows"),
new TableColumn(""),
];
}
row(item: Stage): TemplateResult[] {
return [
html`<div>
<div>${item.name}</div>
<small>${item.verbose_name}</small>
</div>`,
html`${item.flow_set.map((flow) => {
return html`<a href="#/flow/flows/${flow.slug}">
<code>${flow.slug}</code>
</a>`;
})}`,
html`
<ak-modal-button href="${Stage.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Stage.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext("Create")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
${until(Stage.getTypes().then((types) => {
return types.map((type) => {
return html`<li>
<ak-modal-button href="${type.link}">
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
<small>${type.description}</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;
}
}

View File

@ -0,0 +1,87 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/ActionButton";
import { TableColumn } from "../../elements/table/Table";
import { SystemTask, TaskStatus } from "../../api/SystemTask";
@customElement("ak-system-task-list")
export class SystemTaskListPage extends TablePage<SystemTask> {
searchEnabled(): boolean {
return false;
}
pageTitle(): string {
return gettext("System Tasks");
}
pageDescription(): string {
return gettext("Long-running operations which authentik executes in the background.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-automation");
}
@property()
order = "slug";
apiEndpoint(page: number): Promise<AKResponse<SystemTask>> {
return SystemTask.list({
ordering: this.order,
page: page,
}).then((tasks) => {
return {
pagination: {
count: tasks.length,
total_pages: 1,
start_index: 0,
end_index: tasks.length,
current: 1,
},
results: tasks,
};
});
}
columns(): TableColumn[] {
return [
new TableColumn("Identifier", "task_name"),
new TableColumn("Description"),
new TableColumn("Last run"),
new TableColumn("Status"),
new TableColumn("Messages"),
new TableColumn(""),
];
}
taskStatus(task: SystemTask): TemplateResult {
switch (task.status) {
case TaskStatus.SUCCESSFUL:
return html`<i class="fas fa-check pf-m-success" > </i> ${gettext("Successful")}`;
case TaskStatus.WARNING:
return html`<i class="fas fa-exclamation-triangle pf-m-warning" > </i> ${gettext("Warning")}`;
case TaskStatus.ERROR:
return html`<i class="fas fa-times pf-m-danger" > </i> ${gettext("Error")}`;
default:
return html`<i class="fas fa-question-circle" > </i> ${gettext("Unknown")}`;
}
}
row(item: SystemTask): TemplateResult[] {
return [
html`${item.task_name}`,
html`${item.task_description}`,
html`${new Date(item.task_finish_timestamp * 1000).toLocaleString()}`,
this.taskStatus(item),
html`${item.messages.map(m => {
return html`<li>${m}</li>`;
})}`,
html`<ak-action-button url=${SystemTask.retry(item.task_name)}>
${gettext("Retry Task")}
</ak-action-button>`,
];
}
}

View File

@ -0,0 +1,68 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import "../../elements/buttons/TokenCopyButton";
import { TableColumn } from "../../elements/table/Table";
import { Token } from "../../api/Tokens";
@customElement("ak-token-list")
export class TokenListPage extends TablePage<Token> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Tokens");
}
pageDescription(): string {
return gettext("Tokens are used throughout authentik for Email validation stages, Recovery keys and API access.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-security");
}
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Token>> {
return Token.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Identifier", "identifier"),
new TableColumn("User", "user"),
new TableColumn("Expires?", "expiring"),
new TableColumn("Expiry date", "expires"),
new TableColumn(""),
];
}
row(item: Token): TemplateResult[] {
return [
html`${item.identifier}`,
html`${item.user.username}`,
html`${item.expiring ? "Yes" : "No"}`,
html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : "-"}`,
html`
<ak-modal-button href="${Token.adminUrl(`${item.identifier}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-token-copy-button identifier="${item.identifier}">
${gettext("Copy Key")}
</ak-token-copy-button>
`,
];
}
}

View File

@ -0,0 +1,64 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import "../../elements/buttons/TokenCopyButton";
import { Table, TableColumn } from "../../elements/table/Table";
import { Token } from "../../api/Tokens";
@customElement("ak-token-user-list")
export class UserTokenList extends Table<Token> {
searchEnabled(): boolean {
return true;
}
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Token>> {
return Token.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Identifier", "identifier"),
new TableColumn("User", "user"),
new TableColumn("Expires?", "expiring"),
new TableColumn("Expiry date", "expires"),
new TableColumn(""),
];
}
row(item: Token): TemplateResult[] {
return [
html`${item.identifier}`,
html`${item.user.username}`,
html`${item.expiring ? "Yes" : "No"}`,
html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : "-"}`,
html`
<ak-modal-button href="${Token.userUrl(`${item.identifier}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Token.userUrl(`${item.identifier}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-token-copy-button identifier="${item.identifier}">
${gettext("Copy Key")}
</ak-token-copy-button>
`,
];
}
}

View File

@ -0,0 +1,113 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import { TableColumn } from "../../elements/table/Table";
import { User } from "../../api/Users";
@customElement("ak-user-list")
export class UserListPage extends TablePage<User> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Users");
}
pageDescription(): string {
return "";
}
pageIcon(): string {
return gettext("pf-icon pf-icon-user");
}
@property()
order = "username";
apiEndpoint(page: number): Promise<AKResponse<User>> {
return User.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "username"),
new TableColumn("Active", "active"),
new TableColumn("Last login", "last_login"),
new TableColumn(""),
];
}
row(item: User): TemplateResult[] {
return [
html`<div>
<div>${item.username}</div>
<small>${item.name}</small>
</div>`,
html`${item.is_active ? "Yes" : "No"}`,
html`${new Date(item.last_login * 1000).toLocaleString()}`,
html`
<ak-modal-button href="${User.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-dropdown class="pf-c-dropdown">
<button class="pf-c-dropdown__toggle pf-m-primary" type="button">
<span class="pf-c-dropdown__toggle-text">${gettext(item.is_active ? "Disable" : "Enable")}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
<li>
${item.is_active ?
html`<ak-modal-button href="${User.adminUrl(`${item.pk}/disable/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Disable")}
</button>
<div slot="modal"></div>
</ak-modal-button>`:
html`<ak-modal-button href="${User.adminUrl(`${item.pk}/enable/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Enable")}
</button>
<div slot="modal"></div>
</ak-modal-button>`}
</li>
<li class="pf-c-divider" role="separator"></li>
<li>
<ak-modal-button href="${User.adminUrl(`${item.pk}/delete/`)}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
${gettext("Delete")}
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
</ul>
</ak-dropdown>
<a class="pf-c-button pf-m-tertiary" href="${User.adminUrl(`${item.pk}/reset/`)}">
${gettext("Reset Password")}
</a>
<a class="pf-c-button pf-m-tertiary" href="${`-/impersonation/${item.pk}/`}">
${gettext("Impersonate")}
</a>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${User.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -1,23 +1,32 @@
import { html } from "lit-html";
import { Route, SLUG_REGEX, ID_REGEX, UUID_REGEX } from "./elements/router/Route";
import "./pages/LibraryPage";
import "./pages/admin-overview/AdminOverviewPage";
import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/sources/SourcesListPage";
import "./pages/sources/SourceViewPage";
import "./pages/crypto/CertificateKeyPairListPage";
import "./pages/events/EventInfoPage";
import "./pages/events/EventListPage";
import "./pages/events/RuleListPage";
import "./pages/events/TransportListPage";
import "./pages/flows/FlowListPage";
import "./pages/flows/FlowViewPage";
import "./pages/events/EventListPage";
import "./pages/events/EventInfoPage";
import "./pages/events/TransportListPage";
import "./pages/events/RuleListPage";
import "./pages/groups/GroupListPage";
import "./pages/LibraryPage";
import "./pages/outposts/OutpostListPage";
import "./pages/outposts/OutpostServiceConnectionListPage";
import "./pages/policies/PolicyListPage";
import "./pages/property-mappings/PropertyMappingListPage";
import "./pages/providers/ProviderListPage";
import "./pages/providers/ProviderViewPage";
import "./pages/property-mappings/PropertyMappingListPage";
import "./pages/outposts/OutpostListPage";
import "./pages/crypto/CertificateKeyPairListPage";
import "./pages/sources/SourcesListPage";
import "./pages/sources/SourceViewPage";
import "./pages/stages/StageListPage";
import "./pages/stages/InvitationListPage";
import "./pages/stages/PromptListPage";
import "./pages/system-tasks/SystemTaskListPage";
import "./pages/tokens/TokenListPage";
import "./pages/users/UserListPage";
export const ROUTES: Route[] = [
// Prevent infinite Shell loops
@ -25,20 +34,28 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^#.*")).redirect("/library"),
new Route(new RegExp("^/library$"), html`<ak-library></ak-library>`),
new Route(new RegExp("^/administration/overview$"), html`<ak-admin-overview></ak-admin-overview>`),
new Route(new RegExp("^/providers$"), html`<ak-provider-list></ak-provider-list>`),
new Route(new RegExp(`^/providers/(?<id>${ID_REGEX})$`)).then((args) => {
new Route(new RegExp("^/administration/system-tasks$"), html`<ak-system-task-list></ak-system-task-list>`),
new Route(new RegExp("^/core/providers$"), html`<ak-provider-list></ak-provider-list>`),
new Route(new RegExp(`^/core/providers/(?<id>${ID_REGEX})$`)).then((args) => {
return html`<ak-provider-view .providerID=${parseInt(args.id, 10)}></ak-provider-view>`;
}),
new Route(new RegExp("^/applications$"), html`<ak-application-list></ak-application-list>`),
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})$`)).then((args) => {
new Route(new RegExp("^/core/applications$"), html`<ak-application-list></ak-application-list>`),
new Route(new RegExp(`^/core/applications/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-application-view .args=${args}></ak-application-view>`;
}),
new Route(new RegExp("^/sources$"), html`<ak-source-list></ak-source-list>`),
new Route(new RegExp(`^/sources/(?<slug>${SLUG_REGEX})$`)).then((args) => {
new Route(new RegExp("^/core/sources$"), html`<ak-source-list></ak-source-list>`),
new Route(new RegExp(`^/core/sources/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-source-view .args=${args}></ak-source-view>`;
}),
new Route(new RegExp("^/flows$"), html`<ak-flow-list></ak-flow-list>`),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
new Route(new RegExp("^/policy/policies$"), html`<ak-policy-list></ak-policy-list>`),
new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`),
new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`),
new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`),
new Route(new RegExp("^/flow/stages/invitations$"), html`<ak-stage-invitation-list></ak-stage-invitation-list>`),
new Route(new RegExp("^/flow/stages/prompts$"), html`<ak-stage-prompt-list></ak-stage-prompt-list>`),
new Route(new RegExp("^/flow/stages$"), html`<ak-stage-list></ak-stage-list>`),
new Route(new RegExp("^/flow/flows$"), html`<ak-flow-list></ak-flow-list>`),
new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
}),
new Route(new RegExp("^/events/log$"), html`<ak-event-list></ak-event-list>`),
@ -47,7 +64,8 @@ export const ROUTES: Route[] = [
}),
new Route(new RegExp("^/events/transports$"), html`<ak-event-transport-list></ak-event-transport-list>`),
new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`),
new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
new Route(new RegExp("^/core/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/outpost/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
new Route(new RegExp("^/outpost/service-connections$"), html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`),
new Route(new RegExp("^/crypto/certificates$"), html`<ak-crypto-certificatekeypair-list></ak-crypto-certificatekeypair-list>`),
];