Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions src/oauth2-client-editor-41/ClientForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import i18n from '@dhis2/d2-i18n'
import { Button, Modal, ModalTitle, ModalContent } from '@dhis2/ui'
import { getInstance as getD2 } from 'd2'
import FormBuilder from 'd2-ui/lib/forms/FormBuilder.component.js'
import { isUrlArray, isRequired } from 'd2-ui/lib/forms/Validators.js'
import PropTypes from 'prop-types'
import React from 'react'
import MultiToggle from '../form-fields/multi-toggle.js'
import TextField from '../form-fields/text-field.js'
import styles from './ClientForm.module.css'

const formFieldStyle = {
width: '100%',
}

const validateClientID = async (v) => {
const d2 = await getD2()
const list = await d2.models.oAuth2Clients.list({
paging: false,
filter: [`cid:eq:${v}`],
})
if (list.size > 0) {
throw i18n.t('This client ID is already taken')
}
}

const ClientForm = ({ clientModel, onUpdate, onSave, onCancel }) => {
const grantTypes = ((clientModel && clientModel.grantTypes) || []).reduce(
(curr, prev) => {
curr[prev] = true
return curr
},
{}
)

const fields = [
{
name: 'name',
value: clientModel.name,
component: TextField,
props: {
floatingLabelText: i18n.t('Name'),
style: formFieldStyle,
changeEvent: 'onBlur',
},
validators: [
{
validator: isRequired,
message: i18n.t('Required'),
},
],
},
{
name: 'cid',
value: clientModel.cid,
component: TextField,
props: {
floatingLabelText: i18n.t('Client ID'),
style: formFieldStyle,
changeEvent: 'onBlur',
},
validators: [
{
validator: isRequired,
message: i18n.t('Required'),
},
{
validator: (v) => v.toString().trim().length > 0,
message: i18n.t('Required'),
},
],
asyncValidators: [validateClientID],
},
{
name: 'secret',
value: clientModel && clientModel.secret,
component: TextField,
props: {
floatingLabelText: i18n.t('Client Secret'),
disabled: true,
style: formFieldStyle,
},
},
{
name: 'grantTypes',
component: MultiToggle,
style: formFieldStyle,
props: {
label: i18n.t('Grant Types'),
items: [
{
name: 'password',
text: i18n.t('Password'),
value: grantTypes.password,
},
{
name: 'refresh_token',
text: i18n.t('Refresh token'),
value: grantTypes.refresh_token,
},
{
name: 'authorization_code',
text: i18n.t('Authorization code'),
value: grantTypes.authorization_code,
},
],
},
},
{
name: 'redirectUris',
value: (clientModel.redirectUris || []).join('\n'),
component: TextField,
props: {
hintText: i18n.t('One URL per line'),
floatingLabelText: i18n.t('Redirect URIs'),
multiLine: true,
style: formFieldStyle,
changeEvent: 'onBlur',
},
validators: [
{
validator: isUrlArray,
message: i18n.t('This field should contain a list of URLs'),
},
],
},
]

const headerText =
clientModel.id === undefined
? i18n.t('Create new OAuth2 Client')
: i18n.t('Edit OAuth2 Client')
return (
<Modal onClose={onCancel}>
<ModalTitle>{headerText}</ModalTitle>
<ModalContent>
<FormBuilder fields={fields} onUpdateField={onUpdate} />
<div style={{ marginTop: '1rem' }}>
<Button primary onClick={onSave}>
{i18n.t('Save')}
</Button>
<Button
secondary
onClick={onCancel}
className={styles.cancelBtn}
>
{i18n.t('Cancel')}
</Button>
</div>
</ModalContent>
</Modal>
)
}

ClientForm.propTypes = {
clientModel: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
}

export default ClientForm
3 changes: 3 additions & 0 deletions src/oauth2-client-editor-41/ClientForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.cancelBtn {
float: right;
}
78 changes: 78 additions & 0 deletions src/oauth2-client-editor-41/ClientsList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import i18n from '@dhis2/d2-i18n'
import {
CenteredContent,
Table,
TableBody,
TableCell,
TableCellHead,
TableHead,
TableRow,
TableRowHead,
Button,
} from '@dhis2/ui'
import PropTypes from 'prop-types'
import React from 'react'
import styles from './ClientsList.module.css'

