added force publish option for admins

This commit is contained in:
PhilReact 2024-12-16 14:00:43 +02:00
parent d77b6caed4
commit 4c4ee5b1d7
5 changed files with 269 additions and 155 deletions

View File

@ -27,7 +27,8 @@ export const AdminSpace = ({
myAddress, myAddress,
hide, hide,
defaultThread, defaultThread,
setDefaultThread setDefaultThread,
setIsForceShowCreationKeyPopup
}) => { }) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
@ -59,7 +60,7 @@ export const AdminSpace = ({
justifyContent: 'center', justifyContent: 'center',
paddingTop: '25px' paddingTop: '25px'
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>} }}><Typography>Sorry, this space is only for Admins.</Typography></Box>}
{isAdmin && <AdminSpaceInner adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />} {isAdmin && <AdminSpaceInner setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
</div> </div>
); );

View File

@ -1,150 +1,248 @@
import React, { useCallback, useContext, useEffect, useState } from 'react' import React, { useCallback, useContext, useEffect, useState } from "react";
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../../App'; import {
import { Box, Button, Typography } from '@mui/material'; MyContext,
import { decryptResource, validateSecretKey } from '../Group/Group'; getArbitraryEndpointReact,
import { getFee } from '../../background'; getBaseApiReact,
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; } from "../../App";
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { Box, Button, Typography } from "@mui/material";
import { formatTimestampForum } from '../../utils/time'; import {
import { Spacer } from '../../common/Spacer'; decryptResource,
getPublishesFromAdmins,
validateSecretKey,
} from "../Group/Group";
import { getFee } from "../../background";
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
import { formatTimestampForum } from "../../utils/time";
import { Spacer } from "../../common/Spacer";
export const getPublishesFromAdminsAdminSpace = async (
admins: string[],
groupId
) => {
const queryString = admins.map((name) => `name=${name}`).join("&");
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${groupId}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("network error");
}
const adminData = await response.json();
export const getPublishesFromAdminsAdminSpace = async (admins: string[], groupId) => { const filterId = adminData.filter(
const queryString = admins.map((name) => `name=${name}`).join("&"); (data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}`
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${ );
groupId if (filterId?.length === 0) {
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; return false;
const response = await fetch(url); }
if (!response.ok) { const sortedData = filterId.sort((a: any, b: any) => {
throw new Error("network error"); // Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent
return dateB.getTime() - dateA.getTime();
});
return sortedData[0];
};
export const AdminSpaceInner = ({
selectedGroup,
adminsWithNames,
setIsForceShowCreationKeyPopup,
}) => {
const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null);
const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] =
useState(true);
const [isFetchingGroupSecretKey, setIsFetchingGroupSecretKey] =
useState(true);
const [
adminGroupSecretKeyPublishDetails,
setAdminGroupSecretKeyPublishDetails,
] = useState(null);
const [groupSecretKeyPublishDetails, setGroupSecretKeyPublishDetails] =
useState(null);
const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false);
const { show, setTxList, setInfoSnackCustom, setOpenSnackGlobal } =
useContext(MyContext);
const getAdminGroupSecretKey = useCallback(async () => {
try {
if (!selectedGroup) return;
const getLatestPublish = await getPublishesFromAdminsAdminSpace(
adminsWithNames.map((admin) => admin?.name),
selectedGroup
);
if (getLatestPublish === false) return;
let data;
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${
getLatestPublish.name
}/${getLatestPublish.identifier}?encoding=base64`
);
data = await res.text();
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
setAdminGroupSecretKey(decryptedKeyToObject);
setAdminGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) {
} finally {
setIsFetchingAdminGroupSecretKey(false);
} }
const adminData = await response.json(); }, [adminsWithNames, selectedGroup]);
const filterId = adminData.filter( const getGroupSecretKey = useCallback(async () => {
(data: any) => try {
data.identifier === `admins-symmetric-qchat-group-${groupId}` if (!selectedGroup) return;
); const getLatestPublish = await getPublishesFromAdmins(
if (filterId?.length === 0) { adminsWithNames.map((admin) => admin?.name),
return false; selectedGroup
);
if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false);
setGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) {
} finally {
setIsFetchingGroupSecretKey(false);
} }
const sortedData = filterId.sort((a: any, b: any) => { }, [adminsWithNames, selectedGroup]);
// Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent const createCommonSecretForAdmins = async () => {
return dateB.getTime() - dateA.getTime(); try {
}); const fee = await getFee("ARBITRARY");
await show({
message: "Would you like to perform an ARBITRARY transaction?",
publishFee: fee.fee + " QORT",
});
setIsLoadingPublishKey(true);
return sortedData[0]; window
.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", {
groupId: selectedGroup,
previousData: null,
admins: adminsWithNames,
})
.then((response) => {
if (!response?.error) {
setInfoSnackCustom({
type: "success",
message:
"Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.",
});
setOpenSnackGlobal(true);
return;
}
setInfoSnackCustom({
type: "error",
message: response?.error || "unable to re-encrypt secret key",
});
setOpenSnackGlobal(true);
})
.catch((error) => {
setInfoSnackCustom({
type: "error",
message: error?.message || "unable to re-encrypt secret key",
});
setOpenSnackGlobal(true);
});
} catch (error) {}
}; };
export const AdminSpaceInner = ({selectedGroup, adminsWithNames}) => { useEffect(() => {
const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null) getAdminGroupSecretKey();
const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = useState(true) getGroupSecretKey();
const [adminGroupSecretKeyPublishDetails, setAdminGroupSecretKeyPublishDetails] = useState(null) }, [getAdminGroupSecretKey, getGroupSecretKey]);
const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false)
const { show, setTxList, setInfoSnackCustom,
setOpenSnackGlobal } = useContext(MyContext);
const getAdminGroupSecretKey = useCallback(async ()=> {
try {
if(!selectedGroup) return
const getLatestPublish = await getPublishesFromAdminsAdminSpace(adminsWithNames.map((admin)=> admin?.name), selectedGroup)
if(getLatestPublish === false) return
let data;
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${getLatestPublish.name}/${
getLatestPublish.identifier
}?encoding=base64`
);
data = await res.text();
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
setAdminGroupSecretKey(decryptedKeyToObject)
setAdminGroupSecretKeyPublishDetails(getLatestPublish)
} catch (error) {
} finally {
setIsFetchingAdminGroupSecretKey(false)
}
}, [adminsWithNames, selectedGroup])
const createCommonSecretForAdmins = async ()=> {
try {
const fee = await getFee('ARBITRARY')
await show({
message: "Would you like to perform an ARBITRARY transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingPublishKey(true)
window.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", {
groupId: selectedGroup,
previousData: null,
admins: adminsWithNames
})
.then((response) => {
if (!response?.error) {
setInfoSnackCustom({
type: "success",
message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.",
});
setOpenSnackGlobal(true);
return
}
setInfoSnackCustom({
type: "error",
message: response?.error || "unable to re-encrypt secret key",
});
setOpenSnackGlobal(true);
})
.catch((error) => {
setInfoSnackCustom({
type: "error",
message: error?.message || "unable to re-encrypt secret key",
});
setOpenSnackGlobal(true);
});
} catch (error) {
}
}
useEffect(() => {
getAdminGroupSecretKey()
}, [getAdminGroupSecretKey]);
return ( return (
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
flexDirection: 'column', display: "flex",
padding: '10px' flexDirection: "column",
}}> padding: "10px",
alignItems: 'center'
}}
>
<Typography sx={{
fontSize: '14px'
}}>Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait.</Typography>
<Spacer height="25px" /> <Spacer height="25px" />
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
gap: '20px', flexDirection: "column",
width: '300px', gap: "20px",
maxWidth: '90%' width: "300px",
}}> maxWidth: "90%",
{isFetchingAdminGroupSecretKey && <Typography>Fetching Admins secret keys</Typography>} padding: '10px',
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && <Typography>No secret key published yet</Typography>} border: '1px solid gray',
{adminGroupSecretKeyPublishDetails && ( borderRadius: '6px'
<Typography>Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)}</Typography> }}
>
{isFetchingGroupSecretKey && (
<Typography>Fetching Group secret key publishes</Typography>
)} )}
<Button onClick={createCommonSecretForAdmins} variant="contained">Publish admin secret key</Button> {!isFetchingGroupSecretKey &&
groupSecretKeyPublishDetails === false && (
<Typography>No secret key published yet</Typography>
)}
{groupSecretKeyPublishDetails && (
<Typography>
Last encryption date:{" "}
{formatTimestampForum(
groupSecretKeyPublishDetails?.updated ||
groupSecretKeyPublishDetails?.created
)}{" "}
{` by ${groupSecretKeyPublishDetails?.name}`}
</Typography>
)}
<Button disabled={isFetchingGroupSecretKey} onClick={setIsForceShowCreationKeyPopup} variant="contained">
Publish group secret key
</Button>
<Spacer height="20px" />
<Typography sx={{
fontSize: '14px'
}}>This key is to encrypt GROUP related content. This is the only one used in this UI as of now. All group members will be able to see content encrypted with this key.</Typography>
</Box>
<Spacer height="25px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "300px",
maxWidth: "90%",
padding: '10px',
border: '1px solid gray',
borderRadius: '6px'
}}
>
{isFetchingAdminGroupSecretKey && (
<Typography>Fetching Admins secret key</Typography>
)}
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && (
<Typography>No secret key published yet</Typography>
)}
{adminGroupSecretKeyPublishDetails && (
<Typography>
Last encryption date:{" "}
{formatTimestampForum(
adminGroupSecretKeyPublishDetails?.updated ||
adminGroupSecretKeyPublishDetails?.created
)}
</Typography>
)}
<Button disabled={isFetchingAdminGroupSecretKey} onClick={createCommonSecretForAdmins} variant="contained">
Publish admin secret key
</Button>
<Spacer height="20px" />
<Typography sx={{
fontSize: '14px'
}}>This key is to encrypt ADMIN related content. Only admins would see content encrypted with it.</Typography>
</Box> </Box>
</Box> </Box>
) );
} };

View File

@ -54,14 +54,14 @@ const [messageSize, setMessageSize] = useState(0)
const handleUpdateRef = useRef(null); const handleUpdateRef = useRef(null);
const getTimestampEnterChat = async () => { const getTimestampEnterChat = async (selectedGroup) => {
try { try {
return new Promise((res, rej) => { return new Promise((res, rej) => {
window.sendMessage("getTimestampEnterChat") window.sendMessage("getTimestampEnterChat")
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
if(response && selectedGroup && response[selectedGroup]){ if(response && selectedGroup){
lastReadTimestamp.current = response[selectedGroup] lastReadTimestamp.current = response[selectedGroup] || undefined
window.sendMessage("addTimestampEnterChat", { window.sendMessage("addTimestampEnterChat", {
timestamp: Date.now(), timestamp: Date.now(),
groupId: selectedGroup groupId: selectedGroup
@ -89,8 +89,9 @@ const [messageSize, setMessageSize] = useState(0)
}; };
useEffect(()=> { useEffect(()=> {
getTimestampEnterChat() if(!selectedGroup) return
}, []) getTimestampEnterChat(selectedGroup)
}, [selectedGroup])
@ -208,7 +209,9 @@ const [messageSize, setMessageSize] = useState(0)
const formatted = combineUIAndExtensionMsgs const formatted = combineUIAndExtensionMsgs
.filter((rawItem) => !rawItem?.chatReference) .filter((rawItem) => !rawItem?.chatReference)
.map((item) => { .map((item) => {
const additionalFields = item?.data === 'NDAwMQ==' ? {
text: "<p>First group key created.</p>"
} : {}
return { return {
...item, ...item,
id: item.signature, id: item.signature,
@ -216,6 +219,7 @@ const [messageSize, setMessageSize] = useState(0)
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true,
isNotEncrypted: !!item?.messageText, isNotEncrypted: !!item?.messageText,
...additionalFields
} }
}); });
setMessages((prev) => [...prev, ...formatted]); setMessages((prev) => [...prev, ...formatted]);
@ -287,10 +291,12 @@ const [messageSize, setMessageSize] = useState(0)
}); });
} else { } else {
let firstUnreadFound = false; let firstUnreadFound = false;
console.log('combineUIAndExtensionMsgs', combineUIAndExtensionMsgs)
const formatted = combineUIAndExtensionMsgs const formatted = combineUIAndExtensionMsgs
.filter((rawItem) => !rawItem?.chatReference) .filter((rawItem) => !rawItem?.chatReference)
.map((item) => { .map((item) => {
const additionalFields = item?.data === 'NDAwMQ==' ? {
text: "<p>First group key created.</p>"
} : {}
const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender;
if(divide){ if(divide){
@ -303,7 +309,8 @@ const [messageSize, setMessageSize] = useState(0)
repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo,
isNotEncrypted: !!item?.messageText, isNotEncrypted: !!item?.messageText,
unread: false, unread: false,
divide divide,
...additionalFields
} }
}); });
setMessages(formatted); setMessages(formatted);

View File

@ -8,7 +8,7 @@ import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Gro
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup}) => { export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup}) => {
const { show, setTxList } = useContext(MyContext); const { show, setTxList } = useContext(MyContext);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
@ -131,6 +131,9 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
]); ]);
} }
setIsLoading(false); setIsLoading(false);
setTimeout(() => {
setIsForceShowCreationKeyPopup(false)
}, 1000);
}) })
.catch((error) => { .catch((error) => {
console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred"); console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred");
@ -173,6 +176,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
}}> }}>
<Button onClick={()=> { <Button onClick={()=> {
setHideCommonKeyPopup(true) setHideCommonKeyPopup(true)
setIsForceShowCreationKeyPopup(false)
}} size='small'>Hide</Button> }} size='small'>Hide</Button>
</Box> </Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} /> <CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />

View File

@ -432,7 +432,7 @@ export const Group = ({
const [appsModeDev, setAppsModeDev] = useState('home') const [appsModeDev, setAppsModeDev] = useState('home')
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const [groupsProperties, setGroupsProperties] = useState({}) const [groupsProperties, setGroupsProperties] = useState({})
@ -2360,8 +2360,11 @@ export const Group = ({
setDefaultThread={setDefaultThread} setDefaultThread={setDefaultThread}
isPrivate={isPrivate} isPrivate={isPrivate}
/> />
<AdminSpace adminsWithNames={adminsWithNames} selectedGroup={selectedGroup?.groupId} myAddress={myAddress} userInfo={userInfo} hide={groupSection !== "adminSpace"} isAdmin={admins.includes(myAddress)} {groupSection === "adminSpace" && (
/> <AdminSpace setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup?.groupId} myAddress={myAddress} userInfo={userInfo} hide={groupSection !== "adminSpace"} isAdmin={admins.includes(myAddress)}
/>
)}
</> </>
)} )}
@ -2374,11 +2377,11 @@ export const Group = ({
zIndex: 100, zIndex: 100,
}} }}
> >
{isPrivate && admins.includes(myAddress) && {(isPrivate && admins.includes(myAddress) &&
shouldReEncrypt && shouldReEncrypt &&
triedToFetchSecretKey && triedToFetchSecretKey &&
!firstSecretKeyInCreation && !firstSecretKeyInCreation &&
!hideCommonKeyPopup && ( !hideCommonKeyPopup) || isForceShowCreationKeyPopup && (
<CreateCommonSecret <CreateCommonSecret
setHideCommonKeyPopup={setHideCommonKeyPopup} setHideCommonKeyPopup={setHideCommonKeyPopup}
groupId={selectedGroup?.groupId} groupId={selectedGroup?.groupId}
@ -2387,6 +2390,7 @@ export const Group = ({
myAddress={myAddress} myAddress={myAddress}
isOwner={groupOwner?.owner === myAddress} isOwner={groupOwner?.owner === myAddress}
userInfo={userInfo} userInfo={userInfo}
setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup}
noSecretKey={ noSecretKey={
admins.includes(myAddress) && admins.includes(myAddress) &&
!secretKey && !secretKey &&