requested changes

This commit is contained in:
Daniel Hougaard
2025-03-03 21:44:40 +04:00
parent ff269b1063
commit 787c091948
14 changed files with 103 additions and 125 deletions
@@ -44,7 +44,6 @@ export const registerSecretRequestsRouter = async (server: FastifyZodProvider) =
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({ const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
id: req.params.id, id: req.params.id,
actorOrgId: req.permission?.orgId, actorOrgId: req.permission?.orgId,
orgId: req.permission?.orgId,
actor: req.permission?.type, actor: req.permission?.type,
actorId: req.permission?.id, actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod actorAuthMethod: req.permission?.authMethod
@@ -82,7 +81,6 @@ export const registerSecretRequestsRouter = async (server: FastifyZodProvider) =
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({ const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
id: req.params.id, id: req.params.id,
actorOrgId: req.permission?.orgId, actorOrgId: req.permission?.orgId,
orgId: req.permission?.orgId,
actor: req.permission?.type, actor: req.permission?.type,
actorId: req.permission?.id, actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod, actorAuthMethod: req.permission?.authMethod,
@@ -2,7 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas"; import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -32,10 +32,8 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.first(); .first();
if (!secretRequest) { if (!secretRequest) {
throw new DatabaseError({ throw new NotFoundError({
error: new Error("Get Secret Request By Id, Not found"), message: `Secret request with ID '${id}' not found`
message: "Get Secret Request By Id, Not found",
name: "GetSecretRequestById"
}); });
} }
@@ -176,7 +176,6 @@ export const secretSharingServiceFactory = ({
id, id,
actor, actor,
actorId, actorId,
orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TGetSecretRequestByIdDTO) => { }: TGetSecretRequestByIdDTO) => {
@@ -187,22 +186,22 @@ export const secretSharingServiceFactory = ({
} }
if (secretRequest.accessType === SecretSharingAccessType.Organization) { if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (orgId === undefined) { if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
orgId, secretRequest.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
} }
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) { if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
@@ -221,7 +220,6 @@ export const secretSharingServiceFactory = ({
id, id,
actor, actor,
actorId, actorId,
orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
secretValue secretValue
@@ -237,23 +235,23 @@ export const secretSharingServiceFactory = ({
let respondentUsername: string | undefined; let respondentUsername: string | undefined;
if (secretRequest.accessType === SecretSharingAccessType.Organization) { if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (!secretRequest.orgId) {
throw new BadRequestError({ message: "No organization ID present on secret request" });
}
if (!actorOrgId) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
orgId, secretRequest.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (!orgId) {
throw new UnauthorizedError();
}
if (secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
const user = await userDAL.findById(actorId); const user = await userDAL.findById(actorId);
if (!user) { if (!user) {
@@ -478,8 +476,14 @@ export const secretSharingServiceFactory = ({
? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type }) ? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
: await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type }); : await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) if (sharedSecret.userId !== actorId) {
throw new ForbiddenRequestError({
message: "User does not have permission to delete shared secret"
});
}
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) {
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" }); throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
}
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId); const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
@@ -57,12 +57,12 @@ export type TRevealSecretRequestValueDTO = {
export type TGetSecretRequestByIdDTO = { export type TGetSecretRequestByIdDTO = {
id: string; id: string;
} & TOrgPermission; } & Omit<TOrgPermission, "orgId">;
export type TSetSecretRequestValueDTO = { export type TSetSecretRequestValueDTO = {
id: string; id: string;
secretValue: string; secretValue: string;
} & TOrgPermission; } & Omit<TOrgPermission, "orgId">;
export type TDeleteSharedSecretDTO = { export type TDeleteSharedSecretDTO = {
sharedSecretId: string; sharedSecretId: string;
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
import { secretSharingKeys } from "./queries"; import { secretSharingKeys } from "./queries";
import { import {
TCreatedSharedSecret, TCreatedSharedSecret,
TCreateSecretRequestRequest, TCreateSecretRequestRequestDTO,
TCreateSharedSecretRequest, TCreateSharedSecretRequest,
TDeleteSecretRequestDTO, TDeleteSecretRequestDTO,
TDeleteSharedSecretRequestDTO, TDeleteSharedSecretRequestDTO,
@@ -48,7 +48,7 @@ export const useCreatePublicSharedSecret = () => {
export const useCreateSecretRequest = () => { export const useCreateSecretRequest = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (inputData: TCreateSecretRequestRequest) => { mutationFn: async (inputData: TCreateSecretRequestRequestDTO) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>( const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/requests", "/api/v1/secret-sharing/requests",
inputData inputData
@@ -6,6 +6,7 @@ export type TSharedSecret = {
updatedAt: Date; updatedAt: Date;
name: string | null; name: string | null;
lastViewedAt?: Date; lastViewedAt?: Date;
accessType: SecretSharingAccessType;
expiresAt: Date; expiresAt: Date;
expiresAfterViews: number | null; expiresAfterViews: number | null;
encryptedValue: string; encryptedValue: string;
@@ -33,7 +34,7 @@ export type TCreateSharedSecretRequest = {
accessType?: SecretSharingAccessType; accessType?: SecretSharingAccessType;
}; };
export type TCreateSecretRequestRequest = { export type TCreateSecretRequestRequestDTO = {
name?: string; name?: string;
accessType?: SecretSharingAccessType; accessType?: SecretSharingAccessType;
expiresAt: Date; expiresAt: Date;
@@ -3,7 +3,6 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { usePopUp } from "@app/hooks";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab"; import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab"; import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
@@ -14,14 +13,6 @@ enum SecretSharingPageTabs {
} }
export const ShareSecretSection = () => { export const ShareSecretSection = () => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"createSharedSecret",
"deleteSharedSecretConfirmation",
"createSecretRequest",
"deleteSecretRequestConfirmation",
"revealSecretRequestValue"
] as const);
const navigate = useNavigate(); const navigate = useNavigate();
const { selectedTab } = useSearch({ const { selectedTab } = useSearch({
@@ -54,20 +45,10 @@ export const ShareSecretSection = () => {
</Tab> </Tab>
</TabList> </TabList>
<TabPanel value={SecretSharingPageTabs.ShareSecret}> <TabPanel value={SecretSharingPageTabs.ShareSecret}>
<ShareSecretTab <ShareSecretTab />
handlePopUpOpen={handlePopUpOpen}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
handlePopUpClose={handlePopUpClose}
/>
</TabPanel> </TabPanel>
<TabPanel value={SecretSharingPageTabs.RequestSecret}> <TabPanel value={SecretSharingPageTabs.RequestSecret}>
<RequestSecretTab <RequestSecretTab />
handlePopUpOpen={handlePopUpOpen}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
handlePopUpClose={handlePopUpClose}
/>
</TabPanel> </TabPanel>
</Tabs> </Tabs>
</div> </div>
@@ -98,7 +98,12 @@ export const RequestSecretForm = () => {
name="expiresIn" name="expiresIn"
defaultValue="3600000" defaultValue="3600000"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}> <FormControl
label="Expires In"
errorText={error?.message}
tooltipText="Select for how long someone is able to input the secret. If a secret is shared with you in time, it will remain available to you, even after the expiration."
isError={Boolean(error)}
>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
@@ -4,41 +4,21 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { useDeleteSecretRequest } from "@app/hooks/api"; import { useDeleteSecretRequest } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { AddSecretRequestModal } from "./AddSecretRequestModal"; import { AddSecretRequestModal } from "./AddSecretRequestModal";
import { RequestedSecretsTable } from "./RequestedSecretsTable"; import { RequestedSecretsTable } from "./RequestedSecretsTable";
import { RevealSecretValueModal } from "./RevealSecretValueModal"; import { RevealSecretValueModal } from "./RevealSecretValueModal";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>,
data?: any
) => void;
popUp: UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>,
state?: boolean
) => void;
handlePopUpClose: (
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>
) => void;
};
type DeleteModalData = { name: string; id: string }; type DeleteModalData = { name: string; id: string };
export const RequestSecretTab = ({ export const RequestSecretTab = () => {
handlePopUpOpen, const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
popUp, "createSecretRequest",
handlePopUpToggle, "deleteSecretRequestConfirmation",
handlePopUpClose "revealSecretRequestValue"
}: Props) => { ] as const);
const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest(); const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest();
const onDeleteApproved = async () => { const onDeleteApproved = async () => {
@@ -4,7 +4,11 @@ import { format } from "date-fns";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { TSharedSecret, useRevealSecretRequestValue } from "@app/hooks/api/secretSharing"; import {
SecretSharingAccessType,
TSharedSecret,
useRevealSecretRequestValue
} from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
export const RequestedSecretsRow = ({ export const RequestedSecretsRow = ({
@@ -36,6 +40,23 @@ export const RequestedSecretsRow = ({
</Badge> </Badge>
)} )}
</Td> </Td>
<Td>
<Badge variant="primary">
<Tooltip
content={
row.accessType === SecretSharingAccessType.Anyone
? "Anyone can input the secret."
: "Only members of the organization can input the secret."
}
>
<div>
{row.accessType === SecretSharingAccessType.Anyone
? "Anyone"
: "Organization Members"}
</div>
</Tooltip>
</Badge>
</Td>
<Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td> <Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td>
<Td>{row.expiresAt ? format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a") : "-"}</Td> <Td>{row.expiresAt ? format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a") : "-"}</Td>
<Td> <Td>
@@ -43,7 +64,7 @@ export const RequestedSecretsRow = ({
<Tooltip <Tooltip
content={ content={
row.encryptedSecret row.encryptedSecret
? "Reveal shared secret" ? "Reveal secret"
: "Secret value must be provided before it can be viewed." : "Secret value must be provided before it can be viewed."
} }
> >
@@ -72,28 +93,30 @@ export const RequestedSecretsRow = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<IconButton <Tooltip content="Copy link">
isDisabled={Boolean(row.encryptedSecret) || isExpired} <IconButton
className={Boolean(row.encryptedSecret) || isExpired ? "opacity-50" : ""} isDisabled={Boolean(row.encryptedSecret) || isExpired}
onClick={async (e) => { className={Boolean(row.encryptedSecret) || isExpired ? "opacity-50" : ""}
e.stopPropagation(); onClick={async (e) => {
e.stopPropagation();
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.origin}/secret-request/secret/${row.id}` `${window.location.origin}/secret-request/secret/${row.id}`
); );
createNotification({ createNotification({
text: "Shared secret link copied to clipboard.", text: "Shared secret link copied to clipboard.",
type: "success" type: "success"
}); });
}} }}
variant="plain" variant="plain"
ariaLabel="copy link" ariaLabel="copy link"
> >
<FontAwesomeIcon icon={faCopy} /> <FontAwesomeIcon icon={faCopy} />
</IconButton> </IconButton>
</Tooltip>
<Tooltip content="Delete Secret Request"> <Tooltip content="Delete">
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -38,6 +38,7 @@ export const RequestedSecretsTable = ({ handlePopUpOpen }: Props) => {
<Tr> <Tr>
<Th>Name</Th> <Th>Name</Th>
<Th>Status</Th> <Th>Status</Th>
<Th>Access Type</Th>
<Th>Created At</Th> <Th>Created At</Th>
<Th>Valid Until</Th> <Th>Valid Until</Th>
<Th aria-label="button" className="w-5" /> <Th aria-label="button" className="w-5" />
@@ -64,7 +65,7 @@ export const RequestedSecretsTable = ({ handlePopUpOpen }: Props) => {
/> />
)} )}
{!isPending && !data?.secrets?.length && ( {!isPending && !data?.secrets?.length && (
<EmptyState title="No secrets shared yet" icon={faKey} /> <EmptyState title="No secrets requested yet" icon={faKey} />
)} )}
</TableContainer> </TableContainer>
); );
@@ -27,7 +27,7 @@ const Content = ({ secretValue, secretRequestName }: ContentProps) => {
</p> </p>
)} )}
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <div className="mb-8 flex items-center justify-between rounded-md bg-mineshaft-700 p-2 text-base text-gray-400">
<p className="mr-4 break-all">{secretValue}</p> <p className="mr-4 break-all">{secretValue}</p>
<Tooltip content="Click to copy"> <Tooltip content="Click to copy">
<IconButton <IconButton
@@ -46,7 +46,7 @@ const Content = ({ secretValue, secretRequestName }: ContentProps) => {
<div className="mt-8 flex w-full items-center justify-between gap-2"> <div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild> <ModalClose asChild>
<Button colorSchema="primary">Close</Button> <Button colorSchema="secondary">Close</Button>
</ModalClose> </ModalClose>
</div> </div>
</> </>
@@ -4,32 +4,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { useDeleteSharedSecret } from "@app/hooks/api"; import { useDeleteSharedSecret } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { AddShareSecretModal } from "./AddShareSecretModal"; import { AddShareSecretModal } from "./AddShareSecretModal";
import { ShareSecretsTable } from "./ShareSecretsTable"; import { ShareSecretsTable } from "./ShareSecretsTable";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
data?: any
) => void;
popUp: UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
state?: boolean
) => void;
handlePopUpClose: (popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>) => void;
};
type DeleteModalData = { name: string; id: string }; type DeleteModalData = { name: string; id: string };
export const ShareSecretTab = ({ export const ShareSecretTab = () => {
handlePopUpOpen, const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
popUp, "createSharedSecret",
handlePopUpToggle, "deleteSharedSecretConfirmation"
handlePopUpClose ] as const);
}: Props) => {
const deleteSecretShare = useDeleteSharedSecret(); const deleteSecretShare = useDeleteSharedSecret();
const onDeleteApproved = async () => { const onDeleteApproved = async () => {
@@ -6,7 +6,7 @@ export const SecretRequestErrorContainer = () => {
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center"> <div className="text-center">
<FontAwesomeIcon icon={faKey} size="2x" /> <FontAwesomeIcon icon={faKey} size="2x" />
<p className="mt-4">The secret request you are looking is missing or has expired.</p> <p className="mt-4">The secret request you are looking for is missing or has expired.</p>
</div> </div>
</div> </div>
); );