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))
|
- [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))
|
- [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
|
### Bug fixes
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ create.params = {
|
|||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
async function delete_({ token: id }) {
|
async function delete_({ pattern, tokens }) {
|
||||||
await this.deleteAuthenticationToken(id)
|
await this.deleteAuthenticationTokens({ filter: pattern ?? { id: { __or: tokens } } })
|
||||||
}
|
}
|
||||||
|
|
||||||
export { delete_ as delete }
|
export { delete_ as delete }
|
||||||
@ -34,26 +34,8 @@ export { delete_ as delete }
|
|||||||
delete_.description = 'delete an existing authentication token'
|
delete_.description = 'delete an existing authentication token'
|
||||||
|
|
||||||
delete_.params = {
|
delete_.params = {
|
||||||
token: { type: 'string' },
|
tokens: { type: 'array', optional: true, items: { type: 'string' } },
|
||||||
}
|
pattern: { type: 'object', optional: true },
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
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 },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
const forEach = require('lodash/forEach')
|
const forEach = require('lodash/forEach')
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
|
creation: 'Creation',
|
||||||
|
description: 'Description',
|
||||||
|
expiration: 'Expiration',
|
||||||
keyValue: '{key}: {value}',
|
keyValue: '{key}: {value}',
|
||||||
|
|
||||||
|
notDefined: 'Not defined',
|
||||||
statusConnecting: 'Connecting',
|
statusConnecting: 'Connecting',
|
||||||
statusDisconnected: 'Disconnected',
|
statusDisconnected: 'Disconnected',
|
||||||
statusLoading: 'Loading…',
|
statusLoading: 'Loading…',
|
||||||
@ -2046,6 +2050,8 @@ const messages = {
|
|||||||
pifPhysicallyDisconnected: 'Physically disconnected',
|
pifPhysicallyDisconnected: 'Physically disconnected',
|
||||||
|
|
||||||
// ----- User -----
|
// ----- User -----
|
||||||
|
authToken: 'Token',
|
||||||
|
authTokens: 'Authentication tokens',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
@ -2065,15 +2071,23 @@ const messages = {
|
|||||||
forgetTokensSuccess: 'Successfully forgot connection tokens',
|
forgetTokensSuccess: 'Successfully forgot connection tokens',
|
||||||
forgetTokensError: 'Error while forgetting connection tokens',
|
forgetTokensError: 'Error while forgetting connection tokens',
|
||||||
sshKeys: 'SSH keys',
|
sshKeys: 'SSH keys',
|
||||||
|
newAuthToken: 'New token',
|
||||||
newSshKey: 'New SSH key',
|
newSshKey: 'New SSH key',
|
||||||
|
deleteAuthTokens: 'Delete selected authentication tokens',
|
||||||
deleteSshKey: 'Delete',
|
deleteSshKey: 'Delete',
|
||||||
deleteSshKeys: 'Delete selected SSH keys',
|
deleteSshKeys: 'Delete selected SSH keys',
|
||||||
|
newAuthTokenModalTitle: 'New authentication token',
|
||||||
newSshKeyModalTitle: 'New SSH key',
|
newSshKeyModalTitle: 'New SSH key',
|
||||||
sshKeyAlreadyExists: 'SSH key already exists!',
|
sshKeyAlreadyExists: 'SSH key already exists!',
|
||||||
sshKeyErrorTitle: 'Invalid key',
|
sshKeyErrorTitle: 'Invalid key',
|
||||||
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
|
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
key: 'Key',
|
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',
|
deleteSshKeyConfirm: 'Delete SSH key',
|
||||||
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
||||||
deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',
|
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 }) => (
|
export const ShortDate = ({ timestamp }) => (
|
||||||
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
|
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,7 @@ import fetch, { post } from '../fetch'
|
|||||||
import invoke from '../invoke'
|
import invoke from '../invoke'
|
||||||
import Icon from '../icon'
|
import Icon from '../icon'
|
||||||
import logError from '../log-error'
|
import logError from '../log-error'
|
||||||
|
import NewAuthTokenModal from './new-auth-token-modal'
|
||||||
import renderXoItem, { renderXoItemFromId } from '../render-xo-item'
|
import renderXoItem, { renderXoItemFromId } from '../render-xo-item'
|
||||||
import store from 'store'
|
import store from 'store'
|
||||||
import { alert, chooseAction, confirm } from '../modal'
|
import { alert, chooseAction, confirm } from '../modal'
|
||||||
@ -539,6 +540,8 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
|
|||||||
return subscription
|
return subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const subscribeUserAuthTokens = createSubscription(() => _call('user.getAuthenticationTokens'))
|
||||||
|
|
||||||
// System ============================================================
|
// System ============================================================
|
||||||
|
|
||||||
export const apiMethods = _call('system.getMethodsInfo')
|
export const apiMethods = _call('system.getMethodsInfo')
|
||||||
@ -2782,7 +2785,14 @@ export const deleteUsers = users =>
|
|||||||
export const editUser = (user, { email, password, permission }) =>
|
export const editUser = (user, { email, password, permission }) =>
|
||||||
_call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)
|
_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 = () =>
|
export const signOutFromEverywhereElse = () =>
|
||||||
_signOutFromEverywhereElse().then(
|
_signOutFromEverywhereElse().then(
|
||||||
@ -2890,6 +2900,47 @@ export const deleteSshKeys = keys =>
|
|||||||
})
|
})
|
||||||
}, noop)
|
}, 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 --------------------------------------------------
|
// User filters --------------------------------------------------
|
||||||
|
|
||||||
import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
|
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 _, { messages } from 'intl'
|
||||||
import ActionButton from 'action-button'
|
import ActionButton from 'action-button'
|
||||||
import Component from 'base-component'
|
import Component from 'base-component'
|
||||||
|
import Copiable from 'copiable'
|
||||||
import homeFilters from 'home-filters'
|
import homeFilters from 'home-filters'
|
||||||
import Icon from 'icon'
|
import Icon from 'icon'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
@ -16,16 +17,21 @@ import { isEmpty, map } from 'lodash'
|
|||||||
import { injectIntl } from 'react-intl'
|
import { injectIntl } from 'react-intl'
|
||||||
import { Select } from 'form'
|
import { Select } from 'form'
|
||||||
import { Card, CardBlock, CardHeader } from 'card'
|
import { Card, CardBlock, CardHeader } from 'card'
|
||||||
import { addSubscriptions, connectStore, noop } from 'utils'
|
import { addSubscriptions, connectStore, noop, NumericDate } from 'utils'
|
||||||
import {
|
import {
|
||||||
|
addAuthToken,
|
||||||
addSshKey,
|
addSshKey,
|
||||||
changePassword,
|
changePassword,
|
||||||
|
deleteAuthToken,
|
||||||
|
deleteAuthTokens,
|
||||||
deleteSshKey,
|
deleteSshKey,
|
||||||
deleteSshKeys,
|
deleteSshKeys,
|
||||||
|
editAuthToken,
|
||||||
editCustomFilter,
|
editCustomFilter,
|
||||||
removeCustomFilter,
|
removeCustomFilter,
|
||||||
setDefaultHomeFilter,
|
setDefaultHomeFilter,
|
||||||
signOutFromEverywhereElse,
|
signOutFromEverywhereElse,
|
||||||
|
subscribeUserAuthTokens,
|
||||||
subscribeCurrentUser,
|
subscribeCurrentUser,
|
||||||
} from 'xo'
|
} 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({
|
@addSubscriptions({
|
||||||
@ -412,6 +495,8 @@ export default class User extends Component {
|
|||||||
]}
|
]}
|
||||||
<SshKeys />
|
<SshKeys />
|
||||||
<hr />
|
<hr />
|
||||||
|
<UserAuthTokens />
|
||||||
|
<hr />
|
||||||
<UserFilters user={user} />
|
<UserFilters user={user} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user