feat(xo-web/user): user tokens management through XO interface (#6276)
This commit is contained in:
parent
87f1f208c3
commit
c7df11cc6f
@ -9,6 +9,7 @@
|
||||
|
||||
- [Backup] Merge delta backups without copying data when using VHD directories on NFS/SMB/local remote(https://github.com/vatesfr/xen-orchestra/pull/6271))
|
||||
- [Proxies] Ability to copy the proxy access URL (PR [#6287](https://github.com/vatesfr/xen-orchestra/pull/6287))
|
||||
- [User] User tokens management through XO interface (PR [#6276](https://github.com/vatesfr/xen-orchestra/pull/6276))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -25,8 +25,8 @@ create.params = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function delete_({ token: id }) {
|
||||
await this.deleteAuthenticationToken(id)
|
||||
async function delete_({ pattern, tokens }) {
|
||||
await this.deleteAuthenticationTokens({ filter: pattern ?? { id: { __or: tokens } } })
|
||||
}
|
||||
|
||||
export { delete_ as delete }
|
||||
@ -34,26 +34,8 @@ export { delete_ as delete }
|
||||
delete_.description = 'delete an existing authentication token'
|
||||
|
||||
delete_.params = {
|
||||
token: { type: 'string' },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function deleteAll({ except }) {
|
||||
await this.deleteAuthenticationTokens({
|
||||
filter: {
|
||||
user_id: this.apiContext.user.id,
|
||||
id: {
|
||||
__not: except,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteAll.description = 'delete all tokens of the current user except the current one'
|
||||
|
||||
deleteAll.params = {
|
||||
except: { type: 'string', optional: true },
|
||||
tokens: { type: 'array', optional: true, items: { type: 'string' } },
|
||||
pattern: { type: 'object', optional: true },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
@ -4,8 +4,12 @@
|
||||
const forEach = require('lodash/forEach')
|
||||
|
||||
const messages = {
|
||||
creation: 'Creation',
|
||||
description: 'Description',
|
||||
expiration: 'Expiration',
|
||||
keyValue: '{key}: {value}',
|
||||
|
||||
notDefined: 'Not defined',
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
@ -2046,6 +2050,8 @@ const messages = {
|
||||
pifPhysicallyDisconnected: 'Physically disconnected',
|
||||
|
||||
// ----- User -----
|
||||
authToken: 'Token',
|
||||
authTokens: 'Authentication tokens',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
language: 'Language',
|
||||
@ -2065,15 +2071,23 @@ const messages = {
|
||||
forgetTokensSuccess: 'Successfully forgot connection tokens',
|
||||
forgetTokensError: 'Error while forgetting connection tokens',
|
||||
sshKeys: 'SSH keys',
|
||||
newAuthToken: 'New token',
|
||||
newSshKey: 'New SSH key',
|
||||
deleteAuthTokens: 'Delete selected authentication tokens',
|
||||
deleteSshKey: 'Delete',
|
||||
deleteSshKeys: 'Delete selected SSH keys',
|
||||
newAuthTokenModalTitle: 'New authentication token',
|
||||
newSshKeyModalTitle: 'New SSH key',
|
||||
sshKeyAlreadyExists: 'SSH key already exists!',
|
||||
sshKeyErrorTitle: 'Invalid key',
|
||||
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
|
||||
title: 'Title',
|
||||
key: 'Key',
|
||||
deleteAuthTokenConfirm: 'Delete authentication token',
|
||||
deleteAuthTokenConfirmMessage: 'Are you sure you want to delete the authentication token: {id}?',
|
||||
deleteAuthTokensConfirm: 'Delete authentication token{nTokens, plural, one {} other {s}}',
|
||||
deleteAuthTokensConfirmMessage:
|
||||
'Are you sure you want to delete {nTokens, number} autentication token{nTokens, plural, one {} other {s}}?',
|
||||
deleteSshKeyConfirm: 'Delete SSH key',
|
||||
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
||||
deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',
|
||||
|
@ -516,6 +516,10 @@ export const createFakeProgress = (() => {
|
||||
}
|
||||
})()
|
||||
|
||||
export const NumericDate = ({ timestamp }) => (
|
||||
<FormattedDate day='2-digit' hour='numeric' minute='numeric' month='2-digit' value={timestamp} year='numeric' />
|
||||
)
|
||||
|
||||
export const ShortDate = ({ timestamp }) => (
|
||||
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ import fetch, { post } from '../fetch'
|
||||
import invoke from '../invoke'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import NewAuthTokenModal from './new-auth-token-modal'
|
||||
import renderXoItem, { renderXoItemFromId } from '../render-xo-item'
|
||||
import store from 'store'
|
||||
import { alert, chooseAction, confirm } from '../modal'
|
||||
@ -539,6 +540,8 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
|
||||
return subscription
|
||||
}
|
||||
|
||||
export const subscribeUserAuthTokens = createSubscription(() => _call('user.getAuthenticationTokens'))
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
@ -2782,7 +2785,14 @@ export const deleteUsers = users =>
|
||||
export const editUser = (user, { email, password, permission }) =>
|
||||
_call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)
|
||||
|
||||
const _signOutFromEverywhereElse = () => _call('token.deleteAll', { except: cookies.get('token') })
|
||||
const _signOutFromEverywhereElse = () =>
|
||||
_call('token.delete', {
|
||||
pattern: {
|
||||
id: {
|
||||
__not: cookies.get('token'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const signOutFromEverywhereElse = () =>
|
||||
_signOutFromEverywhereElse().then(
|
||||
@ -2890,6 +2900,47 @@ export const deleteSshKeys = keys =>
|
||||
})
|
||||
}, noop)
|
||||
|
||||
export const addAuthToken = async () => {
|
||||
const { description, expiration } = await confirm({
|
||||
body: <NewAuthTokenModal />,
|
||||
icon: 'user',
|
||||
title: _('newAuthTokenModalTitle'),
|
||||
})
|
||||
const expires = new Date(expiration).setHours(23, 59, 59)
|
||||
return _call('token.create', {
|
||||
description,
|
||||
expiresIn: Number.isNaN(expires) ? undefined : expires - new Date().getTime(),
|
||||
})::tap(subscribeUserAuthTokens.forceRefresh)
|
||||
}
|
||||
|
||||
export const deleteAuthToken = async ({ id }) => {
|
||||
await confirm({
|
||||
body: _('deleteAuthTokenConfirmMessage', {
|
||||
id,
|
||||
}),
|
||||
icon: 'user',
|
||||
title: _('deleteAuthTokenConfirm'),
|
||||
})
|
||||
return _call('token.delete', { tokens: [id] })::tap(subscribeUserAuthTokens.forceRefresh)
|
||||
}
|
||||
|
||||
export const deleteAuthTokens = async tokens => {
|
||||
await confirm({
|
||||
body: _('deleteAuthTokensConfirmMessage', {
|
||||
nTokens: tokens.length,
|
||||
}),
|
||||
icon: 'user',
|
||||
title: _('deleteAuthTokensConfirm', { nTokens: tokens.length }),
|
||||
})
|
||||
return _call('token.delete', { tokens: tokens.map(token => token.id) })::tap(subscribeUserAuthTokens.forceRefresh)
|
||||
}
|
||||
|
||||
export const editAuthToken = ({ description, id }) =>
|
||||
_call('token.set', {
|
||||
description,
|
||||
id,
|
||||
})::tap(subscribeUserAuthTokens.forceRefresh)
|
||||
|
||||
// User filters --------------------------------------------------
|
||||
|
||||
import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
|
||||
|
48
packages/xo-web/src/common/xo/new-auth-token-modal/index.js
Normal file
48
packages/xo-web/src/common/xo/new-auth-token-modal/index.js
Normal file
@ -0,0 +1,48 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
|
||||
export default class NewAuthTokenModal extends BaseComponent {
|
||||
get value() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
render() {
|
||||
const { description, expiration } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='pb-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('expiration')}</Col>
|
||||
<Col size={8}>
|
||||
<input
|
||||
className='form-control'
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
onChange={this.linkState('expiration')}
|
||||
type='date'
|
||||
value={expiration ?? ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className='pb-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('description')}</Col>
|
||||
<Col size={8}>
|
||||
<textarea
|
||||
className='form-control'
|
||||
onChange={this.linkState('description')}
|
||||
rows={10}
|
||||
value={description ?? ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import * as FormGrid from 'form-grid'
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import homeFilters from 'home-filters'
|
||||
import Icon from 'icon'
|
||||
import PropTypes from 'prop-types'
|
||||
@ -16,16 +17,21 @@ import { isEmpty, map } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Select } from 'form'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { addSubscriptions, connectStore, noop } from 'utils'
|
||||
import { addSubscriptions, connectStore, noop, NumericDate } from 'utils'
|
||||
import {
|
||||
addAuthToken,
|
||||
addSshKey,
|
||||
changePassword,
|
||||
deleteAuthToken,
|
||||
deleteAuthTokens,
|
||||
deleteSshKey,
|
||||
deleteSshKeys,
|
||||
editAuthToken,
|
||||
editCustomFilter,
|
||||
removeCustomFilter,
|
||||
setDefaultHomeFilter,
|
||||
signOutFromEverywhereElse,
|
||||
subscribeUserAuthTokens,
|
||||
subscribeCurrentUser,
|
||||
} from 'xo'
|
||||
|
||||
@ -282,6 +288,83 @@ const SshKeys = addSubscriptions({
|
||||
)
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
const COLUMNS_AUTH_TOKENS = [
|
||||
{
|
||||
itemRenderer: ({ id }) => (
|
||||
<Copiable tagName='pre' data={id}>
|
||||
{id.slice(0, 5)}…
|
||||
</Copiable>
|
||||
),
|
||||
name: _('authToken'),
|
||||
},
|
||||
{
|
||||
itemRenderer: token => (
|
||||
<Text value={token.description ?? ''} onChange={description => editAuthToken({ ...token, description })} />
|
||||
),
|
||||
name: _('description'),
|
||||
sortCriteria: 'description',
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ created_at }) => {
|
||||
if (created_at !== undefined) {
|
||||
return <NumericDate timestamp={created_at} />
|
||||
}
|
||||
return _('notDefined')
|
||||
},
|
||||
name: _('creation'),
|
||||
sortCriteria: 'created_at',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
itemRenderer: ({ expiration }) => <NumericDate timestamp={expiration} />,
|
||||
name: _('expiration'),
|
||||
sortCriteria: 'expiration',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS_AUTH_TOKENS = [
|
||||
{
|
||||
handler: deleteAuthToken,
|
||||
icon: 'delete',
|
||||
label: _('delete'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS_AUTH_TOKENS = [
|
||||
{
|
||||
handler: deleteAuthTokens,
|
||||
icon: 'delete',
|
||||
label: _('deleteAuthTokens'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const UserAuthTokens = addSubscriptions({
|
||||
userAuthTokens: subscribeUserAuthTokens,
|
||||
})(({ userAuthTokens }) => (
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='user' /> {_('authTokens')}
|
||||
<ActionButton className='btn-success pull-right' icon='add' handler={addAuthToken}>
|
||||
{_('newAuthToken')}
|
||||
</ActionButton>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<SortedTable
|
||||
collection={userAuthTokens}
|
||||
columns={COLUMNS_AUTH_TOKENS}
|
||||
stateUrlParam='s_auth_tokens'
|
||||
groupedActions={GROUPED_ACTIONS_AUTH_TOKENS}
|
||||
individualActions={INDIVIDUAL_ACTIONS_AUTH_TOKENS}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@addSubscriptions({
|
||||
@ -412,6 +495,8 @@ export default class User extends Component {
|
||||
]}
|
||||
<SshKeys />
|
||||
<hr />
|
||||
<UserAuthTokens />
|
||||
<hr />
|
||||
<UserFilters user={user} />
|
||||
</Page>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user