old-remnantchat/src/components/Shell/RoomShareDialog.tsx
Jeremy Kahn 6cbfaacf1a
feat(verification): [closes #209] Verified peers (#216)
* refactor(bootstrap): add BootstrapShim
* feat(security): [#209] generate public/private keys
* refactor(encryption): move encryption utils to a service
* feat(encryption): [wip] implement convertCryptoKeyToString
* fix(user-settings): serialize crypto keys to strings
* feat(user-settings): deserialize user settings from IndexedDB
* feat(user-settings): upgrade persisted settings on boot
* feat(user-settings): automatically migrate persisted user settings
* refactor(encryption): simplify CryptoKey stringification
* refactor(encryption): DRY up EncryptionService
* feat(verification): send public key to new peers
* refactor(encryption): use class instance
* refactor(serialization): use class instance
* refactor(verification): [wip] create usePeerVerification hook
* feat(verification): encrypt verification token
* feat(verification): send encrypted token to peer
* feat(verification): verify peer
* refactor(verification): use enum for verification state
* feat(verification): expire verification requests
* fix(updatePeer): update with fresh state data
* feat(verification): display verification state
* refactor(usePeerVerification): store verification timer in Peer
* feat(verification): present tooltips explaining verification state
* feat(ui): show full page loading indicator
* feat(init): present bootup failure reasons
* refactor(init): move init to its own file
* feat(verification): show errors upon verification failure
* refactor(verification): move workaround to usePeerVerification
* feat(verification): present peer public keys
* refactor(verification): move peer public key rendering to its own component
* refactor(verification): only pass publicKey into renderer
* feat(verification): show user's own public key
* refactor(naming): rename Username to UserInfo
* refactor(loading): encapsulate height styling
* feat(verification): improve user messaging
* refactor(style): improve formatting and variable names
* feat(verification): add user info tooltip
* docs(verification): explain verification
2023-12-09 17:47:05 -06:00

199 lines
6.1 KiB
TypeScript

import Alert, { AlertColor } from '@mui/material/Alert'
import Button from '@mui/material/Button'
import Checkbox from '@mui/material/Checkbox'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import IconButton from '@mui/material/IconButton'
import FormControlLabel from '@mui/material/FormControlLabel'
import TextField from '@mui/material/TextField'
import Tooltip from '@mui/material/Tooltip'
import CloseIcon from '@mui/icons-material/Close'
import { AlertOptions } from 'models/shell'
import { useEffect, useState, SyntheticEvent } from 'react'
import { sleep } from 'utils'
import { encryptionService } from 'services/Encryption'
export interface RoomShareDialogProps {
isOpen: boolean
handleClose: () => void
roomId: string
password: string
showAlert: (message: string, options?: AlertOptions) => void
copyToClipboard: (
content: string,
alert: string,
severity: AlertColor
) => Promise<void>
}
export function RoomShareDialog(props: RoomShareDialogProps) {
const [isAdvanced, setIsAdvanced] = useState(false)
const [isUnderstood, setIsUnderstood] = useState(false)
const [password, setPassword] = useState('')
const [passThrottled, setPassThrottled] = useState(false)
const handleClose = () => {
props.handleClose()
setPassword('')
}
useEffect(() => {
if (!isAdvanced) setIsUnderstood(false)
}, [isAdvanced])
useEffect(() => {
if (!isUnderstood) setPassword('')
}, [isUnderstood])
const url = window.location.href.split('#')[0]
const copyWithPass = async () => {
const encoded = await encryptionService.encodePassword(
props.roomId,
password
)
if (encoded === props.password) {
const params = new URLSearchParams()
params.set('secret', props.password)
await props.copyToClipboard(
`${url}#${params}`,
'Private room URL with password copied to clipboard',
'warning'
)
handleClose()
} else {
setPassThrottled(true)
props.showAlert('Incorrect password entered. Please wait 2s to retry.', {
severity: 'error',
})
await sleep(2000)
setPassThrottled(false)
}
}
const copyWithoutPass = async () => {
await props.copyToClipboard(
url,
isAdvanced
? 'Private room URL without password copied to clipboard'
: 'Current URL copied to clipboard',
'success'
)
handleClose()
}
const handleFormSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault()
if (!passThrottled) copyWithPass()
}
return (
<Dialog
open={props.isOpen}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form onSubmit={handleFormSubmit}>
{isAdvanced && (
<DialogTitle id="alert-dialog-title">
Copy URL with password
<Button onClick={() => setIsAdvanced(false)}>Simple</Button>
<IconButton
aria-label="close"
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
)}
{isAdvanced && (
<DialogContent>
<DialogContentText sx={{ mb: 2 }}>
Copy URL to this private room containing an indecipherable hash of
the password. When using this URL, users will not need to enter
the password themselves.
</DialogContentText>
<Alert severity="error" sx={{ mb: 2 }}>
Be careful where and how this URL is shared. Anybody who obtains
it can enter the room. The sharing medium must be trusted, as well
as all potential recipients of the URL, just as if you were
sharing the password itself.
</Alert>
<Alert severity="warning">
By design, the password hash does not leave the web browser when
this URL is used to access the room. However, web browsers can
still independently record the full URL in the address history,
and may even store the history in the cloud if configured to do
so.
</Alert>
<FormControlLabel
label="I understand the risks"
control={
<Checkbox
checked={isUnderstood}
onChange={e => setIsUnderstood(e.target.checked)}
/>
}
/>
<TextField
autoFocus
margin="none"
id="password"
label="Password"
type="password"
fullWidth
variant="standard"
value={password}
disabled={!isUnderstood}
onChange={e => setPassword(e.target.value)}
/>
</DialogContent>
)}
<DialogActions>
{isAdvanced ? (
<Tooltip title="Copy room URL with password. No password entry required to access room.">
<span>
<Button
type="submit"
onClick={copyWithPass}
color="error"
disabled={
password.length === 0 || !isUnderstood || passThrottled
}
>
Copy URL with password
</Button>
</span>
</Tooltip>
) : (
<Button onClick={() => setIsAdvanced(true)} color="error">
Advanced
</Button>
)}
<Tooltip title="Copy room URL. Password required to access room.">
<Button onClick={copyWithoutPass} color="success" autoFocus>
Copy URL
</Button>
</Tooltip>
</DialogActions>
</form>
</Dialog>
)
}