Can set user recovery key from UI
This commit is contained in:
@@ -159,6 +159,13 @@ impl MatrixClient {
|
|||||||
.await
|
.await
|
||||||
.map_err(MatrixClientError::RestoreSession)?;
|
.map_err(MatrixClientError::RestoreSession)?;
|
||||||
|
|
||||||
|
// Wait for encryption tasks to complete
|
||||||
|
client
|
||||||
|
.client
|
||||||
|
.encryption()
|
||||||
|
.wait_for_e2ee_initialization_tasks()
|
||||||
|
.await;
|
||||||
|
|
||||||
// Force token refresh to make sure session is still alive, otherwise disconnect user
|
// Force token refresh to make sure session is still alive, otherwise disconnect user
|
||||||
if let Err(refresh_error) = client.client.oauth().refresh_access_token().await {
|
if let Err(refresh_error) = client.client.oauth().refresh_access_token().await {
|
||||||
if let RefreshTokenError::OAuth(e) = &refresh_error
|
if let RefreshTokenError::OAuth(e) = &refresh_error
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface UserInfo {
|
|||||||
email: string;
|
email: string;
|
||||||
matrix_user_id?: string;
|
matrix_user_id?: string;
|
||||||
matrix_device_id?: string;
|
matrix_device_id?: string;
|
||||||
matrix_recovery_state?: string;
|
matrix_recovery_state?: "Enabled" | "Disabled" | "Unknown" | "Incomplete";
|
||||||
}
|
}
|
||||||
|
|
||||||
const TokenStateKey = "auth-state";
|
const TokenStateKey = "auth-state";
|
||||||
|
|||||||
@@ -33,4 +33,15 @@ export class MatrixLinkApi {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new user recovery key
|
||||||
|
*/
|
||||||
|
static async SetRecoveryKey(key: string): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/matrix_link/set_recovery_key",
|
||||||
|
method: "POST",
|
||||||
|
jsonData: { key },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,17 @@ const LoadingMessageContextK =
|
|||||||
export function LoadingMessageProvider(
|
export function LoadingMessageProvider(
|
||||||
p: PropsWithChildren
|
p: PropsWithChildren
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(0);
|
||||||
|
|
||||||
const [message, setMessage] = React.useState("");
|
const [message, setMessage] = React.useState("");
|
||||||
|
|
||||||
const hook: LoadingMessageContext = {
|
const hook: LoadingMessageContext = {
|
||||||
show(message) {
|
show(message) {
|
||||||
setMessage(message);
|
setMessage(message);
|
||||||
setOpen(true);
|
setOpen((v) => v + 1);
|
||||||
},
|
},
|
||||||
hide() {
|
hide() {
|
||||||
setMessage("");
|
setOpen((v) => v - 1);
|
||||||
setOpen(false);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ export function LoadingMessageProvider(
|
|||||||
<>
|
<>
|
||||||
<LoadingMessageContextK value={hook}>{p.children}</LoadingMessageContextK>
|
<LoadingMessageContextK value={hook}>{p.children}</LoadingMessageContextK>
|
||||||
|
|
||||||
<Dialog open={open}>
|
<Dialog open={open > 0}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import KeyIcon from "@mui/icons-material/Key";
|
||||||
import LinkIcon from "@mui/icons-material/Link";
|
import LinkIcon from "@mui/icons-material/Link";
|
||||||
import LinkOffIcon from "@mui/icons-material/LinkOff";
|
import LinkOffIcon from "@mui/icons-material/LinkOff";
|
||||||
import {
|
import {
|
||||||
@@ -5,8 +8,15 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardActions,
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
import { MatrixLinkApi } from "../api/MatrixLinkApi";
|
import { MatrixLinkApi } from "../api/MatrixLinkApi";
|
||||||
import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
||||||
import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider";
|
import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider";
|
||||||
@@ -19,7 +29,14 @@ export function MatrixLinkRoute(): React.ReactElement {
|
|||||||
const user = useUserInfo();
|
const user = useUserInfo();
|
||||||
return (
|
return (
|
||||||
<MatrixGWRouteContainer label={"Matrix account link"}>
|
<MatrixGWRouteContainer label={"Matrix account link"}>
|
||||||
{user.info.matrix_user_id === null ? <ConnectCard /> : <ConnectedCard />}
|
{user.info.matrix_user_id === null ? (
|
||||||
|
<ConnectCard />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ConnectedCard />
|
||||||
|
<EncryptionKeyStatus />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</MatrixGWRouteContainer>
|
</MatrixGWRouteContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,8 +92,6 @@ function ConnectedCard(): React.ReactElement {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const loadingMessage = useLoadingMessage();
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
const info = useUserInfo();
|
|
||||||
|
|
||||||
const user = useUserInfo();
|
const user = useUserInfo();
|
||||||
|
|
||||||
const handleDisconnect = async () => {
|
const handleDisconnect = async () => {
|
||||||
@@ -91,13 +106,13 @@ function ConnectedCard(): React.ReactElement {
|
|||||||
console.error(`Failed to unlink user account! ${e}`);
|
console.error(`Failed to unlink user account! ${e}`);
|
||||||
alert(`Failed to unlink your account! ${e}`);
|
alert(`Failed to unlink your account! ${e}`);
|
||||||
} finally {
|
} finally {
|
||||||
info.reloadUserInfo();
|
user.reloadUserInfo();
|
||||||
loadingMessage.hide();
|
loadingMessage.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card style={{ marginBottom: "10px" }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h5" component="div" gutterBottom>
|
<Typography variant="h5" component="div" gutterBottom>
|
||||||
<i>Connected to your Matrix account</i>
|
<i>Connected to your Matrix account</i>
|
||||||
@@ -135,3 +150,102 @@ function ConnectedCard(): React.ReactElement {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EncryptionKeyStatus(): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
|
const user = useUserInfo();
|
||||||
|
|
||||||
|
const [typeNewKey, setTypeNewKey] = React.useState(false);
|
||||||
|
const [newKey, setNewKey] = React.useState("");
|
||||||
|
|
||||||
|
const handleSetKey = () => setTypeNewKey(true);
|
||||||
|
const cancelSetKey = () => setTypeNewKey(false);
|
||||||
|
const handleSubmitKey = async () => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Updating recovery key...");
|
||||||
|
|
||||||
|
await MatrixLinkApi.SetRecoveryKey(newKey);
|
||||||
|
setNewKey("");
|
||||||
|
setTypeNewKey(false);
|
||||||
|
|
||||||
|
snackbar("Recovery key successfully updated!");
|
||||||
|
user.reloadUserInfo();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set new recovery key! ${e}`);
|
||||||
|
alert(`Failed to set new recovery key! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h5" component="div" gutterBottom>
|
||||||
|
Recovery keys
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
<p>
|
||||||
|
Recovery key is used to verify MatrixGW connection and access
|
||||||
|
message history in encrypted rooms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Current encryption status:{" "}
|
||||||
|
{user.info.matrix_recovery_state === "Enabled" ? (
|
||||||
|
<CheckIcon
|
||||||
|
style={{ display: "inline", verticalAlign: "middle" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CloseIcon
|
||||||
|
style={{ display: "inline", verticalAlign: "middle" }}
|
||||||
|
/>
|
||||||
|
)}{" "}
|
||||||
|
{user.info.matrix_recovery_state}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<KeyIcon />}
|
||||||
|
onClick={handleSetKey}
|
||||||
|
>
|
||||||
|
Set new recovery key
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Set new key dialog */}
|
||||||
|
<Dialog open={typeNewKey} onClose={cancelSetKey}>
|
||||||
|
<DialogTitle>Set new recovery key</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Enter below you recovery key to verify this session and gain access
|
||||||
|
to old messages.
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
label="Recovery key"
|
||||||
|
type="text"
|
||||||
|
variant="standard"
|
||||||
|
autoComplete="off"
|
||||||
|
value={newKey}
|
||||||
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={cancelSetKey}>Cancel</Button>
|
||||||
|
<Button onClick={handleSubmitKey} disabled={newKey === ""} autoFocus>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import { Button } from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
|
import { AuthApi, type UserInfo } from "../../api/AuthApi";
|
||||||
|
import { useAuth } from "../../App";
|
||||||
|
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
import DashboardHeader from "./DashboardHeader";
|
import DashboardHeader from "./DashboardHeader";
|
||||||
import DashboardSidebar from "./DashboardSidebar";
|
import DashboardSidebar from "./DashboardSidebar";
|
||||||
import { AuthApi, type UserInfo } from "../../api/AuthApi";
|
|
||||||
import { AsyncWidget } from "../AsyncWidget";
|
|
||||||
import { Button } from "@mui/material";
|
|
||||||
import { useAuth } from "../../App";
|
|
||||||
|
|
||||||
interface UserInfoContext {
|
interface UserInfoContext {
|
||||||
info: UserInfo;
|
info: UserInfo;
|
||||||
@@ -21,12 +23,25 @@ const UserInfoContextK = React.createContext<UserInfoContext | null>(null);
|
|||||||
|
|
||||||
export default function BaseAuthenticatedPage(): React.ReactElement {
|
export default function BaseAuthenticatedPage(): React.ReactElement {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const alert = useAlert();
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
const [userInfo, setuserInfo] = React.useState<null | UserInfo>(null);
|
const [userInfo, setuserInfo] = React.useState<null | UserInfo>(null);
|
||||||
const loadUserInfo = async () => {
|
const loadUserInfo = async () => {
|
||||||
setuserInfo(await AuthApi.GetUserInfo());
|
setuserInfo(await AuthApi.GetUserInfo());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reloadUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Refreshing user information...");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load user information! ${e}`);
|
||||||
|
alert(`Failed to load user information! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -85,7 +100,7 @@ export default function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
<UserInfoContextK
|
<UserInfoContextK
|
||||||
value={{
|
value={{
|
||||||
info: userInfo!,
|
info: userInfo!,
|
||||||
reloadUserInfo: loadUserInfo,
|
reloadUserInfo,
|
||||||
signOut,
|
signOut,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user