Authentication token generation WIP (#98)

You can now generate links to join chatrooms with your current user. This can be used to create Docks in OBS for example. This could also be used to generate authentication token to join the chat from 3rd party tools.
This commit is contained in:
John Livingston
2024-06-16 19:48:02 +02:00
parent e83150cf87
commit 90afdafbd9
24 changed files with 988 additions and 205 deletions

View File

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
export const AddSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
</svg>`
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
export const RemoveSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-x-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>
</svg>`

View File

@ -15,24 +15,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { classMap } from 'lit/directives/class-map.js'
import { LivechatElement } from './livechat'
import { ptTr } from '../directives/translation'
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
const AddSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>
</svg>`
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
const RemoveSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-x-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>
</svg>`
import { AddSVG, RemoveSVG } from '../buttons'
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>

View File

@ -10,4 +10,5 @@ import './configuration-section-header'
import './tags-input'
import './image-file-input'
import './spinner'
import './token-list'
import './error'

View File

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { LivechatTokenListElement } from '../token-list'
import { html, TemplateResult } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { repeat } from 'lit/directives/repeat.js'
import { ptTr } from '../../directives/translation'
import { AddSVG, RemoveSVG } from '../../buttons'
export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
return html`<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">${ptTr(LOC_TOKEN_LABEL)}</th>
<th scope="col">${ptTr(LOC_TOKEN_JID)}</th>
<th scope="col">${ptTr(LOC_TOKEN_PASSWORD)}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
${
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
html`<tr>
<td>${
el.mode === 'select'
? html`<input
type="radio"
?selected=${el.currentSelectedToken?.id === token.id}
@click=${el.selectToken}
/>`
: ''
}</td>
<td>${token.label}</td>
<td>${token.jid}</td>
<td>${token.password}</td>
<td>
<button type="button"
class="livechat-revoke-token"
.title=${ptTr(LOC_TOKEN_ACTION_REVOKE) as any}
?disabled=${el.actionDisabled}
@click=${() => {
el.revokeToken(token).then(() => {}, () => {})
}}
>
${unsafeHTML(RemoveSVG)}
</button>
</td>
</tr>`
})
}
</tbody>
<tfoot>
<tr>
<td>
<button type="button"
class="livechat-create-token"
.title=${ptTr(LOC_TOKEN_ACTION_CREATE) as any}
?disabled=${el.actionDisabled}
@click=${() => {
el.createToken().then(() => {}, () => {})
}}
>
${unsafeHTML(AddSVG)}
</button>
</td>
</tr>
</tfoot>
</table>
</div>`
}

View File

@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { LivechatElement } from './livechat'
import { TokenListService } from '../services/token-list'
import { tplTokenList } from './templates/token-list'
import { Task } from '@lit/task'
import { LivechatToken } from 'shared/lib/types'
@customElement('livechat-token-list')
export class LivechatTokenListElement extends LivechatElement {
/**
* Indicate the mode to use:
* * list: just display tokens
* * select: select one token
*/
@property({ attribute: true })
public mode: 'select' | 'list' = 'list'
@property({ attribute: false })
public tokenList?: LivechatToken[]
@property({ attribute: false })
public currentSelectedToken?: LivechatToken
@property({ attribute: false })
public actionDisabled: boolean = false
private readonly _tokenListService: TokenListService
private readonly _asyncTaskRender: Task
constructor () {
super()
this._tokenListService = new TokenListService()
this._asyncTaskRender = this._initTask()
}
protected _initTask (): Task {
return new Task(this, {
task: async () => {
this.tokenList = await this._tokenListService.fetchTokenList()
this.actionDisabled = false
},
args: () => []
})
}
protected override render = (): unknown => {
return this._asyncTaskRender.render({
pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`,
complete: () => tplTokenList(this)
})
}
public selectToken (ev: Event, token: LivechatToken): void {
ev.preventDefault()
if (!this.tokenList?.includes(token)) { return }
this.currentSelectedToken = token
this.dispatchEvent(new CustomEvent('update', {}))
}
public async revokeToken (token: LivechatToken): Promise<void> {
this.actionDisabled = true
try {
await this._tokenListService.revokeToken(token)
this.tokenList = this.tokenList?.filter(t => t !== token) ?? []
if (this.currentSelectedToken === token) {
this.currentSelectedToken = undefined
}
this.requestUpdate('tokenList')
this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) {
this.logger.error(err)
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
} finally {
this.actionDisabled = false
}
}
public async createToken (): Promise<void> {
this.actionDisabled = true
try {
const token = await this._tokenListService.createToken(await this.ptTranslate(LOC_TOKEN_DEFAULT_LABEL))
this.tokenList ??= []
this.tokenList.push(token)
if (this.mode === 'select') {
this.currentSelectedToken = token
}
this.requestUpdate('tokenList')
this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) {
this.logger.error(err)
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
} finally {
this.actionDisabled = false
}
}
}

View File

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { getPtContext } from '../contexts/peertube'
import { getBaseRoute } from '../../../utils/uri'
import { LivechatToken } from 'shared/lib/types'
export class TokenListService {
private readonly _headers: any = {}
private readonly _apiUrl: string
constructor () {
this._headers = getPtContext().ptOptions.peertubeHelpers.getAuthHeader() ?? {}
this._headers['content-type'] = 'application/json;charset=UTF-8'
this._apiUrl = getBaseRoute(getPtContext().ptOptions) + '/api/auth/tokens'
}
public async fetchTokenList (): Promise<LivechatToken[]> {
const response = await fetch(
this._apiUrl,
{
method: 'GET',
headers: this._headers
}
)
if (!response.ok) {
throw new Error('Can\'t get livechat token list.')
}
return response.json()
}
public async createToken (label: string): Promise<LivechatToken> {
const response = await fetch(
this._apiUrl,
{
method: 'POST',
headers: this._headers,
body: JSON.stringify({
label
})
}
)
if (!response.ok) {
throw new Error('Can\'t create livechat token.')
}
return response.json()
}
public async revokeToken (token: LivechatToken): Promise<void> {
const response = await fetch(
this._apiUrl + '/' + encodeURIComponent(token.id),
{
method: 'DELETE',
headers: this._headers
}
)
if (!response.ok) {
throw new Error('Can\'t delete livechat token.')
}
return response.json()
}
}