const ClientsList = ({ clients, onClientEdit, onClientDelete }) => {
if (clients.length === 0) {
return (
<CenteredContent>
<p>
{i18n.t('There are currently no OAuth2 clients registered')}
</p>
</CenteredContent>
)
}

return (
<Table>
<TableHead>
<TableRowHead>
<TableCellHead>{i18n.t('Name')}</TableCellHead>
<TableCellHead>{i18n.t('Password')}</TableCellHead>
<TableCellHead>{i18n.t('Refresh token')}</TableCellHead>
<TableCellHead>
{i18n.t('Authorization code')}
</TableCellHead>
<TableCellHead>{/* Buttons column */}</TableCellHead>
</TableRowHead>
</TableHead>
<TableBody>
{clients.map((client) => (
<TableRow key={client.authorization_code}>
<TableCell>{client.name}</TableCell>
<TableCell>{client.password}</TableCell>
<TableCell>{client.refresh_token}</TableCell>
<TableCell>{client.authorization_code}</TableCell>
<TableCell>
<Button
small
primary
className={styles.editBtn}
onClick={() => onClientEdit(client)}
>
{i18n.t('Edit')}
</Button>
<Button
small
destructive
onClick={() => onClientDelete(client)}
>
{i18n.t('Delete')}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}

ClientsList.propTypes = {
clients: PropTypes.array.isRequired,
onClientDelete: PropTypes.func.isRequired,
onClientEdit: PropTypes.func.isRequired,
}

export default ClientsList
3 changes: 3 additions & 0 deletions src/oauth2-client-editor-41/ClientsList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.editBtn {
margin-right: var(--spacers-dp16);
}
149 changes: 149 additions & 0 deletions src/oauth2-client-editor-41/OAuth2ClientEditor.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import i18n from '@dhis2/d2-i18n'
import { CircularLoader, CenteredContent, Button } from '@dhis2/ui'
import { getInstance as getD2 } from 'd2'
import React, { Component } from 'react'
import settingsActions from '../settingsActions.js'
import ClientForm from './ClientForm.js'
import ClientsList from './ClientsList.js'
import oa2Actions from './oauth2Client.actions.js'
import oa2ClientStore from './oauth2Client.store.js'
import styles from './OAuth2ClientEditor.module.css'

function generateSecret() {
const alphabet = '0123456789abcdef'
let uid = ''
for (let i = 0; i < 32; i++) {
uid += alphabet.charAt(Math.random() * alphabet.length)
if (i === 8 || i === 12 || i === 16 || i === 20) {
uid += '-'
}
}
return uid
}

class OAuth2ClientEditor extends Component {
state = {
showForm: false,
saving: false,
}

componentDidMount() {
this.subscriptions = []
this.subscriptions.push(
oa2ClientStore.subscribe(() => {
this.forceUpdate()
})
)

this.subscriptions.push(
oa2Actions.delete.subscribe(() => {
this.setState({ saving: false })
})
)

oa2Actions.load()
}

componentWillUnmount() {
this.subscriptions.forEach((sub) => {
sub.unsubscribe()
})
}

cancelAction = () => {
this.clientModel = undefined
oa2Actions.load()
this.setState({ showForm: false })
}

newAction = () => {
getD2().then((d2) => {
this.clientModel = d2.models.oAuth2Client.create()
this.clientModel.secret = generateSecret()
this.setState({ showForm: true })
})
}

editAction = (model) => {
this.clientModel = model
this.setState({ showForm: true })
}

deleteAction = (model) => {
this.setState({ showForm: false, saving: true })
oa2Actions.delete(model.id ? model : this.clientModel)
this.clientModel = undefined
}

saveAction = () => {
this.clientModel.name = this.clientModel.name || ''
this.clientModel.cid = this.clientModel.cid || ''
this.setState({ saving: true })
this.clientModel
.save()
.then((importReport) => {
if (importReport.status !== 'OK') {
throw new Error(importReport)
}

settingsActions.showSnackbarMessage(
i18n.t('OAuth2 client saved')
)
oa2Actions.load()
this.setState({ showForm: false, saving: false })
})
.catch(() => {
settingsActions.showSnackbarMessage(
i18n.t('Failed to save OAuth2 client')
)
this.setState({ saving: false })
})
}

formUpdateAction = (field, v) => {
let value = v
if (field === 'redirectUris') {
value = v.split('\n').filter((a) => a.trim().length > 0)
}
this.clientModel[field] = value
this.forceUpdate()
}

render() {
const clients = oa2ClientStore.state
if (!clients || this.state.saving) {
return (
<CenteredContent>
<CircularLoader />
</CenteredContent>
)
}

return (
<div className={styles.wrapper}>
<ClientsList
clients={clients}
onClientEdit={this.editAction}
onClientDelete={this.deleteAction}
/>
<Button
primary
className={styles.addClientBtn}
onClick={this.newAction}
>
{i18n.t('Add OAuth2 client')}
</Button>
{this.state.showForm && (
<ClientForm
clientModel={this.clientModel}
onUpdate={this.formUpdateAction}
onSave={this.saveAction}
onCancel={this.cancelAction}
/>
)}
</div>
)
}
}

export default OAuth2ClientEditor
Loading
Loading