CollectionMembers: add pages to control collection memberships.

master
Tom Hacohen 4 years ago
parent 0bb7867059
commit 2f565994b7

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import * as Etebase from "etebase";
import TextField from "@material-ui/core/TextField";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import LoadingIndicator from "../widgets/LoadingIndicator";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import PrettyFingerprint from "../widgets/PrettyFingerprint";
import { CachedCollection } from "../Pim/helpers";
import { useCredentials } from "../credentials";
interface PropsType {
collection: CachedCollection;
onOk: (username: string, publicKey: Uint8Array, accessLevel: Etebase.CollectionAccessLevel) => void;
onClose: () => void;
}
export default function CollectionMemberAddDialog(props: PropsType) {
const etebase = useCredentials()!;
const [addUser, setAddUser] = React.useState("");
const [publicKey, setPublicKey] = React.useState<Uint8Array>();
const [readOnly, setReadOnly] = React.useState(false);
const [userChosen, setUserChosen] = React.useState(false);
const [error, setError] = React.useState<Error>();
async function onAddRequest(_user: string) {
setUserChosen(true);
const inviteMgr = etebase.getInvitationManager();
try {
const userProfile = await inviteMgr.fetchUserProfile(addUser);
setPublicKey(userProfile.pubkey);
} catch (e) {
setError(e);
}
}
function onOk() {
props.onOk(addUser, publicKey!, readOnly ? Etebase.CollectionAccessLevel.ReadOnly : Etebase.CollectionAccessLevel.ReadWrite);
}
const { onClose } = props;
if (error) {
return (
<>
<ConfirmationDialog
title="Error adding member"
labelOk="OK"
open
onOk={onClose}
onCancel={onClose}
>
User ({addUser}) not found. Have they setup their encryption password from one of the apps?
</ConfirmationDialog>
</>
);
}
if (publicKey) {
return (
<>
<ConfirmationDialog
title="Verify security fingerprint"
labelOk="OK"
open
onOk={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="Invite user"
labelOk="OK"
open={!userChosen}
onOk={onAddRequest}
onCancel={onClose}
>
{userChosen ?
<LoadingIndicator />
:
<>
<TextField
name="addUser"
placeholder="Username"
style={{ width: "100%" }}
value={addUser}
onChange={(ev) => setAddUser(ev.target.value)}
/>
<FormControlLabel
control={
<Checkbox
checked={readOnly}
onChange={(event) => setReadOnly(event.target.checked)}
/>
}
label="Read only?"
/>
</>
}
</ConfirmationDialog>
</>
);
}
}

@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import * as Etebase from "etebase";
import { List, ListItem } from "../widgets/List";
import IconMemberAdd from "@material-ui/icons/PersonAdd";
import VisibilityIcon from "@material-ui/icons/Visibility";
import AppBarOverride from "../widgets/AppBarOverride";
import Container from "../widgets/Container";
import LoadingIndicator from "../widgets/LoadingIndicator";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import { useCredentials } from "../credentials";
import { getCollectionManager } from "../etebase-helpers";
import { CachedCollection } from "../Pim/helpers";
import CollectionMemberAddDialog from "./CollectionMemberAddDialog";
interface PropsType {
collection: CachedCollection;
}
export default function CollectionMembers(props: PropsType) {
const etebase = useCredentials()!;
const [members_, setMembers] = React.useState<Etebase.CollectionMember[]>();
const [revokeUser, setRevokeUser] = React.useState<string | null>(null);
const [addMemberOpen, setAddMemberOpen] = React.useState(false);
// FIXME: add error handling
const { collection, metadata } = props.collection;
async function fetchMembers() {
const colMgr = getCollectionManager(etebase);
const memberManager = colMgr.getMemberManager(collection);
const members = await memberManager.list();
setMembers(members.data);
}
React.useEffect(() => {
fetchMembers();
}, []);
function onRevokeRequest(user: string) {
setRevokeUser(user);
}
async function onRevokeDo() {
const colMgr = getCollectionManager(etebase);
const memberManager = colMgr.getMemberManager(collection);
await memberManager.remove(revokeUser!);
await fetchMembers();
setRevokeUser(null);
}
async function onMemberAdd(username: string, pubkey: Uint8Array, accessLevel: Etebase.CollectionAccessLevel) {
const inviteMgr = etebase.getInvitationManager();
await inviteMgr.invite(collection, username, pubkey, accessLevel);
await fetchMembers();
setAddMemberOpen(false);
}
const members = members_?.filter((x) => x.username !== etebase.user.username);
return (
<>
<AppBarOverride title={`${metadata.name} - Members`} />
<Container style={{ maxWidth: "30rem" }}>
{members ?
<List>
<ListItem rightIcon={<IconMemberAdd />} onClick={() => setAddMemberOpen(true)}>
Invite user
</ListItem>
{(members.length > 0 ?
members.map((member) => (
<ListItem
key={member.username}
onClick={() => onRevokeRequest(member.username)}
rightIcon={(member.accessLevel === Etebase.CollectionAccessLevel.ReadOnly) ? (<VisibilityIcon />) : undefined}
>
{member.username}
</ListItem>
))
:
<ListItem>
No members
</ListItem>
)}
</List>
:
<LoadingIndicator />
}
</Container>
<ConfirmationDialog
title="Remove member"
labelOk="OK"
open={revokeUser !== null}
onOk={onRevokeDo}
onCancel={() => setRevokeUser(null)}
>
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 &&
<CollectionMemberAddDialog
collection={props.collection}
onOk={onMemberAdd}
onClose={() => setAddMemberOpen(false)}
/>
}
</>
);
}

