Journal members: implement giving access

master
Tom Hacohen 6 years ago
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,28 +40,36 @@ 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) => (
<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>
))}
</List>
))
:
<div>No members</div>
)
<ListItem>
No members
</ListItem>
)}
</List>
:
<LoadingIndicator />
}
@ -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…
Cancel
Save