* rework general settings * account settings - WIP * add missing key prop * add object url hook * extract wide modal styles * profile settings and image editor - WIP * add outline style to upload card * remove file param from bind upload atom hook * add compact variant to upload card * add compact upload card renderer * add option to update profile avatar * add option to change profile displayname * allow displayname change based on capabilities check * rearrange settings components into folders * add system notification settings * add initial page param in settings * convert account data hook to typescript * add push rule hook * add notification mode hook * add notification mode switcher component * add all messages notification settings options * add special messages notification settings * add keyword notifications * add ignored users section * improve ignore user list strings * add about settings * add access token option in about settings * add developer tools settings * add expand button to account data dev tool option * update folds * fix editable active element textarea check * do not close dialog when editable element in focus * add text area plugins * add text area intent handler hook * add newline intent mod in text area * add next line hotkey in text area intent hook * add syntax error position dom utility function * add account data editor * add button to send new account data in dev tools * improve custom emoji plugin * add more custom emojis hooks * add text util css * add word break in setting tile title and description * emojis and sticker user settings - WIP * view image packs from settings * emoji pack editing - WIP * add option to edit pack meta * change saved changes message * add image edit and delete controls * add option to upload pack images and apply changes * fix state event type when updating image pack * lazy load pack image tile img * hide upload image button when user can not edit pack * add option to add or remove global image packs * upgrade to rust crypto (#2168) * update matrix js sdk * remove dead code * use rust crypto * update setPowerLevel usage * fix types * fix deprecated isRoomEncrypted method uses * fix deprecated room.currentState uses * fix deprecated import/export room keys func * fix merge issues in image pack file * fix remaining issues in image pack file * start indexedDBStore * update package lock and vite-plugin-top-level-await * user session settings - WIP * add useAsync hook * add password stage uia * add uia flow matrix error hook * add UIA action component * add options to delete sessions * add sso uia stage * fix SSO stage complete error * encryption - WIP * update user settings encryption terminology * add default variant to password input * use password input in uia password stage * add options for local backup in user settings * remove typo in import local backup password input label * online backup - WIP * fix uia sso action * move access token settings from about to developer tools * merge encryption tab into sessions and rename it to devices * add device placeholder tile * add logout dialog * add logout button for current device * move other devices in component * render unverified device verification tile * add learn more section for current device verification * add device verification status badge * add info card component * add index file for password input component * add types for secret storage * add component to access secret storage key * manual verification - WIP * update matrix-js-sdk to v35 * add manual verification * use react query for device list * show unverified tab on sidebar * fix device list updates * add session key details to current device * render restore encryption backup * fix loading state of restore backup * fix unverified tab settings closes after verification * key backup tile - WIP * fix unverified tab badge * rename session key to device key in device tile * improve backup restore functionality * fix restore button enabled after layout reload during restoring backup * update backup info on status change * add backup disconnection failures * add device verification using sas * restore backup after verification * show option to logout on startup error screen * fix key backup hook update on decryption key cached * add option to enable device verification * add device verification reset dialog * add logout button in settings drawer * add encrypted message lost on logout * fix backup restore never finish with 0 keys * fix setup dialog hides when enabling device verification * show backup details in menu * update setup device verification body copy * replace deprecated method * fix displayname appear as mxid in settings * remove old refactored codes * fix types
421 lines
13 KiB
TypeScript
421 lines
13 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Input,
|
|
Overlay,
|
|
OverlayBackdrop,
|
|
OverlayCenter,
|
|
Spinner,
|
|
Text,
|
|
color,
|
|
} from 'folds';
|
|
import React, { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
|
import {
|
|
AuthDict,
|
|
AuthType,
|
|
IAuthData,
|
|
MatrixError,
|
|
RegisterRequest,
|
|
UIAFlow,
|
|
createClient,
|
|
} from 'matrix-js-sdk';
|
|
import { PasswordInput } from '../../../components/password-input';
|
|
import {
|
|
getLoginTermUrl,
|
|
getUIAFlowForStages,
|
|
hasStageInFlows,
|
|
requiredStageInFlows,
|
|
} from '../../../utils/matrix-uia';
|
|
import { useUIACompleted, useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows';
|
|
import { AsyncState, AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
|
|
import { RegisterError, RegisterResult, register, useRegisterComplete } from './registerUtil';
|
|
import { FieldError } from '../FiledError';
|
|
import {
|
|
AutoDummyStageDialog,
|
|
AutoTermsStageDialog,
|
|
EmailStageDialog,
|
|
ReCaptchaStageDialog,
|
|
RegistrationTokenStageDialog,
|
|
} from '../../../components/uia-stages';
|
|
import { useRegisterEmail } from '../../../hooks/useRegisterEmail';
|
|
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
|
|
import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
|
|
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../../hooks/types';
|
|
|
|
export const SUPPORTED_REGISTER_STAGES = [
|
|
AuthType.RegistrationToken,
|
|
AuthType.Terms,
|
|
AuthType.Recaptcha,
|
|
AuthType.Email,
|
|
AuthType.Dummy,
|
|
];
|
|
type RegisterFormInputs = {
|
|
usernameInput: HTMLInputElement;
|
|
passwordInput: HTMLInputElement;
|
|
confirmPasswordInput: HTMLInputElement;
|
|
tokenInput?: HTMLInputElement;
|
|
emailInput?: HTMLInputElement;
|
|
termsInput?: HTMLInputElement;
|
|
};
|
|
|
|
type FormData = {
|
|
username: string;
|
|
password: string;
|
|
token?: string;
|
|
email?: string;
|
|
terms?: boolean;
|
|
clientSecret: string;
|
|
};
|
|
|
|
const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => {
|
|
const pickedStages: string[] = [];
|
|
if (formData.token) pickedStages.push(AuthType.RegistrationToken);
|
|
if (formData.email) pickedStages.push(AuthType.Email);
|
|
if (formData.terms) pickedStages.push(AuthType.Terms);
|
|
if (hasStageInFlows(uiaFlows, AuthType.Recaptcha)) {
|
|
pickedStages.push(AuthType.Recaptcha);
|
|
}
|
|
|
|
return pickedStages;
|
|
};
|
|
|
|
type RegisterUIAFlowProps = {
|
|
formData: FormData;
|
|
flow: UIAFlow;
|
|
authData: IAuthData;
|
|
registerEmailState: AsyncState<RequestEmailTokenResponse, MatrixError>;
|
|
registerEmail: RequestEmailTokenCallback;
|
|
onRegister: (registerReqData: RegisterRequest) => void;
|
|
};
|
|
function RegisterUIAFlow({
|
|
formData,
|
|
flow,
|
|
authData,
|
|
registerEmailState,
|
|
registerEmail,
|
|
onRegister,
|
|
}: RegisterUIAFlowProps) {
|
|
const completed = useUIACompleted(authData);
|
|
const { getStageToComplete } = useUIAFlow(authData, flow);
|
|
|
|
const stageToComplete = getStageToComplete();
|
|
|
|
const handleAuthDict = useCallback(
|
|
(authDict: AuthDict) => {
|
|
const { password, username } = formData;
|
|
onRegister({
|
|
auth: authDict,
|
|
password,
|
|
username,
|
|
initial_device_display_name: 'Cinny Web',
|
|
});
|
|
},
|
|
[onRegister, formData]
|
|
);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
window.location.reload();
|
|
}, []);
|
|
|
|
if (!stageToComplete) return null;
|
|
return (
|
|
<UIAFlowOverlay
|
|
currentStep={completed.length + 1}
|
|
stepCount={flow.stages.length}
|
|
onCancel={handleCancel}
|
|
>
|
|
{stageToComplete.type === AuthType.RegistrationToken && (
|
|
<RegistrationTokenStageDialog
|
|
token={formData.token}
|
|
stageData={stageToComplete}
|
|
submitAuthDict={handleAuthDict}
|
|
onCancel={handleCancel}
|
|
/>
|
|
)}
|
|
{stageToComplete.type === AuthType.Terms && (
|
|
<AutoTermsStageDialog
|
|
stageData={stageToComplete}
|
|
submitAuthDict={handleAuthDict}
|
|
onCancel={handleCancel}
|
|
/>
|
|
)}
|
|
{stageToComplete.type === AuthType.Recaptcha && (
|
|
<ReCaptchaStageDialog
|
|
stageData={stageToComplete}
|
|
submitAuthDict={handleAuthDict}
|
|
onCancel={handleCancel}
|
|
/>
|
|
)}
|
|
{stageToComplete.type === AuthType.Email && (
|
|
<EmailStageDialog
|
|
email={formData.email}
|
|
clientSecret={formData.clientSecret}
|
|
stageData={stageToComplete}
|
|
requestEmailToken={registerEmail}
|
|
emailTokenState={registerEmailState}
|
|
submitAuthDict={handleAuthDict}
|
|
onCancel={handleCancel}
|
|
/>
|
|
)}
|
|
{stageToComplete.type === AuthType.Dummy && (
|
|
<AutoDummyStageDialog
|
|
stageData={stageToComplete}
|
|
submitAuthDict={handleAuthDict}
|
|
onCancel={handleCancel}
|
|
/>
|
|
)}
|
|
</UIAFlowOverlay>
|
|
);
|
|
}
|
|
|
|
type PasswordRegisterFormProps = {
|
|
authData: IAuthData;
|
|
uiaFlows: UIAFlow[];
|
|
defaultUsername?: string;
|
|
defaultEmail?: string;
|
|
defaultRegisterToken?: string;
|
|
};
|
|
export function PasswordRegisterForm({
|
|
authData,
|
|
uiaFlows,
|
|
defaultUsername,
|
|
defaultEmail,
|
|
defaultRegisterToken,
|
|
}: PasswordRegisterFormProps) {
|
|
const serverDiscovery = useAutoDiscoveryInfo();
|
|
const baseUrl = serverDiscovery['m.homeserver'].base_url;
|
|
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
|
|
const params = useUIAParams(authData);
|
|
const termUrl = getLoginTermUrl(params);
|
|
const [formData, setFormData] = useState<FormData>();
|
|
|
|
const [ongoingFlow, setOngoingFlow] = useState<UIAFlow>();
|
|
|
|
const [registerEmailState, registerEmail] = useRegisterEmail(mx);
|
|
|
|
const [registerState, handleRegister] = useAsyncCallback<
|
|
RegisterResult,
|
|
MatrixError,
|
|
[RegisterRequest]
|
|
>(useCallback(async (registerReqData) => register(mx, registerReqData), [mx]));
|
|
const [ongoingAuthData, customRegisterResp] =
|
|
registerState.status === AsyncStatus.Success ? registerState.data : [];
|
|
const registerError =
|
|
registerState.status === AsyncStatus.Error ? registerState.error : undefined;
|
|
|
|
useRegisterComplete(customRegisterResp);
|
|
|
|
const handleSubmit: ChangeEventHandler<HTMLFormElement> = (evt) => {
|
|
evt.preventDefault();
|
|
const {
|
|
usernameInput,
|
|
passwordInput,
|
|
confirmPasswordInput,
|
|
emailInput,
|
|
tokenInput,
|
|
termsInput,
|
|
} = evt.target as HTMLFormElement & RegisterFormInputs;
|
|
const token = tokenInput?.value.trim();
|
|
const username = usernameInput.value.trim();
|
|
const password = passwordInput.value;
|
|
const confirmPassword = confirmPasswordInput.value;
|
|
if (password !== confirmPassword) {
|
|
return;
|
|
}
|
|
const email = emailInput?.value.trim();
|
|
const terms = termsInput?.value === 'on';
|
|
|
|
if (!username) {
|
|
usernameInput.focus();
|
|
return;
|
|
}
|
|
|
|
const fData: FormData = {
|
|
username,
|
|
password,
|
|
token,
|
|
email,
|
|
terms,
|
|
clientSecret: mx.generateClientSecret(),
|
|
};
|
|
const pickedStages = pickStages(uiaFlows, fData);
|
|
const pickedFlow = getUIAFlowForStages(uiaFlows, pickedStages);
|
|
setOngoingFlow(pickedFlow);
|
|
setFormData(fData);
|
|
handleRegister({
|
|
username,
|
|
password,
|
|
auth: {
|
|
session: authData.session,
|
|
},
|
|
initial_device_display_name: 'Cinny Web',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
|
|
<Box direction="Column" gap="100">
|
|
<Text as="label" size="L400" priority="300">
|
|
Username
|
|
</Text>
|
|
<Input
|
|
variant="Background"
|
|
defaultValue={defaultUsername}
|
|
name="usernameInput"
|
|
size="500"
|
|
outlined
|
|
required
|
|
/>
|
|
{registerError?.errcode === RegisterError.UserTaken && (
|
|
<FieldError message="This username is already taken." />
|
|
)}
|
|
{registerError?.errcode === RegisterError.UserInvalid && (
|
|
<FieldError message="This username contains invalid characters." />
|
|
)}
|
|
{registerError?.errcode === RegisterError.UserExclusive && (
|
|
<FieldError message="This username is reserved." />
|
|
)}
|
|
</Box>
|
|
<ConfirmPasswordMatch initialValue>
|
|
{(match, doMatch, passRef, confPassRef) => (
|
|
<>
|
|
<Box direction="Column" gap="100">
|
|
<Text as="label" size="L400" priority="300">
|
|
Password
|
|
</Text>
|
|
<PasswordInput
|
|
ref={passRef}
|
|
onChange={doMatch}
|
|
name="passwordInput"
|
|
variant="Background"
|
|
size="500"
|
|
outlined
|
|
required
|
|
/>
|
|
{registerError?.errcode === RegisterError.PasswordWeak && (
|
|
<FieldError
|
|
message={
|
|
registerError.data.error ??
|
|
'Weak Password. Password rejected by server please choosing more strong Password.'
|
|
}
|
|
/>
|
|
)}
|
|
{registerError?.errcode === RegisterError.PasswordShort && (
|
|
<FieldError
|
|
message={
|
|
registerError.data.error ??
|
|
'Short Password. Password rejected by server please choosing more long Password.'
|
|
}
|
|
/>
|
|
)}
|
|
</Box>
|
|
<Box direction="Column" gap="100">
|
|
<Text as="label" size="L400" priority="300">
|
|
Confirm Password
|
|
</Text>
|
|
<PasswordInput
|
|
ref={confPassRef}
|
|
onChange={doMatch}
|
|
name="confirmPasswordInput"
|
|
variant="Background"
|
|
size="500"
|
|
style={{ color: match ? undefined : color.Critical.Main }}
|
|
outlined
|
|
required
|
|
/>
|
|
</Box>
|
|
</>
|
|
)}
|
|
</ConfirmPasswordMatch>
|
|
{hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && (
|
|
<Box direction="Column" gap="100">
|
|
<Text as="label" size="L400" priority="300">
|
|
{requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)
|
|
? 'Registration Token'
|
|
: 'Registration Token (Optional)'}
|
|
</Text>
|
|
<Input
|
|
variant="Background"
|
|
defaultValue={defaultRegisterToken}
|
|
name="tokenInput"
|
|
size="500"
|
|
required={requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)}
|
|
outlined
|
|
/>
|
|
</Box>
|
|
)}
|
|
{hasStageInFlows(uiaFlows, AuthType.Email) && (
|
|
<Box direction="Column" gap="100">
|
|
<Text as="label" size="L400" priority="300">
|
|
{requiredStageInFlows(uiaFlows, AuthType.Email) ? 'Email' : 'Email (Optional)'}
|
|
</Text>
|
|
<Input
|
|
variant="Background"
|
|
defaultValue={defaultEmail}
|
|
name="emailInput"
|
|
type="email"
|
|
size="500"
|
|
required={requiredStageInFlows(uiaFlows, AuthType.Email)}
|
|
outlined
|
|
/>
|
|
</Box>
|
|
)}
|
|
|
|
{hasStageInFlows(uiaFlows, AuthType.Terms) && termUrl && (
|
|
<Box alignItems="Center" gap="200">
|
|
<Checkbox name="termsInput" size="300" variant="Primary" required />
|
|
<Text size="T300">
|
|
I accept server{' '}
|
|
<a href={termUrl} target="_blank" rel="noreferrer">
|
|
Terms and Conditions
|
|
</a>
|
|
.
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
{registerError?.errcode === RegisterError.RateLimited && (
|
|
<FieldError message="Failed to register. Your register request has been rate-limited by server, Please try after some time." />
|
|
)}
|
|
{registerError?.errcode === RegisterError.Forbidden && (
|
|
<FieldError message="Failed to register. The homeserver does not permit registration." />
|
|
)}
|
|
{registerError?.errcode === RegisterError.InvalidRequest && (
|
|
<FieldError message="Failed to register. Invalid request." />
|
|
)}
|
|
{registerError?.errcode === RegisterError.Unknown && (
|
|
<FieldError message={registerError.data.error ?? 'Failed to register. Unknown Reason.'} />
|
|
)}
|
|
<span data-spacing-node />
|
|
<Button variant="Primary" size="500" type="submit">
|
|
<Text as="span" size="B500">
|
|
Register
|
|
</Text>
|
|
</Button>
|
|
</Box>
|
|
{registerState.status === AsyncStatus.Success &&
|
|
formData &&
|
|
ongoingFlow &&
|
|
ongoingAuthData && (
|
|
<RegisterUIAFlow
|
|
formData={formData}
|
|
flow={ongoingFlow}
|
|
authData={ongoingAuthData}
|
|
registerEmail={registerEmail}
|
|
registerEmailState={registerEmailState}
|
|
onRegister={handleRegister}
|
|
/>
|
|
)}
|
|
{registerState.status === AsyncStatus.Loading && (
|
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
|
<OverlayCenter>
|
|
<Spinner variant="Secondary" size="600" />
|
|
</OverlayCenter>
|
|
</Overlay>
|
|
)}
|
|
</>
|
|
);
|
|
}
|