CollectionMembers: add pages to control collection memberships.
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)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue