Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions web/src/admin/users/UserBulkRevokeSessionsForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import "#elements/buttons/SpinnerButton/index";

import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { MessageLevel } from "#common/messages";

import { ModalButton } from "#elements/buttons/ModalButton";
import { showMessage } from "#elements/messages/MessageContainer";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";

import { CoreApi, User } from "@goauthentik/api";

import { msg, str } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";

type UserMetadata = { key: string; value: string }[];

@customElement("ak-user-bulk-revoke-sessions-table")
export class UserBulkRevokeSessionsTable extends Table<User> {
paginated = false;

@property({ attribute: false })
objects: User[] = [];

@property({ attribute: false })
metadata!: (item: User) => UserMetadata;

@state()
sessionCounts: Map<number, number> = new Map();

async apiEndpoint(): Promise<PaginatedResponse<User>> {
// Fetch session counts for each user
for (const user of this.objects) {
try {
const sessions = await new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsList({
userUsername: user.username,
});
this.sessionCounts.set(user.pk, sessions.pagination.count);
} catch {
this.sessionCounts.set(user.pk, 0);
}
}
this.requestUpdate();

return Promise.resolve({
pagination: {
count: this.objects.length,
current: 1,
totalPages: 1,
startIndex: 1,
endIndex: this.objects.length,
next: 0,
previous: 0,
},
results: this.objects,
});
}

protected override rowLabel(item: User): string | null {
return item.username || null;
}

protected get columns(): TableColumn[] {
return [[msg("Username")], [msg("Name")], [msg("Active Sessions")]];
}

row(item: User): SlottedTemplateResult[] {
const sessionCount = this.sessionCounts.get(item.pk);
return [
html`${item.username}`,
html`${item.name || msg("No name set")}`,
html`${sessionCount !== undefined ? sessionCount : html`<ak-spinner size="sm"></ak-spinner>`}`,
];
}

renderToolbarContainer(): SlottedTemplateResult {
return nothing;
}
}

@customElement("ak-user-bulk-revoke-sessions")
export class UserBulkRevokeSessionsForm extends ModalButton {
@property({ attribute: false })
users: User[] = [];

@state()
isRevoking = false;

@state()
revokedCount = 0;

async confirm(): Promise<void> {
this.isRevoking = true;
this.revokedCount = 0;

try {
for (const user of this.users) {
// Get all sessions for this user
const sessions = await new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsList({
userUsername: user.username,
pageSize: 1000, // Get all sessions
});

// Delete each session
for (const session of sessions.results) {
if (session.uuid) {
await new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
uuid: session.uuid,
});
this.revokedCount++;
}
}
}

this.onSuccess();
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
this.open = false;
} catch (e) {
this.onError(e as Error);
throw e;
} finally {
this.isRevoking = false;
}
}

onSuccess(): void {
showMessage({
message: msg(
str`Successfully revoked ${this.revokedCount} session(s) for ${this.users.length} user(s)`,
),
level: MessageLevel.success,
});
}

onError(e: Error): void {
showMessage({
message: msg(str`Failed to revoke sessions: ${e.toString()}`),
level: MessageLevel.error,
});
}

renderModalInner(): TemplateResult {
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">${msg("Revoke Sessions")}</h1>
</div>
</section>
<section class="pf-c-modal-box__body pf-m-light">
<form class="pf-c-form pf-m-horizontal">
<p class="pf-c-title">
${msg(
str`Are you sure you want to revoke all sessions for ${this.users.length} user(s)?`,
)}
</p>
<p>
${msg(
"This will force the selected users to re-authenticate on all their devices.",
)}
</p>
</form>
</section>
<section class="pf-c-modal-box__body pf-m-light">
<ak-user-bulk-revoke-sessions-table
.objects=${this.users}
.metadata=${(item: User) => {
return [
{ key: msg("Username"), value: item.username },
{ key: msg("Name"), value: item.name || "" },
];
}}
>
</ak-user-bulk-revoke-sessions-table>
</section>
<footer class="pf-c-modal-box__footer">
<ak-spinner-button
.callAction=${() => {
return this.confirm();
}}
class="pf-m-warning"
>
${msg("Revoke Sessions")} </ak-spinner-button
>&nbsp;
<ak-spinner-button
.callAction=${async () => {
this.open = false;
}}
class="pf-m-secondary"
>
${msg("Cancel")}
</ak-spinner-button>
</footer>`;
}
}

declare global {
interface HTMLElementTagNameMap {
"ak-user-bulk-revoke-sessions-table": UserBulkRevokeSessionsTable;
"ak-user-bulk-revoke-sessions": UserBulkRevokeSessionsForm;
}
}
82 changes: 44 additions & 38 deletions web/src/admin/users/UserListPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "#admin/users/ServiceAccountForm";
import "#admin/users/UserActiveForm";
import "#admin/users/UserBulkRevokeSessionsForm";
import "#admin/users/UserForm";
import "#admin/users/UserImpersonateForm";
import "#admin/users/UserPasswordForm";
Expand Down Expand Up @@ -162,45 +163,50 @@ export class UserListPage extends WithBrandConfig(
const shouldShowWarning = this.selectedElements.find((el) => {
return el.pk === currentUser?.pk || el.pk === originalUser?.pk;
});
return html`<ak-forms-delete-bulk
objectLabel=${msg("User(s)")}
.objects=${this.selectedElements}
.metadata=${(item: User) => {
return [
{ key: msg("Username"), value: item.username },
{ key: msg("ID"), value: item.pk.toString() },
{ key: msg("UID"), value: item.uid },
];
}}
.usedBy=${(item: User) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({
id: item.pk,
});
}}
.delete=${(item: User) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({
id: item.pk,
});
}}
>
${shouldShowWarning
? html`<div slot="notice" class="pf-c-form__alert">
<div class="pf-c-alert pf-m-inline pf-m-warning">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
return html`<ak-user-bulk-revoke-sessions .users=${this.selectedElements}>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-warning">
${msg("Revoke Sessions")}
</button>
</ak-user-bulk-revoke-sessions>
<ak-forms-delete-bulk
objectLabel=${msg("User(s)")}
.objects=${this.selectedElements}
.metadata=${(item: User) => {
return [
{ key: msg("Username"), value: item.username },
{ key: msg("ID"), value: item.pk.toString() },
{ key: msg("UID"), value: item.uid },
];
}}
.usedBy=${(item: User) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({
id: item.pk,
});
}}
.delete=${(item: User) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({
id: item.pk,
});
}}
>
${shouldShowWarning
? html`<div slot="notice" class="pf-c-form__alert">
<div class="pf-c-alert pf-m-inline pf-m-warning">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">
${msg(
str`Warning: You're about to delete the user you're logged in as (${shouldShowWarning.username}). Proceed at your own risk.`,
)}
</h4>
</div>
<h4 class="pf-c-alert__title">
${msg(
str`Warning: You're about to delete the user you're logged in as (${shouldShowWarning.username}). Proceed at your own risk.`,
)}
</h4>
</div>
</div>`
: nothing}
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
</div>`
: nothing}
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}

renderToolbarAfter(): TemplateResult {
Expand Down
Loading