@ -16,6 +16,7 @@ import CollectionList from "./CollectionList";
import CollectionImport from "./CollectionImport"; import CollectionImport from "./CollectionImport";
import PageNotFound from "../PageNotFound"; import PageNotFound from "../PageNotFound";
import CollectionEdit from "./CollectionEdit"; import CollectionEdit from "./CollectionEdit";
import CollectionMembers from "./CollectionMembers";
import Collection from "./Collection"; import Collection from "./Collection";
const decryptCollections = getDecryptCollectionsFunction(); const decryptCollections = getDecryptCollectionsFunction();
@ -118,7 +119,7 @@ export default function CollectionsMain() {
path={routeResolver.getRoute("collections._id.members")} path={routeResolver.getRoute("collections._id.members")}
exact exact
> >
Members <CollectionMembers collection={collection} />
</Route> </Route>
<Route <Route
path={routeResolver.getRoute("collections._id")} path={routeResolver.getRoute("collections._id")}

@ -80,7 +80,7 @@ export default function JournalMemberAddDialog(props: PropsType) {
Verify {addUser}'s security fingerprint to ensure the encryption is secure. Verify {addUser}'s security fingerprint to ensure the encryption is secure.
</p> </p>
<div style={{ textAlign: "center" }}> <div style={{ textAlign: "center" }}>
<PrettyFingerprint publicKey={publicKey} /> <PrettyFingerprint publicKey={publicKey as any} />
</div> </div>
</ConfirmationDialog> </ConfirmationDialog>
</> </>

@ -18,15 +18,12 @@ import { setSettings } from "../store/actions";
import Container from "../widgets/Container"; import Container from "../widgets/Container";
import AppBarOverride from "../widgets/AppBarOverride"; import AppBarOverride from "../widgets/AppBarOverride";
import PrettyFingerprint from "../widgets/PrettyFingerprint"; import PrettyFingerprint from "../widgets/PrettyFingerprint";
import { useCredentials } from "../credentials";
function SecurityFingerprint() { function SecurityFingerprint() {
const userInfo = useSelector((state: StoreState) => state.cache.userInfo); const etebase = useCredentials()!;
const inviteMgr = etebase.getInvitationManager();
if (!userInfo) { const publicKey = inviteMgr.pubkey;
return <p>Security fingerprint error.</p>;
}
const publicKey = userInfo.publicKey;
return ( return (
<> <>

@ -2,50 +2,17 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react"; import * as React from "react";
import sjcl from "sjcl";
import { byte, base64 } from "etesync"; import * as Etebase from "etebase";
function byteArray4ToNumber(bytes: byte[], offset: number) {
// tslint:disable:no-bitwise
return (
((bytes[offset + 0] & 0xff) * (1 << 24)) +
((bytes[offset + 1] & 0xff) * (1 << 16)) +
((bytes[offset + 2] & 0xff) * (1 << 8)) +
((bytes[offset + 3] & 0xff))
);
}
function getEncodedChunk(publicKey: byte[], offset: number) {
const chunk = byteArray4ToNumber(publicKey, offset) % 100000;
return chunk.toString().padStart(5, "0");
}
interface PropsType { interface PropsType {
publicKey: base64; publicKey: Uint8Array;
} }
class PrettyFingerprint extends React.PureComponent<PropsType> { export default function PrettyFingerprint(props: PropsType) {
public render() { const prettyFingerprint = Etebase.getPrettyFingerprint(props.publicKey);
const fingerprint = sjcl.codec.bytes.fromBits(
sjcl.hash.sha256.hash(sjcl.codec.base64.toBits(this.props.publicKey))
);
const spacing = " ";
const prettyPublicKey =
getEncodedChunk(fingerprint, 0) + spacing +
getEncodedChunk(fingerprint, 4) + spacing +
getEncodedChunk(fingerprint, 8) + spacing +
getEncodedChunk(fingerprint, 12) + "\n" +
getEncodedChunk(fingerprint, 16) + spacing +
getEncodedChunk(fingerprint, 20) + spacing +
getEncodedChunk(fingerprint, 24) + spacing +
getEncodedChunk(fingerprint, 28);
return ( return (
<pre>{prettyPublicKey}</pre> <pre>{prettyFingerprint}</pre>
); );
} }
}
export default PrettyFingerprint;

Loading…
Cancel
Save