Journal members: implement giving access
parent
d0c9a9c559
commit
28c16ccae5
|
@ -0,0 +1,132 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
|
||||
import LoadingIndicator from '../widgets/LoadingIndicator';
|
||||
import ConfirmationDialog from '../widgets/ConfirmationDialog';
|
||||
import PrettyFingerprint from '../widgets/PrettyFingerprint';
|
||||
|
||||
import * as EteSync from '../api/EteSync';
|
||||
import { CredentialsData } from '../store';
|
||||
|
||||
import { handleInputChange } from '../helpers';
|
||||
|
||||
interface PropsType {
|
||||
etesync: CredentialsData;
|
||||
info: EteSync.CollectionInfo;
|
||||
onOk: (user: string, publicKey: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
class JournalMemberAddDialog extends React.PureComponent<PropsType> {
|
||||
public state = {
|
||||
addUser: '',
|
||||
publicKey: '',
|
||||
userChosen: false,
|
||||
error: undefined as Error | undefined,
|
||||
};
|
||||
|
||||
private handleInputChange: any;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
this.handleInputChange = handleInputChange(this);
|
||||
this.onAddRequest = this.onAddRequest.bind(this);
|
||||
this.onOk = this.onOk.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { onClose } = this.props;
|
||||
const { addUser, userChosen, publicKey, error } = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<ConfirmationDialog
|
||||
title="Error adding member"
|
||||
labelOk="OK"
|
||||
open
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
>
|
||||
User ({addUser}) not found, or has journal sharing disabled.
|
||||
</ConfirmationDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (publicKey) {
|
||||
return (
|
||||
<>
|
||||
<ConfirmationDialog
|
||||
title="Verify security fingerprint"
|
||||
labelOk="OK"
|
||||
open
|
||||
onOk={this.onOk}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<p>
|
||||
Verify {addUser}'s security fingerprint to ensure the encryption is secure.
|
||||
</p>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<PrettyFingerprint publicKey={publicKey} />
|
||||
</div>
|
||||
</ConfirmationDialog>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<ConfirmationDialog
|
||||
title="Add member"
|
||||
labelOk="OK"
|
||||
open={!userChosen}
|
||||
onOk={this.onAddRequest}
|
||||
onCancel={onClose}
|
||||
>
|
||||
{ userChosen ?
|
||||
<LoadingIndicator />
|
||||
:
|
||||
<TextField
|
||||
name="addUser"
|
||||
type="email"
|
||||
placeholder="User email"
|
||||
style={{ width: '100%' }}
|
||||
value={addUser}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
}
|
||||
</ConfirmationDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onAddRequest(user: string) {
|
||||
this.setState({
|
||||
userChosen: true,
|
||||
});
|
||||
|
||||
const { etesync } = this.props;
|
||||
const { addUser } = this.state;
|
||||
|
||||
const creds = etesync.credentials;
|
||||
const apiBase = etesync.serviceApiUrl;
|
||||
const userInfoManager = new EteSync.UserInfoManager(creds, apiBase);
|
||||
userInfoManager.fetch(addUser).then((userInfo) => {
|
||||
this.setState({
|
||||
publicKey: userInfo.publicKey,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({error});
|
||||
});
|
||||
}
|
||||
|
||||
private onOk() {
|
||||
const { addUser, publicKey } = this.state;
|
||||
this.props.onOk(addUser, publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
export default JournalMemberAddDialog;
|
|
@ -1,20 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import * as sjcl from 'sjcl';
|
||||
|
||||
import { List, ListItem } from '../widgets/List';
|
||||
|
||||
import { Theme, withTheme } from '@material-ui/core/styles';
|
||||
import IconMemberAdd from '@material-ui/icons/PersonAdd';
|
||||
|
||||
import AppBarOverride from '../widgets/AppBarOverride';
|
||||
import Container from '../widgets/Container';
|
||||
import LoadingIndicator from '../widgets/LoadingIndicator';
|
||||
import ConfirmationDialog from '../widgets/ConfirmationDialog';
|
||||
|
||||
import JournalMemberAddDialog from './JournalMemberAddDialog';
|
||||
|
||||
import * as EteSync from '../api/EteSync';
|
||||
import { CredentialsData } from '../store';
|
||||
import { CredentialsData, UserInfoData } from '../store';
|
||||
|
||||
import { SyncInfoJournal } from '../SyncGate';
|
||||
|
||||
interface PropsType {
|
||||
etesync: CredentialsData;
|
||||
info: EteSync.CollectionInfo;
|
||||
syncJournal: SyncInfoJournal;
|
||||
userInfo: UserInfoData;
|
||||
}
|
||||
|
||||
interface PropsTypeInner extends PropsType {
|
||||
|
@ -25,6 +32,7 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
public state = {
|
||||
members: null as EteSync.JournalMemberJson[] | null,
|
||||
revokeUser: null as string | null,
|
||||
addMemberOpen: false,
|
||||
};
|
||||
|
||||
constructor(props: PropsTypeInner) {
|
||||
|
@ -32,30 +40,38 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
|
||||
this.onRevokeRequest = this.onRevokeRequest.bind(this);
|
||||
this.onRevokeDo = this.onRevokeDo.bind(this);
|
||||
this.onMemberAdd = this.onMemberAdd.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { info } = this.props;
|
||||
const { members, revokeUser } = this.state;
|
||||
const { syncJournal } = this.props;
|
||||
const { members, revokeUser, addMemberOpen } = this.state;
|
||||
|
||||
const info = syncJournal.collection;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBarOverride title={`${info.displayName} - Members`} />
|
||||
<Container style={{maxWidth: '30rem'}}>
|
||||
{ members ?
|
||||
(members.length > 0 ?
|
||||
<List>
|
||||
{ members.map((member) => (
|
||||
<List>
|
||||
<ListItem rightIcon={<IconMemberAdd />} onClick={() => this.setState({ addMemberOpen: true })}>
|
||||
Add member
|
||||
</ListItem>
|
||||
{(members.length > 0 ?
|
||||
members.map((member) => (
|
||||
<ListItem key={member.user} onClick={() => this.onRevokeRequest(member.user)}>
|
||||
{member.user}
|
||||
</ListItem>
|
||||
))}
|
||||
))
|
||||
:
|
||||
<ListItem>
|
||||
No members
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
:
|
||||
<div>No members</div>
|
||||
)
|
||||
:
|
||||
<LoadingIndicator />
|
||||
<LoadingIndicator />
|
||||
}
|
||||
</Container>
|
||||
<ConfirmationDialog
|
||||
|
@ -68,6 +84,15 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
Would you like to revoke {revokeUser}'s access?<br />
|
||||
Please be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.
|
||||
</ConfirmationDialog>
|
||||
|
||||
{ addMemberOpen &&
|
||||
<JournalMemberAddDialog
|
||||
etesync={this.props.etesync}
|
||||
info={info}
|
||||
onOk={this.onMemberAdd}
|
||||
onClose={() => this.setState({ addMemberOpen: false })}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -77,7 +102,8 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
}
|
||||
|
||||
private fetchMembers() {
|
||||
const { etesync, info } = this.props;
|
||||
const { etesync, syncJournal } = this.props;
|
||||
const info = syncJournal.collection;
|
||||
|
||||
const creds = etesync.credentials;
|
||||
const apiBase = etesync.serviceApiUrl;
|
||||
|
@ -96,8 +122,9 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
}
|
||||
|
||||
private onRevokeDo() {
|
||||
const { etesync, info } = this.props;
|
||||
const { etesync, syncJournal } = this.props;
|
||||
const { revokeUser } = this.state;
|
||||
const info = syncJournal.collection;
|
||||
|
||||
const creds = etesync.credentials;
|
||||
const apiBase = etesync.serviceApiUrl;
|
||||
|
@ -109,6 +136,35 @@ class JournalMembers extends React.PureComponent<PropsTypeInner> {
|
|||
revokeUser: null,
|
||||
});
|
||||
}
|
||||
|
||||
private onMemberAdd(user: string, publicKey: string) {
|
||||
const { etesync, syncJournal, userInfo } = this.props;
|
||||
const journal = syncJournal.journal;
|
||||
const derived = this.props.etesync.encryptionKey;
|
||||
|
||||
const keyPair = userInfo.getKeyPair(new EteSync.CryptoManager(derived, 'userInfo', userInfo.version));
|
||||
let cryptoManager: EteSync.CryptoManager;
|
||||
if (journal.key) {
|
||||
const asymmetricCryptoManager = new EteSync.AsymmetricCryptoManager(keyPair);
|
||||
const derivedJournalKey = asymmetricCryptoManager.decryptBytes(journal.key);
|
||||
cryptoManager = EteSync.CryptoManager.fromDerivedKey(derivedJournalKey, journal.version);
|
||||
} else {
|
||||
cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version);
|
||||
}
|
||||
|
||||
const pubkeyBytes = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(publicKey));
|
||||
const encryptedKey = sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(cryptoManager.getEncryptedKey(keyPair, pubkeyBytes)));
|
||||
|
||||
const creds = etesync.credentials;
|
||||
const apiBase = etesync.serviceApiUrl;
|
||||
const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, journal.uid);
|
||||
journalMembersManager.create({ user, key: encryptedKey }).then(() => {
|
||||
this.fetchMembers();
|
||||
});
|
||||
this.setState({
|
||||
addMemberOpen: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme()(JournalMembers);
|
||||
|
|
|
@ -92,7 +92,8 @@ class Journals extends React.PureComponent {
|
|||
render={() => (
|
||||
<JournalMembers
|
||||
etesync={this.props.etesync}
|
||||
info={collectionInfo}
|
||||
syncJournal={syncJournal}
|
||||
userInfo={this.props.userInfo}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue