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