Can set user recovery key from UI

This commit is contained in:
2025-11-10 17:42:32 +01:00
parent a23d671376
commit 84c90ea033
6 changed files with 162 additions and 16 deletions

View File

@@ -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

View File

@@ -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";

View File

@@ -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 },
});
}
} }

View File

@@ -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

View File

@@ -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>
</>
);
}

View File

@@ -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,
}} }}
> >