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:
22
client/common/lib/buttons.ts
Normal file
22
client/common/lib/buttons.ts
Normal 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>`
|
@ -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>
|
||||
|
||||
|
@ -10,4 +10,5 @@ import './configuration-section-header'
|
||||
import './tags-input'
|
||||
import './image-file-input'
|
||||
import './spinner'
|
||||
import './token-list'
|
||||
import './error'
|
||||
|
74
client/common/lib/elements/templates/token-list.ts
Normal file
74
client/common/lib/elements/templates/token-list.ts
Normal 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>`
|
||||
}
|
102
client/common/lib/elements/token-list.ts
Normal file
102
client/common/lib/elements/token-list.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
69
client/common/lib/services/token-list.ts
Normal file
69
client/common/lib/services/token-list.ts
Normal 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()
|
||||
}
|
||||
}
|
@ -10,8 +10,9 @@ import { LivechatElement } from '../../lib/elements/livechat'
|
||||
import { tplShareChatCopy, tplShareChatTips, tplShareChatTabs, tplShareChatOptions } from './templates/share-chat'
|
||||
import { isAutoColorsAvailable } from 'shared/lib/autocolors'
|
||||
import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
|
||||
import { isAnonymousUser } from '../../../utils/user'
|
||||
|
||||
const validTabNames = ['peertube', 'embed', 'xmpp'] as const
|
||||
const validTabNames = ['peertube', 'embed', 'dock', 'xmpp'] as const
|
||||
|
||||
type ValidTabNames = typeof validTabNames[number]
|
||||
|
||||
@ -60,6 +61,12 @@ export class ShareChatElement extends LivechatElement {
|
||||
@property({ attribute: false })
|
||||
public xmppUriEnabled: boolean = false
|
||||
|
||||
/**
|
||||
* Should we render the Dock tab?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public dockEnabled: boolean = false
|
||||
|
||||
/**
|
||||
* Can we use autocolors?
|
||||
*/
|
||||
@ -100,6 +107,10 @@ export class ShareChatElement extends LivechatElement {
|
||||
super.firstUpdated(changedProperties)
|
||||
const settings = this._settings
|
||||
this.xmppUriEnabled = !!settings['prosody-room-allow-s2s']
|
||||
// Note: for dockEnabled, we check:
|
||||
// * that the user is logged in
|
||||
// * that the video is local (for remote video, tests case are too complicated, and it's not the main use case, so…)
|
||||
this.dockEnabled = !isAnonymousUser(this.ptContext.ptOptions) && this._video.isLocal
|
||||
this.autocolorsAvailable = isAutoColorsAvailable(settings['converse-theme'])
|
||||
|
||||
this._restorePreviousState()
|
||||
@ -154,6 +165,9 @@ export class ShareChatElement extends LivechatElement {
|
||||
if (!this.xmppUriEnabled && this.currentTab === 'xmpp') {
|
||||
this.currentTab = 'peertube'
|
||||
}
|
||||
if (!this.dockEnabled && this.currentTab === 'dock') {
|
||||
this.currentTab = 'peertube'
|
||||
}
|
||||
}
|
||||
|
||||
protected _saveCurrentState (): void {
|
||||
@ -177,6 +191,7 @@ export class ShareChatElement extends LivechatElement {
|
||||
switch (this.currentTab) {
|
||||
case 'peertube': return this._computeUrlPeertube()
|
||||
case 'embed': return this._computeUrlEmbed()
|
||||
case 'dock': return this._computeUrlDock()
|
||||
case 'xmpp': return this._computeUrlXMPP()
|
||||
default:
|
||||
return {
|
||||
@ -206,6 +221,32 @@ export class ShareChatElement extends LivechatElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected _computeUrlDock (): ComputedUrl {
|
||||
return {
|
||||
shareString: '',
|
||||
openUrl: undefined
|
||||
}
|
||||
// const uriOptions: UriOptions = {
|
||||
// ignoreAutoColors: true,
|
||||
// permanent: true
|
||||
// }
|
||||
|
||||
// // Note: for the "embed" case, the url is always the same as the iframe.
|
||||
// // So we use getIframeUri to compte, and just change the finale result if we really want the iframe.
|
||||
// const url = getIframeUri(this.ptContext.ptOptions, this._settings, this._video, uriOptions)
|
||||
// if (!url) {
|
||||
// return {
|
||||
// shareString: '',
|
||||
// openUrl: undefined
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// shareString: url,
|
||||
// openUrl: url
|
||||
// }
|
||||
}
|
||||
|
||||
protected _computeUrlEmbed (): ComputedUrl {
|
||||
const uriOptions: UriOptions = {
|
||||
ignoreAutoColors: this.autocolorsAvailable ? !this.embedAutocolors : true,
|
||||
|
@ -22,7 +22,11 @@ export function tplShareChatCopy (el: ShareChatElement): TemplateResult {
|
||||
input.setSelectionRange(0, 99999) /* For mobile devices */
|
||||
}}
|
||||
/>
|
||||
<button type="button" class="btn btn-outline-secondary text-uppercase" @click=${el.copyUrl}>
|
||||
<button
|
||||
type="button" class="btn btn-outline-secondary text-uppercase"
|
||||
@click=${el.copyUrl}
|
||||
?disabled=${!computedUrl.shareString}
|
||||
>
|
||||
${ptTr(LOC_COPY)}
|
||||
</button>
|
||||
<button
|
||||
@ -60,6 +64,11 @@ export function tplShareChatTabs (el: ShareChatElement): TemplateResult {
|
||||
return html`
|
||||
${_tplShareChatTab(el, 'peertube', LOC_WEB)}
|
||||
${_tplShareChatTab(el, 'embed', LOC_SHARE_CHAT_EMBED)}
|
||||
${
|
||||
el.dockEnabled
|
||||
? _tplShareChatTab(el, 'dock', LOC_SHARE_CHAT_DOCK)
|
||||
: ''
|
||||
}
|
||||
${
|
||||
el.xmppUriEnabled
|
||||
? _tplShareChatTab(el, 'xmpp', LOC_CONNECT_USING_XMPP)
|
||||
@ -79,6 +88,10 @@ export function tplShareChatTips (el: ShareChatElement): TemplateResult {
|
||||
label = LOC_TIPS_FOR_STREAMERS
|
||||
tips = html`<livechat-help-button .page=${'documentation/user/obs'}></livechat-help-button>`
|
||||
break
|
||||
case 'dock':
|
||||
label = LOC_SHARE_CHAT_DOCK_TIPS
|
||||
tips = html`<livechat-help-button .page=${'documentation/user/obs'}></livechat-help-button>`
|
||||
break
|
||||
case 'xmpp':
|
||||
label = LOC_CONNECT_USING_XMPP_HELP
|
||||
break
|
||||
@ -154,6 +167,10 @@ function _tplShareChatEmbedOptions (el: ShareChatElement): TemplateResult {
|
||||
`
|
||||
}
|
||||
|
||||
function _tplShareChatDockOptions (_el: ShareChatElement): TemplateResult {
|
||||
return html`<livechat-token-list mode="select"></livechat-token-list>`
|
||||
}
|
||||
|
||||
function _tplShareChatXMPPOptions (_el: ShareChatElement): TemplateResult {
|
||||
return html``
|
||||
}
|
||||
@ -167,6 +184,9 @@ export function tplShareChatOptions (el: ShareChatElement): TemplateResult {
|
||||
case 'embed':
|
||||
tpl = _tplShareChatEmbedOptions(el)
|
||||
break
|
||||
case 'dock':
|
||||
tpl = _tplShareChatDockOptions(el)
|
||||
break
|
||||
case 'xmpp':
|
||||
tpl = _tplShareChatXMPPOptions(el)
|
||||
break
|
||||
|
Reference in New Issue
Block a user