diff --git a/backend/src/db/migrations/20240728010334_secret-sharing-name.ts b/backend/src/db/migrations/20240728010334_secret-sharing-name.ts new file mode 100644 index 000000000..5bf43065f --- /dev/null +++ b/backend/src/db/migrations/20240728010334_secret-sharing-name.ts @@ -0,0 +1,39 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name"); + if (!doesNameExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.string("name").nullable(); + }); + } + + const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt"); + if (!doesLastViewedAtExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.timestamp("lastViewedAt").nullable(); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.SecretSharing)) { + const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name"); + if (doesNameExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.dropColumn("name"); + }); + } + + const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt"); + if (doesLastViewedAtExist) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + t.dropColumn("lastViewedAt"); + }); + } + } +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index c05f22b31..f4c525a4f 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { EnforcementLevel } from "@app/lib/types"; - import { TImmutableDBKeys } from "./models"; export const AccessApprovalPoliciesSchema = z.object({ @@ -17,7 +15,7 @@ export const AccessApprovalPoliciesSchema = z.object({ envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.string().default("hard") }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index 99df71f8d..60cbb2e3b 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({ isDisabled: z.boolean().default(false).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(), orgId: z.string().uuid(), + slug: z.string(), createdAt: z.date(), - updatedAt: z.date(), - slug: z.string() + updatedAt: z.date() }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/org-memberships.ts b/backend/src/db/schemas/org-memberships.ts index 7fc6f46eb..e77b6e9c9 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -18,7 +18,7 @@ export const OrgMembershipsSchema = z.object({ orgId: z.string().uuid(), roleId: z.string().uuid().nullable().optional(), projectFavorites: z.string().array().nullable().optional(), - isActive: z.boolean() + isActive: z.boolean().default(true) }); export type TOrgMemberships = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index 7ca0b71d9..218a0f922 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -15,12 +15,12 @@ export const SecretApprovalRequestsSchema = z.object({ conflicts: z.unknown().nullable().optional(), slug: z.string(), folderId: z.string().uuid(), - bypassReason: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), isReplicated: z.boolean().nullable().optional(), committerUserId: z.string().uuid(), - statusChangedByUserId: z.string().uuid().nullable().optional() + statusChangedByUserId: z.string().uuid().nullable().optional(), + bypassReason: z.string().nullable().optional() }); export type TSecretApprovalRequests = z.infer; diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 4406ad493..de75fbafc 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { SecretSharingAccessType } from "@app/lib/types"; - import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ @@ -18,10 +16,12 @@ export const SecretSharingSchema = z.object({ expiresAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(), - accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization), createdAt: z.date(), updatedAt: z.date(), - expiresAfterViews: z.number().nullable().optional() + expiresAfterViews: z.number().nullable().optional(), + accessType: z.string().default("anyone"), + name: z.string().nullable().optional(), + lastViewedAt: z.date().nullable().optional() }); export type TSecretSharing = z.infer; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index a53f879dd..a23c1f596 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -19,21 +19,31 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => rateLimit: readLimit }, schema: { + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0), + limit: z.coerce.number().min(1).max(100).default(25) + }), response: { - 200: z.array(SecretSharingSchema) + 200: z.object({ + secrets: z.array(SecretSharingSchema), + totalCount: z.number() + }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({ + const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({ actor: req.permission.type, actorId: req.permission.id, - orgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + ...req.query }); - return sharedSecrets; + return { + secrets, + totalCount + }; } }); @@ -48,7 +58,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => id: z.string().uuid() }), querystring: z.object({ - hashedHex: z.string() + hashedHex: z.string().min(1) }), response: { 200: SecretSharingSchema.pick({ @@ -64,11 +74,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex( - req.params.id, - req.query.hashedHex, - req.permission?.orgId - ); + const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({ + sharedSecretId: req.params.id, + hashedHex: req.query.hashedHex, + orgId: req.permission?.orgId + }); if (!sharedSecret) return undefined; return { encryptedValue: sharedSecret.encryptedValue, @@ -91,11 +101,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => schema: { body: z.object({ encryptedValue: z.string(), + hashedHex: z.string(), iv: z.string(), tag: z.string(), - hashedHex: z.string(), expiresAt: z.string(), - expiresAfterViews: z.number().optional() + expiresAfterViews: z.number().min(1).optional() }), response: { 200: z.object({ @@ -104,14 +114,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body; const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({ - encryptedValue, - iv, - tag, - hashedHex, - expiresAt, - expiresAfterViews, + ...req.body, accessType: SecretSharingAccessType.Anyone }); return { id: sharedSecret.id }; @@ -126,12 +130,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, schema: { body: z.object({ + name: z.string().max(50).optional(), encryptedValue: z.string(), + hashedHex: z.string(), iv: z.string(), tag: z.string(), - hashedHex: z.string(), expiresAt: z.string(), - expiresAfterViews: z.number().optional(), + expiresAfterViews: z.number().min(1).optional(), accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) }), response: { @@ -142,20 +147,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body; const sharedSecret = await req.server.services.secretSharing.createSharedSecret({ actor: req.permission.type, actorId: req.permission.id, orgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - encryptedValue, - iv, - tag, - hashedHex, - expiresAt, - expiresAfterViews, - accessType: req.body.accessType + ...req.body }); return { id: sharedSecret.id }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 16b66d871..d1b4edd4b 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -10,6 +10,25 @@ export type TSecretSharingDALFactory = ReturnType { const sharedSecretOrm = ormify(db, TableName.SecretSharing); + const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => { + try { + interface CountResult { + count: string; + } + + const count = await db + .replicaNode()(TableName.SecretSharing) + .where(`${TableName.SecretSharing}.orgId`, orgId) + .where(`${TableName.SecretSharing}.userId`, userId) + .count("*") + .first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all user-org shared secrets" }); + } + }; + const pruneExpiredSharedSecrets = async (tx?: Knex) => { try { const today = new Date(); @@ -19,8 +38,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { .update({ encryptedValue: "", tag: "", - iv: "", - hashedHex: "" + iv: "" }); return docs; } catch (error) { @@ -50,8 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { await sharedSecretOrm.updateById(id, { encryptedValue: "", iv: "", - tag: "", - hashedHex: "" + tag: "" }); } catch (error) { throw new DatabaseError({ @@ -63,6 +80,7 @@ export const secretSharingDALFactory = (db: TDbClient) => { return { ...sharedSecretOrm, + countAllUserOrgSharedSecrets, pruneExpiredSharedSecrets, softDeleteById, findActiveSharedSecrets diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index da2f52534..1f38bf1f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,5 +1,5 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { SecretSharingAccessType } from "@app/lib/types"; import { TOrgDALFactory } from "../org/org-dal"; @@ -8,7 +8,8 @@ import { TCreatePublicSharedSecretDTO, TCreateSharedSecretDTO, TDeleteSharedSecretDTO, - TSharedSecretPermission + TGetActiveSharedSecretByIdDTO, + TGetSharedSecretsDTO } from "./secret-sharing-types"; type TSecretSharingServiceFactoryDep = { @@ -24,21 +25,21 @@ export const secretSharingServiceFactory = ({ secretSharingDAL, orgDAL }: TSecretSharingServiceFactoryDep) => { - const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { - const { - actor, - actorId, - orgId, - actorAuthMethod, - actorOrgId, - encryptedValue, - iv, - tag, - accessType, - hashedHex, - expiresAt, - expiresAfterViews - } = createSharedSecretInput; + const createSharedSecret = async ({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + encryptedValue, + hashedHex, + iv, + tag, + name, + accessType, + expiresAt, + expiresAfterViews + }: TCreateSharedSecretDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); @@ -60,10 +61,11 @@ export const secretSharingServiceFactory = ({ } const newSharedSecret = await secretSharingDAL.create({ + name, encryptedValue, + hashedHex, iv, tag, - hashedHex, expiresAt: new Date(expiresAt), expiresAfterViews, userId: actorId, @@ -74,8 +76,15 @@ export const secretSharingServiceFactory = ({ return { id: newSharedSecret.id }; }; - const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput; + const createPublicSharedSecret = async ({ + encryptedValue, + hashedHex, + iv, + tag, + expiresAt, + expiresAfterViews, + accessType + }: TCreatePublicSharedSecretDTO) => { if (new Date(expiresAt) < new Date()) { throw new BadRequestError({ message: "Expiration date cannot be in the past" }); } @@ -95,9 +104,9 @@ export const secretSharingServiceFactory = ({ const newSharedSecret = await secretSharingDAL.create({ encryptedValue, + hashedHex, iv, tag, - hashedHex, expiresAt: new Date(expiresAt), expiresAfterViews, accessType @@ -105,43 +114,93 @@ export const secretSharingServiceFactory = ({ return { id: newSharedSecret.id }; }; - const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => { - const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const getSharedSecrets = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + offset, + limit + }: TGetSharedSecretsDTO) => { + if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); - const userSharedSecrets = await secretSharingDAL.findActiveSharedSecrets({ userId: actorId, orgId }); - return userSharedSecrets; + + const secrets = await secretSharingDAL.find( + { + userId: actorId, + orgId: actorOrgId + }, + { offset, limit, sort: [["createdAt", "desc"]] } + ); + + const count = await secretSharingDAL.countAllUserOrgSharedSecrets({ + orgId: actorOrgId, + userId: actorId + }); + + return { + secrets, + totalCount: count + }; }; - const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => { - const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); - if (!sharedSecret) return; + const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => { + const sharedSecret = await secretSharingDAL.findOne({ + id: sharedSecretId, + hashedHex + }); + if (!sharedSecret) + throw new NotFoundError({ + message: "Shared secret not found" + }); + + const { accessType, expiresAt, expiresAfterViews } = sharedSecret; const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - // Support organization level access for secret sharing - if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { - return { - ...sharedSecret, - encryptedValue: "", - iv: "", - tag: "", - orgName - }; + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + throw new UnauthorizedError(); + + if (expiresAt !== null && expiresAt < new Date()) { + // check lifetime expiry + await secretSharingDAL.softDeleteById(sharedSecretId); + throw new ForbiddenRequestError({ + message: "Access denied: Secret has expired by lifetime" + }); } - if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) { - return; + + if (expiresAfterViews !== null && expiresAfterViews === 0) { + // check view count expiry + await secretSharingDAL.softDeleteById(sharedSecretId); + throw new ForbiddenRequestError({ + message: "Access denied: Secret has expired by view count" + }); } - if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) { - if (sharedSecret.expiresAfterViews === 0) { - await secretSharingDAL.softDeleteById(sharedSecretId); - return; - } + + if (expiresAfterViews) { + // decrement view count if view count expiry set await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); } - if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) { - return { ...sharedSecret, orgName }; - } - return { ...sharedSecret, orgName: undefined }; + + await secretSharingDAL.updateById(sharedSecretId, { + lastViewedAt: new Date() + }); + + return { + ...sharedSecret, + orgName: + sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId + ? orgName + : undefined + }; }; const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { @@ -157,6 +216,6 @@ export const secretSharingServiceFactory = ({ createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, - getActiveSharedSecretByIdAndHashedHex + getActiveSharedSecretById }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index bb0e19d5b..76d723c62 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -1,7 +1,12 @@ -import { SecretSharingAccessType } from "@app/lib/types"; +import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +export type TGetSharedSecretsDTO = { + offset: number; + limit: number; +} & TGenericPermission; + export type TSharedSecretPermission = { actor: ActorType; actorId: string; @@ -9,18 +14,25 @@ export type TSharedSecretPermission = { actorOrgId: string; orgId: string; accessType?: SecretSharingAccessType; + name?: string; }; export type TCreatePublicSharedSecretDTO = { encryptedValue: string; + hashedHex: string; iv: string; tag: string; - hashedHex: string; expiresAt: string; expiresAfterViews?: number; accessType: SecretSharingAccessType; }; +export type TGetActiveSharedSecretByIdDTO = { + sharedSecretId: string; + hashedHex: string; + orgId?: string; +}; + export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TDeleteSharedSecretDTO = { diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 7e19ece33..08e0b59ba 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -29,6 +29,7 @@ export * from "./secretFolders"; export * from "./secretImports"; export * from "./secretRotation"; export * from "./secrets"; +export * from "./secretSharing"; export * from "./secretSnapshots"; export * from "./serverDetails"; export * from "./serviceTokens"; diff --git a/frontend/src/hooks/api/secretSharing/mutations.ts b/frontend/src/hooks/api/secretSharing/mutations.ts index e0c1dcc3c..7dec0bd5e 100644 --- a/frontend/src/hooks/api/secretSharing/mutations.ts +++ b/frontend/src/hooks/api/secretSharing/mutations.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { secretSharingKeys } from "./queries"; import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types"; export const useCreateSharedSecret = () => { @@ -11,7 +12,7 @@ export const useCreateSharedSecret = () => { const { data } = await apiRequest.post("/api/v1/secret-sharing", inputData); return data; }, - onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"]) + onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets()) }); }; @@ -25,7 +26,7 @@ export const useCreatePublicSharedSecret = () => { ); return data; }, - onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"]) + onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets()) }); }; @@ -38,8 +39,6 @@ export const useDeleteSharedSecret = () => { ); return data; }, - onSuccess: () => { - queryClient.invalidateQueries(["sharedSecrets"]); - } + onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets()) }); }; diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index 44b2c3193..89a804f6e 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -2,24 +2,59 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { SecretSharingAccessType, TSharedSecret, TViewSharedSecretResponse } from "./types"; +import { TSharedSecret, TViewSharedSecretResponse } from "./types"; -export const useGetSharedSecrets = () => { +export const secretSharingKeys = { + allSharedSecrets: () => ["sharedSecrets"] as const, + specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) => + [...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const +}; + +export const useGetSharedSecrets = ({ + offset = 0, + limit = 25 +}: { + offset: number; + limit: number; +}) => { return useQuery({ - queryKey: ["sharedSecrets"], + queryKey: secretSharingKeys.specificSharedSecrets({ offset, limit }), queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/secret-sharing/"); + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }); + + const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>( + "/api/v1/secret-sharing/", + { + params + } + ); return data; } }); }; -export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => { +export const useGetActiveSharedSecretById = ({ + sharedSecretId, + hashedHex +}: { + sharedSecretId: string; + hashedHex: string; +}) => { return useQuery({ + enabled: Boolean(sharedSecretId) && Boolean(hashedHex), queryFn: async () => { - if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "", accessType: SecretSharingAccessType.Organization, orgName: "" }); + const params = new URLSearchParams({ + hashedHex + }); + const { data } = await apiRequest.get( - `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}` + `/api/v1/secret-sharing/public/${sharedSecretId}`, + { + params + } ); return { encryptedValue: data.encryptedValue, diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 3a9576e1e..d35fea8d6 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -4,16 +4,24 @@ export type TSharedSecret = { orgId: string; createdAt: Date; updatedAt: Date; -} & TCreateSharedSecretRequest; - -export type TCreateSharedSecretRequest = { + name: string | null; + lastViewedAt?: Date; + expiresAt: Date; + expiresAfterViews: number | null; encryptedValue: string; iv: string; tag: string; +}; + +export type TCreateSharedSecretRequest = { + name?: string; + encryptedValue: string; hashedHex: string; + iv: string; + tag: string; expiresAt: Date; expiresAfterViews?: number; - accessType: SecretSharingAccessType; + accessType?: SecretSharingAccessType; }; export type TViewSharedSecretResponse = { diff --git a/frontend/src/pages/share-secret/index.tsx b/frontend/src/pages/share-secret/index.tsx index 53b034650..8fb2b26c4 100644 --- a/frontend/src/pages/share-secret/index.tsx +++ b/frontend/src/pages/share-secret/index.tsx @@ -13,7 +13,7 @@ const ShareNewPublicSecretPage = () => {
- +
); diff --git a/frontend/src/pages/shared/secret/[id]/index.tsx b/frontend/src/pages/shared/secret/[id]/index.tsx index bda56347b..71c670fad 100644 --- a/frontend/src/pages/shared/secret/[id]/index.tsx +++ b/frontend/src/pages/shared/secret/[id]/index.tsx @@ -1,6 +1,6 @@ import Head from "next/head"; -import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage"; +import { ViewSecretPublicPage } from "@app/views/ViewSecretPublicPage"; const SecretSharedPublicPage = () => { return ( @@ -12,9 +12,7 @@ const SecretSharedPublicPage = () => { -
- -
+ ); }; diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 2709590e2..147e9d2b8 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -404,12 +404,7 @@ export const SecretListView = ({ isOpen={popUp.createTag.isOpen} onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)} /> - + ); }; diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx deleted file mode 100644 index abed9b289..000000000 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import crypto from "crypto"; - -import { useEffect, useRef } from "react"; -import { Controller } from "react-hook-form"; -import { AxiosError } from "axios"; -import * as yup from "yup"; - -import { createNotification } from "@app/components/notifications"; -import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; -import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2"; -import { - SecretSharingAccessType, - useCreatePublicSharedSecret, - useCreateSharedSecret -} from "@app/hooks/api/secretSharing"; - -const schema = yup.object({ - value: yup.string().max(10000).required().label("Shared Secret Value"), - expiresAfterViews: yup.string().required().label("Expires After Views"), - expiresInValue: yup.string().min(1).required().label("Expiration Value"), - accessType: yup.string().required().label("General Access") -}); - -export type FormData = yup.InferType; - -// values in ms -const expiresInOptions = [ - { label: "5 min", value: 5 * 60 * 1000 }, - { label: "30 min", value: 30 * 60 * 1000 }, - { label: "1 hour", value: 60 * 60 * 1000 }, - { label: "1 day", value: 24 * 60 * 60 * 1000 }, - { label: "7 days", value: 7 * 24 * 60 * 60 * 1000 }, - { label: "14 days", value: 14 * 24 * 60 * 60 * 1000 }, - { label: "30 days", value: 30 * 24 * 60 * 60 * 1000 } -]; - -const viewLimitOptions = [ - { label: "1", value: 1 }, - { label: "Unlimited", value: -1 } -]; - -export const AddShareSecretForm = ({ - isPublic, - inModal, - handleSubmit, - control, - isSubmitting, - setNewSharedSecret, - isInputDisabled -}: { - isPublic: boolean; - inModal: boolean; - handleSubmit: any; - control: any; - isSubmitting: boolean; - setNewSharedSecret: (value: string) => void; - isInputDisabled?: boolean; -}) => { - const isMounted = useRef(true); - - useEffect(() => { - return () => { - isMounted.current = false; - }; - }, []); - - const publicSharedSecretCreator = useCreatePublicSharedSecret(); - const privateSharedSecretCreator = useCreateSharedSecret(); - const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator; - - const onFormSubmit = async ({ - value, - expiresInValue, - expiresAfterViews, - accessType - }: FormData) => { - try { - const expiresAt = new Date(new Date().getTime() + Number(expiresInValue)); - - const key = crypto.randomBytes(16).toString("hex"); - const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: value, - key - }); - - const { id } = await createSharedSecret.mutateAsync({ - encryptedValue: ciphertext, - iv, - tag, - hashedHex, - expiresAt, - expiresAfterViews: expiresAfterViews === "-1" ? undefined : Number(expiresAfterViews), - accessType: accessType as SecretSharingAccessType - }); - - if (isMounted.current) { - setNewSharedSecret( - `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( - hashedHex - )}-${encodeURIComponent(key)}` - ); - createNotification({ - text: "Successfully created a shared secret", - type: "success" - }); - } - } catch (err) { - console.error(err); - const axiosError = err as AxiosError; - if (axiosError?.response?.status === 401) { - createNotification({ - text: "You do not have access to create shared secrets", - type: "error" - }); - } else { - createNotification({ - text: "Failed to create a shared secret", - type: "error" - }); - } - } - }; - return ( -
-
-
- ( - -