diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index d846e76..3eca30f 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -159,6 +159,13 @@ impl MatrixClient { .await .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 if let Err(refresh_error) = client.client.oauth().refresh_access_token().await { if let RefreshTokenError::OAuth(e) = &refresh_error diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index e504b25..c9d0fa8 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -8,7 +8,7 @@ export interface UserInfo { email: string; matrix_user_id?: string; matrix_device_id?: string; - matrix_recovery_state?: string; + matrix_recovery_state?: "Enabled" | "Disabled" | "Unknown" | "Incomplete"; } const TokenStateKey = "auth-state"; diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index d5b3cb7..12507b7 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -33,4 +33,15 @@ export class MatrixLinkApi { method: "POST", }); } + + /** + * Set a new user recovery key + */ + static async SetRecoveryKey(key: string): Promise { + await APIClient.exec({ + uri: "/matrix_link/set_recovery_key", + method: "POST", + jsonData: { key }, + }); + } } diff --git a/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx index 083a985..6338de7 100644 --- a/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx +++ b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx @@ -17,18 +17,17 @@ const LoadingMessageContextK = export function LoadingMessageProvider( p: PropsWithChildren ): React.ReactElement { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(0); const [message, setMessage] = React.useState(""); const hook: LoadingMessageContext = { show(message) { setMessage(message); - setOpen(true); + setOpen((v) => v + 1); }, hide() { - setMessage(""); - setOpen(false); + setOpen((v) => v - 1); }, }; @@ -36,7 +35,7 @@ export function LoadingMessageProvider( <> {p.children} - + 0}>
- {user.info.matrix_user_id === null ? : } + {user.info.matrix_user_id === null ? ( + + ) : ( + <> + + + + )} ); } @@ -75,8 +92,6 @@ function ConnectedCard(): React.ReactElement { const alert = useAlert(); const loadingMessage = useLoadingMessage(); - const info = useUserInfo(); - const user = useUserInfo(); const handleDisconnect = async () => { @@ -91,13 +106,13 @@ function ConnectedCard(): React.ReactElement { console.error(`Failed to unlink user account! ${e}`); alert(`Failed to unlink your account! ${e}`); } finally { - info.reloadUserInfo(); + user.reloadUserInfo(); loadingMessage.hide(); } }; return ( - + Connected to your Matrix account @@ -135,3 +150,102 @@ function ConnectedCard(): React.ReactElement { ); } + +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 ( + <> + + + + Recovery keys + + +

+ Recovery key is used to verify MatrixGW connection and access + message history in encrypted rooms. +

+ +

+ Current encryption status:{" "} + {user.info.matrix_recovery_state === "Enabled" ? ( + + ) : ( + + )}{" "} + {user.info.matrix_recovery_state} +

+
+
+ + + +
+ + {/* Set new key dialog */} + + Set new recovery key + + + Enter below you recovery key to verify this session and gain access + to old messages. + + setNewKey(e.target.value)} + fullWidth + /> + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 0382979..17d9f2a 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -1,15 +1,17 @@ +import { Button } from "@mui/material"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; 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 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 { info: UserInfo; @@ -21,12 +23,25 @@ const UserInfoContextK = React.createContext(null); export default function BaseAuthenticatedPage(): React.ReactElement { const theme = useTheme(); + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); const [userInfo, setuserInfo] = React.useState(null); const loadUserInfo = async () => { 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 navigate = useNavigate(); @@ -85,7 +100,7 @@ export default function BaseAuthenticatedPage(): React.ReactElement {