blob: 0978b334269c028a4f6ac8fc9ee266a571452257 [file] [log] [blame]
/**
* @license
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {customElement, property, query, state} from 'lit/decorators.js';
import {css, html, CSSResult, LitElement} from 'lit';
import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
import {Timestamp} from '@gerritcodereview/typescript-api/rest-api';
export interface BindValueChangeEventDetail {
value: string | undefined;
}
export type BindValueChangeEvent = CustomEvent<BindValueChangeEventDetail>;
export interface TokenInfo {
id: string;
token?: string;
expiration?: Timestamp;
}
//TODO(Thomas): Remove after updated Typescript API was released with Gerrit 3.13
export declare interface ServerInfo {
auth: AuthInfo;
}
export declare interface AuthInfo {
max_token_lifetime?: string;
}
@customElement('gr-serviceuser-tokens')
export class GrServiceUserTokens extends LitElement {
@property()
pluginRestApi!: RestPluginApi;
@property({type: String})
serviceUserId?: string; @query('#generatedAuthTokenModal')
@query('#generatedAuthTokenModal')
generatedAuthTokenModal?: HTMLDialogElement;
@state()
username?: string;
@state()
generatedAuthToken?: TokenInfo;
@state()
status?: string;
@state()
maxLifetime: string = 'unlimited';
@property({type: Array})
tokens: TokenInfo[] = [];
@property({type: String})
newTokenId = '';
@property({type: String})
newLifetime = '';
@query('#generateButton') generateButton!: HTMLButtonElement;
@query('#newToken') tokenInput!: HTMLInputElement;
@query('#lifetime') tokenLifetime!: HTMLInputElement;
async loadData(pluginRestApi: RestPluginApi) {
this.pluginRestApi = pluginRestApi;
this.serviceUserId = this.baseURI.split('/').pop();
await pluginRestApi
.get<ServerInfo>('/a/config/server/info')
.then(config => {
this.maxLifetime = config?.auth?.max_token_lifetime || 'unlimited';
});
await this.fetchTokens();
}
private fetchTokens() {
this.pluginRestApi
.get<TokenInfo[]>(`/a/config/server/serviceuser~serviceusers/${this.serviceUserId}/tokens`)
.then(tokens => {
this.tokens = tokens;
});
}
static override get styles() {
return [
window.Gerrit.styles.form as CSSResult,
window.Gerrit.styles.modal as CSSResult,
css`
.token {
font-family: var(--monospace-font-family);
font-size: var(--font-size-mono);
line-height: var(--line-height-mono);
}
#generatedAuthTokenModal {
padding: var(--spacing-xxl);
width: 50em;
}
#generatedAuthTokenDisplay {
margin: var(--spacing-l) 0;
}
#generatedAuthTokenDisplay .title {
width: unset;
}
#generatedAuthTokenDisplay .value {
font-family: var(--monospace-font-family);
font-size: var(--font-size-mono);
line-height: var(--line-height-mono);
}
#authTokenWarning {
font-style: italic;
text-align: center;
}
#existing {
margin-top: var(--spacing-l);
margin-bottom: var(--spacing-l);
}
#existing .idColumn {
min-width: 15em;
width: auto;
}
.closeButton {
bottom: 2em;
position: absolute;
right: 2em;
}
.expired {
color: var(--negative-red-text-color);
}
.lifeTimeInput {
min-width: 23em;
}
`,
];
}
override render() {
return html`
<div class="gr-form-styles">
<fieldset id="existing">
<table>
<thead>
<tr>
<th class="idColumn">ID</th>
<th class="expirationColumn">Expiration Date</th>
<th></th>
</tr>
</thead>
<tbody>
${this.tokens.map(tokenInfo => this.renderToken(tokenInfo))}
</tbody>
<tfoot>
${this.renderFooterRow()}
</tfoot>
</table>
</fieldset>
</div>
<dialog
tabindex="-1"
id="generatedAuthTokenModal"
@closed=${this.generatedAuthTokenModalClosed}
>
<div class="gr-form-styles">
<section id="generatedAuthTokenDisplay">
<span class="title">New Token:</span>
<span class="value"
>${this.status || this.generatedAuthToken?.token}</span
>
<gr-copy-clipboard
hasTooltip=""
buttonTitle="Copy token to clipboard"
hideInput=""
.text=${this.status ? '' : this.generatedAuthToken?.token}
>
</gr-copy-clipboard>
</section>
<section
id="authTokenWarning"
?hidden=${!this.generatedAuthToken?.expiration}
>
This token will be valid until &nbsp;
<gr-date-formatter
showDateAndTime
withTooltip
.dateStr=${this.generatedAuthToken?.expiration}
></gr-date-formatter>
.
</section>
<section id="authTokenWarning">
This token will not be displayed again.<br />
If you lose it, you will need to generate a new one.
</section>
<gr-button link="" class="closeButton" @click=${this.closeModal}
>Close</gr-button
>
</div>
</dialog>`;
}
private renderToken(tokenInfo: TokenInfo) {
return html` <tr class=${this.isTokenExpired(tokenInfo) ? 'expired' : ''}>
<td class="idColumn">${tokenInfo.id}</td>
<td class="expirationColumn">
<gr-date-formatter
withTooltip
showDateAndTime
dateFormat="STD"
.dateStr=${tokenInfo.expiration}
></gr-date-formatter>
</td>
<td>
<gr-button
id="deleteButton"
@click=${() => this.handleDeleteTap(tokenInfo.id)}
>Delete</gr-button
>
</td>
</tr>`;
}
private renderFooterRow() {
return html`
<tr>
<th style="vertical-align: top;">
<iron-input
id="newToken"
.bindValue=${this.newTokenId}
@bind-value-changed=${(e: BindValueChangeEvent) => {
this.newTokenId = e.detail.value ?? '';
}}
>
<input
is="iron-input"
placeholder="New Token ID"
@keydown=${this.handleInputKeydown}
/>
</iron-input>
</th>
<th style="vertical-align: top;">
<iron-input
.bindValue=${this.newLifetime}
@bind-value-changed=${(e: BindValueChangeEvent) => {
this.newLifetime = e.detail.value ?? '';
}}
>
<input
class="lifeTimeInput"
is="iron-input"
placeholder="Lifetime (e.g. 30d)"
@keydown=${this.handleInputKeydown}
/>
</iron-input></br>
(Max. allowed lifetime: ${this.formatDuration(this.maxLifetime)})
</th>
<th>
<gr-button
id="generateButton"
link=""
?disabled=${!this.newTokenId.length}
@click=${this.handleGenerateTap}
>Generate</gr-button
>
</th>
</tr>
`;
}
private formatDuration(durationMinutes: string) {
if (!durationMinutes) return '';
if (durationMinutes === 'unlimited') return 'unlimited';
let minutes = parseInt(durationMinutes, 10);
let hours = Math.floor(minutes / 60);
minutes = minutes % 60;
let days = Math.floor(hours / 24);
hours = hours % 24;
const years = Math.floor(days / 365);
days = days % 365;
let formatted = '';
if (years) formatted += `${years}y `;
if (days) formatted += `${days}d `;
if (hours) formatted += `${hours}h `;
if (minutes) formatted += `${minutes}m`;
return formatted;
}
private isTokenExpired(tokenInfo: TokenInfo) {
if (!tokenInfo.expiration) return false;
return new Date(tokenInfo.expiration.replace(' ', 'T') + 'Z') < new Date();
}
private handleInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.stopPropagation();
this.handleGenerateTap();
}
}
private handleGenerateTap() {
this.generateButton.disabled = true;
this.status = 'Generating...';
this.generatedAuthTokenModal?.showModal();
this.pluginRestApi
.put<TokenInfo>(
`/a/config/server/serviceuser~serviceusers/${this.serviceUserId}/tokens/${this.newTokenId}`,
{
id: this.newTokenId,
lifetime: this.newLifetime,
},
)
.catch(err => {
this.closeModal();
this.dispatchEvent(
new CustomEvent('show-error', {
detail: {message: err},
composed: true,
bubbles: true,
})
);
})
.then(newToken => {
if (newToken) {
this.generatedAuthToken = newToken;
this.status = undefined;
this.fetchTokens();
this.tokenInput.value = '';
this.tokenLifetime.value = '';
} else {
this.status = 'Failed to generate';
}
})
.finally(() => {
this.generateButton.disabled = false;
});
}
private handleDeleteTap(id: string) {
this.pluginRestApi
.delete(
`/a/config/server/serviceuser~serviceusers/${this.serviceUserId}/tokens/${id}`)
.catch(err => {
this.dispatchEvent(
new CustomEvent('show-error', {
detail: {message: err},
composed: true,
bubbles: true,
})
);
})
.then(() => {
this.fetchTokens();
});
}
private generatedAuthTokenModalClosed() {
this.status = undefined;
this.generatedAuthToken = undefined;
}
private closeModal() {
this.generatedAuthTokenModal?.close();
}
}