core: user paths (#3085)
* init Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add user_path_template Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add to sources and flow Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add outposts & api Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * dark theme for treeview Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add search Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add docs and tests for validation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add to user write stage Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add web ui Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: improve error handling Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { CoreApi, SessionUser } from "@goauthentik/api";
|
||||
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
|
||||
import { activateLocale } from "../interfaces/locale";
|
||||
import { DEFAULT_CONFIG } from "./Config";
|
||||
|
||||
@ -21,7 +21,7 @@ export function me(): Promise<SessionUser> {
|
||||
activateLocale(locale);
|
||||
}
|
||||
return user;
|
||||
}).catch((ex) => {
|
||||
}).catch((ex: ResponseError) => {
|
||||
const defaultUser: SessionUser = {
|
||||
user: {
|
||||
pk: -1,
|
||||
|
@ -277,6 +277,12 @@ html > form > input {
|
||||
.pf-c-select__menu-item:hover {
|
||||
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
.pf-c-select__menu-wrapper:focus-within,
|
||||
.pf-c-select__menu-wrapper.pf-m-focus,
|
||||
.pf-c-select__menu-item:focus,
|
||||
.pf-c-select__menu-item.pf-m-focus {
|
||||
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
.pf-c-button.pf-m-plain:hover {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
@ -395,6 +401,14 @@ html > form > input {
|
||||
.pf-c-wizard__nav-link::before {
|
||||
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
|
||||
}
|
||||
/* tree view */
|
||||
.pf-c-tree-view__node:focus {
|
||||
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
.pf-c-tree-view__content:hover,
|
||||
.pf-c-tree-view__content:focus-within {
|
||||
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-data-list__item {
|
||||
|
206
web/src/elements/TreeView.ts
Normal file
206
web/src/elements/TreeView.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import AKGlobal from "../authentik.css";
|
||||
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { EVENT_REFRESH } from "../constants";
|
||||
import { setURLParams } from "./router/RouteMatch";
|
||||
|
||||
export interface TreeViewItem {
|
||||
id: string;
|
||||
label: string;
|
||||
childItems: TreeViewItem[];
|
||||
parent?: TreeViewItem;
|
||||
level: number;
|
||||
}
|
||||
|
||||
@customElement("ak-treeview-node")
|
||||
export class TreeViewNode extends LitElement {
|
||||
@property({ attribute: false })
|
||||
item?: TreeViewItem;
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
host?: TreeView;
|
||||
|
||||
@property()
|
||||
path = "";
|
||||
|
||||
@property()
|
||||
separator = "";
|
||||
|
||||
get openable(): boolean {
|
||||
return (this.item?.childItems || []).length > 0;
|
||||
}
|
||||
|
||||
get fullPath(): string {
|
||||
const pathItems = [];
|
||||
let item = this.item;
|
||||
while (item) {
|
||||
pathItems.push(item.id);
|
||||
item = item.parent;
|
||||
}
|
||||
return pathItems.reverse().join(this.separator);
|
||||
}
|
||||
|
||||
protected createRenderRoot(): Element {
|
||||
return this;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
const pathSegments = this.path.split(this.separator);
|
||||
const level = this.item?.level || 0;
|
||||
// Ignore the last item as that shouldn't be expanded
|
||||
pathSegments.pop();
|
||||
if (pathSegments[level] == this.item?.id) {
|
||||
this.open = true;
|
||||
}
|
||||
if (this.path === this.fullPath && this.host !== undefined) {
|
||||
this.host.activeNode = this;
|
||||
}
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
|
||||
return html`
|
||||
<li
|
||||
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
||||
role="treeitem"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="pf-c-tree-view__content">
|
||||
<button
|
||||
class="pf-c-tree-view__node ${this.host?.activeNode === this
|
||||
? "pf-m-current"
|
||||
: ""}"
|
||||
@click=${() => {
|
||||
if (this.host) {
|
||||
this.host.activeNode = this;
|
||||
}
|
||||
setURLParams({ path: this.fullPath });
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class="pf-c-tree-view__node-container">
|
||||
${this.openable
|
||||
? html` <button
|
||||
class="pf-c-tree-view__node-toggle"
|
||||
@click=${(e: Event) => {
|
||||
if (this.openable) {
|
||||
this.open = !this.open;
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-tree-view__node-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>`
|
||||
: html``}
|
||||
<span class="pf-c-tree-view__node-icon">
|
||||
<i
|
||||
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
<span class="pf-c-tree-view__node-text">${this.item?.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="pf-c-tree-view__list" role="group" ?hidden=${!shouldRenderChildren}>
|
||||
${this.item?.childItems.map((item) => {
|
||||
return html`<ak-treeview-node
|
||||
.item=${item}
|
||||
path=${this.path}
|
||||
separator=${this.separator}
|
||||
.host=${this.host}
|
||||
></ak-treeview-node>`;
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-treeview")
|
||||
export class TreeView extends LitElement {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFTreeView, AKGlobal];
|
||||
}
|
||||
|
||||
@property({ type: Array })
|
||||
items: string[] = [];
|
||||
|
||||
@property()
|
||||
path = "";
|
||||
|
||||
@state()
|
||||
activeNode?: TreeViewNode;
|
||||
|
||||
separator = "/";
|
||||
|
||||
createNode(path: string[], tree: TreeViewItem[], level: number): TreeViewItem {
|
||||
const id = path.shift();
|
||||
const idx = tree.findIndex((e: TreeViewItem) => {
|
||||
return e.id == id;
|
||||
});
|
||||
if (idx < 0) {
|
||||
const item: TreeViewItem = {
|
||||
id: id || "",
|
||||
label: id || "",
|
||||
childItems: [],
|
||||
level: level,
|
||||
};
|
||||
tree.push(item);
|
||||
if (path.length !== 0) {
|
||||
const child = this.createNode(path, tree[tree.length - 1].childItems, level + 1);
|
||||
child.parent = item;
|
||||
}
|
||||
return item;
|
||||
} else {
|
||||
return this.createNode(path, tree[idx].childItems, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
parse(data: string[]): TreeViewItem[] {
|
||||
const tree: TreeViewItem[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const path: string = data[i];
|
||||
const split: string[] = path.split(this.separator);
|
||||
this.createNode(split, tree, 0);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const result = this.parse(this.items);
|
||||
return html`<div class="pf-c-tree-view pf-m-guides">
|
||||
<ul class="pf-c-tree-view__list" role="tree">
|
||||
<!-- @ts-ignore -->
|
||||
<ak-treeview-node
|
||||
.item=${{
|
||||
id: "",
|
||||
label: t`Root`,
|
||||
childItems: result,
|
||||
level: -1,
|
||||
} as TreeViewItem}
|
||||
path=${this.path}
|
||||
?open=${true}
|
||||
separator=${this.separator}
|
||||
.host=${this}
|
||||
></ak-treeview-node>
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { CoreApi } from "@goauthentik/api";
|
||||
import { CoreApi, ResponseError } from "@goauthentik/api";
|
||||
|
||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||
import { ERROR_CLASS, SECONDARY_CLASS, SUCCESS_CLASS } from "../../constants";
|
||||
@ -37,15 +37,15 @@ export class TokenCopyButton extends ActionButton {
|
||||
this.buttonClass = SUCCESS_CLASS;
|
||||
return token.key;
|
||||
})
|
||||
.catch((err: Error | Response | undefined) => {
|
||||
.catch((err: Error | ResponseError | undefined) => {
|
||||
this.buttonClass = ERROR_CLASS;
|
||||
if (err instanceof Error) {
|
||||
if (!(err instanceof ResponseError)) {
|
||||
setTimeout(() => {
|
||||
this.buttonClass = SECONDARY_CLASS;
|
||||
}, 1500);
|
||||
throw err;
|
||||
}
|
||||
return err?.json().then((errResp) => {
|
||||
return err.response.json().then((errResp) => {
|
||||
setTimeout(() => {
|
||||
this.buttonClass = SECONDARY_CLASS;
|
||||
}, 1500);
|
||||
@ -92,15 +92,15 @@ export class TokenCopyButton extends ActionButton {
|
||||
this.setDone(SUCCESS_CLASS);
|
||||
});
|
||||
})
|
||||
.catch((err: Response | Error) => {
|
||||
if (err instanceof Error) {
|
||||
.catch((err: ResponseError | Error) => {
|
||||
if (!(err instanceof ResponseError)) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
return err?.json().then((errResp) => {
|
||||
return err.response.json().then((errResp) => {
|
||||
this.setDone(ERROR_CLASS);
|
||||
throw new Error(errResp["detail"]);
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { ValidationError } from "@goauthentik/api";
|
||||
import { ResponseError, ValidationError } from "@goauthentik/api";
|
||||
|
||||
import { EVENT_REFRESH } from "../../constants";
|
||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||
@ -209,13 +209,13 @@ export class Form<T> extends LitElement {
|
||||
);
|
||||
return r;
|
||||
})
|
||||
.catch(async (ex: Response | Error) => {
|
||||
if (ex instanceof Error) {
|
||||
.catch(async (ex: Error | ResponseError) => {
|
||||
if (!(ex instanceof ResponseError)) {
|
||||
throw ex;
|
||||
}
|
||||
let msg = ex.statusText;
|
||||
if (ex.status > 399 && ex.status < 500) {
|
||||
const errorMessage: ValidationError = await ex.json();
|
||||
let msg = ex.response.statusText;
|
||||
if (ex.response.status > 399 && ex.response.status < 500) {
|
||||
const errorMessage: ValidationError = await ex.response.json();
|
||||
if (!errorMessage) return errorMessage;
|
||||
if (errorMessage instanceof Error) {
|
||||
throw errorMessage;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
FlowsApi,
|
||||
LayoutEnum,
|
||||
RedirectChallenge,
|
||||
ResponseError,
|
||||
ShellChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@ -193,7 +194,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
.catch((e: Error | ResponseError) => {
|
||||
this.errorMessage(e);
|
||||
return false;
|
||||
})
|
||||
@ -226,7 +227,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
this.setBackground(this.challenge.flowInfo.background);
|
||||
}
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
.catch((e: Error | ResponseError) => {
|
||||
// Catch JSON or Update errors
|
||||
this.errorMessage(e);
|
||||
})
|
||||
@ -235,9 +236,11 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
});
|
||||
}
|
||||
|
||||
async errorMessage(error: Error | Response): Promise<void> {
|
||||
async errorMessage(error: Error | ResponseError): Promise<void> {
|
||||
let body = "";
|
||||
if (error instanceof Error) {
|
||||
if (error instanceof ResponseError) {
|
||||
body = await error.response.text();
|
||||
} else if (error instanceof Error) {
|
||||
body = error.message;
|
||||
}
|
||||
this.challenge = {
|
||||
|
@ -15,6 +15,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import {
|
||||
PlexAuthenticationChallenge,
|
||||
PlexAuthenticationChallengeResponseRequest,
|
||||
ResponseError,
|
||||
} from "@goauthentik/api";
|
||||
import { SourcesApi } from "@goauthentik/api";
|
||||
|
||||
@ -48,8 +49,8 @@ export class PlexLoginInit extends BaseStage<
|
||||
.then((r) => {
|
||||
window.location.assign(r.to);
|
||||
})
|
||||
.catch((r: Response) => {
|
||||
r.json().then((body: { detail: string }) => {
|
||||
.catch((r: ResponseError) => {
|
||||
r.response.json().then((body: { detail: string }) => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: body.detail,
|
||||
|
@ -12,7 +12,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { Flow, FlowsApi } from "@goauthentik/api";
|
||||
import { Flow, FlowsApi, ResponseError } from "@goauthentik/api";
|
||||
|
||||
import { AndNext, DEFAULT_CONFIG } from "../../api/Config";
|
||||
import "../../elements/PageHeader";
|
||||
@ -164,10 +164,13 @@ export class FlowViewPage extends LitElement {
|
||||
)}`;
|
||||
window.open(finalURL, "_blank");
|
||||
})
|
||||
.catch((exc: Response) => {
|
||||
.catch((exc: ResponseError) => {
|
||||
// This request can return a HTTP 400 when a flow
|
||||
// is not applicable.
|
||||
window.open(exc.url, "_blank");
|
||||
window.open(
|
||||
exc.response.url,
|
||||
"_blank",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -209,7 +209,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "username",
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
|
@ -7,7 +7,9 @@ import { until } from "lit/directives/until.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
Group,
|
||||
LDAPSource,
|
||||
LDAPSourceRequest,
|
||||
PropertymappingsApi,
|
||||
@ -15,6 +17,7 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import "../../../elements/SearchSelect";
|
||||
import "../../../elements/forms/FormGroup";
|
||||
import "../../../elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||
@ -301,31 +304,49 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
||||
<span slot="header"> ${t`Additional settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${t`Group`} name="syncParentGroup">
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=""
|
||||
?selected=${this.instance?.syncParentGroup === undefined}
|
||||
>
|
||||
---------
|
||||
</option>
|
||||
${until(
|
||||
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
|
||||
return groups.results.map((group) => {
|
||||
return html`<option
|
||||
value=${ifDefined(group.pk)}
|
||||
?selected=${this.instance?.syncParentGroup === group.pk}
|
||||
>
|
||||
${group.name}
|
||||
</option>`;
|
||||
});
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
<!-- @ts-ignore -->
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||
args,
|
||||
);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group ? group.pk : undefined;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === this.instance?.syncParentGroup;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Parent group for all the groups imported from LDAP.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.userPathTemplate,
|
||||
"goauthentik.io/sources/%(slug)s",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Addition User DN`}
|
||||
name="additionalUserDn"
|
||||
|
@ -268,6 +268,19 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.userPathTemplate,
|
||||
"goauthentik.io/sources/%(slug)s",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
@ -215,6 +215,19 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.userPathTemplate,
|
||||
"goauthentik.io/sources/%(slug)s",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
@ -232,6 +232,19 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.userPathTemplate,
|
||||
"goauthentik.io/sources/%(slug)s",
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Delete temporary users after`}
|
||||
?required=${true}
|
||||
|
@ -3,11 +3,11 @@ import { t } from "@lingui/macro";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import { CoreApi, StagesApi, UserWriteStage } from "@goauthentik/api";
|
||||
import { CoreApi, CoreGroupsListRequest, Group, StagesApi, UserWriteStage } from "@goauthentik/api";
|
||||
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import "../../../elements/SearchSelect";
|
||||
import "../../../elements/forms/FormGroup";
|
||||
import "../../../elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||
@ -74,29 +74,44 @@ export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
|
||||
${t`Mark newly created users as inactive.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="userPathTemplate">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.userPathTemplate, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Path new users will be created under.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Group`} name="createUsersGroup">
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=""
|
||||
?selected=${this.instance?.createUsersGroup === undefined}
|
||||
>
|
||||
---------
|
||||
</option>
|
||||
${until(
|
||||
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
|
||||
return groups.results.map((group) => {
|
||||
return html`<option
|
||||
value=${ifDefined(group.pk)}
|
||||
?selected=${this.instance?.createUsersGroup ===
|
||||
group.pk}
|
||||
>
|
||||
${group.name}
|
||||
</option>`;
|
||||
});
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
<!-- @ts-ignore -->
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||
args,
|
||||
);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group ? group.pk : undefined;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === this.instance?.createUsersGroup;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Newly created users are added to this group, if a group is selected.`}
|
||||
</p>
|
||||
|
@ -7,7 +7,7 @@ import { until } from "lit/directives/until.js";
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
|
||||
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
|
||||
|
||||
import { AKResponse } from "../../api/Client";
|
||||
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
||||
@ -244,8 +244,8 @@ export class RelatedUserList extends Table<User> {
|
||||
description: rec.link,
|
||||
});
|
||||
})
|
||||
.catch((ex: Response) => {
|
||||
ex.json().then(() => {
|
||||
.catch((ex: ResponseError) => {
|
||||
ex.response.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`No recovery flow is configured.`,
|
||||
|
@ -69,6 +69,14 @@ export class UserForm extends ModelForm<User, number> {
|
||||
${t`User's primary identifier. 150 characters or fewer.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Path`} ?required=${true} name="path">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.path, "users")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Name`} name="name">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -4,10 +4,12 @@ import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import AKGlobal from "../../authentik.css";
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
|
||||
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
|
||||
|
||||
import { AKResponse } from "../../api/Client";
|
||||
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
||||
@ -15,12 +17,13 @@ import { me } from "../../api/Users";
|
||||
import { uiConfig } from "../../common/config";
|
||||
import { PFColor } from "../../elements/Label";
|
||||
import { PFSize } from "../../elements/Spinner";
|
||||
import "../../elements/TreeView";
|
||||
import "../../elements/buttons/ActionButton";
|
||||
import "../../elements/forms/DeleteBulkForm";
|
||||
import "../../elements/forms/ModalForm";
|
||||
import { MessageLevel } from "../../elements/messages/Message";
|
||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||
import { getURLParam, updateURLParams } from "../../elements/router/RouteMatch";
|
||||
import { getURLParam } from "../../elements/router/RouteMatch";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
import { TablePage } from "../../elements/table/TablePage";
|
||||
import { first } from "../../utils";
|
||||
@ -51,11 +54,11 @@ export class UserListPage extends TablePage<User> {
|
||||
@property()
|
||||
order = "last_login";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
||||
@property()
|
||||
path = getURLParam<string>("path", "/");
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFDescriptionList, PFAlert);
|
||||
return super.styles.concat(PFDescriptionList, PFCard, PFAlert, AKGlobal);
|
||||
}
|
||||
|
||||
async apiEndpoint(page: number): Promise<AKResponse<User>> {
|
||||
@ -64,11 +67,7 @@ export class UserListPage extends TablePage<User> {
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
attributes: this.hideServiceAccounts
|
||||
? JSON.stringify({
|
||||
"goauthentik.io/user/service-account__isnull": true,
|
||||
})
|
||||
: undefined,
|
||||
pathStartswith: getURLParam("path", ""),
|
||||
});
|
||||
}
|
||||
|
||||
@ -251,8 +250,8 @@ export class UserListPage extends TablePage<User> {
|
||||
description: rec.link,
|
||||
});
|
||||
})
|
||||
.catch((ex: Response) => {
|
||||
ex.json().then(() => {
|
||||
.catch((ex: ResponseError) => {
|
||||
ex.response.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`No recovery flow is configured.`,
|
||||
@ -320,33 +319,25 @@ export class UserListPage extends TablePage<User> {
|
||||
`;
|
||||
}
|
||||
|
||||
renderToolbarAfter(): TemplateResult {
|
||||
return html`
|
||||
<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<div class="pf-c-input-group">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
class="pf-c-check__input"
|
||||
type="checkbox"
|
||||
id="hide-service-accounts"
|
||||
name="hide-service-accounts"
|
||||
?checked=${this.hideServiceAccounts}
|
||||
@change=${() => {
|
||||
this.hideServiceAccounts = !this.hideServiceAccounts;
|
||||
this.page = 1;
|
||||
this.fetch();
|
||||
updateURLParams({
|
||||
hideServiceAccounts: this.hideServiceAccounts,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label class="pf-c-check__label" for="hide-service-accounts">
|
||||
${t`Hide service-accounts`}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
renderSidebarBefore(): TemplateResult {
|
||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">${t`User folders`}</div>
|
||||
<div class="pf-c-card__body">
|
||||
${until(
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersPathsRetrieve({
|
||||
search: this.search,
|
||||
})
|
||||
.then((paths) => {
|
||||
return html`<ak-treeview
|
||||
.items=${paths.paths}
|
||||
path=${this.path}
|
||||
></ak-treeview>`;
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
FlowChallengeResponseRequest,
|
||||
FlowsApi,
|
||||
RedirectChallenge,
|
||||
ResponseError,
|
||||
ShellChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@ -80,7 +81,7 @@ export class UserSettingsFlowExecutor extends LitElement implements StageHost {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
.catch((e: Error | ResponseError) => {
|
||||
this.errorMessage(e);
|
||||
return false;
|
||||
})
|
||||
|
Reference in New Issue
Block a user