mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 14:26:38 +00:00
Merge pull request #2190 from Infisical/secret-sharing-update
Secret Sharing Update
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const AccessApprovalPoliciesSchema = z.object({
|
export const AccessApprovalPoliciesSchema = z.object({
|
||||||
@@ -17,7 +15,7 @@ export const AccessApprovalPoliciesSchema = z.object({
|
|||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
enforcementLevel: z.string().default("hard")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export const KmsKeysSchema = z.object({
|
|||||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||||
isReserved: z.boolean().default(true).nullable().optional(),
|
isReserved: z.boolean().default(true).nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
|
slug: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date()
|
||||||
slug: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const OrgMembershipsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
roleId: z.string().uuid().nullable().optional(),
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
projectFavorites: z.string().array().nullable().optional(),
|
projectFavorites: z.string().array().nullable().optional(),
|
||||||
isActive: z.boolean()
|
isActive: z.boolean().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export const SecretApprovalRequestsSchema = z.object({
|
|||||||
conflicts: z.unknown().nullable().optional(),
|
conflicts: z.unknown().nullable().optional(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
bypassReason: z.string().nullable().optional(),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isReplicated: z.boolean().nullable().optional(),
|
isReplicated: z.boolean().nullable().optional(),
|
||||||
committerUserId: z.string().uuid(),
|
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<typeof SecretApprovalRequestsSchema>;
|
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
@@ -18,10 +16,12 @@ export const SecretSharingSchema = z.object({
|
|||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: 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<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
|||||||
@@ -19,21 +19,31 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
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: {
|
response: {
|
||||||
200: z.array(SecretSharingSchema)
|
200: z.object({
|
||||||
|
secrets: z.array(SecretSharingSchema),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
|
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
orgId: req.permission.orgId,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
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()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
hashedHex: z.string()
|
hashedHex: z.string().min(1)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema.pick({
|
200: SecretSharingSchema.pick({
|
||||||
@@ -64,11 +74,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
|
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
|
||||||
req.params.id,
|
sharedSecretId: req.params.id,
|
||||||
req.query.hashedHex,
|
hashedHex: req.query.hashedHex,
|
||||||
req.permission?.orgId
|
orgId: req.permission?.orgId
|
||||||
);
|
});
|
||||||
if (!sharedSecret) return undefined;
|
if (!sharedSecret) return undefined;
|
||||||
return {
|
return {
|
||||||
encryptedValue: sharedSecret.encryptedValue,
|
encryptedValue: sharedSecret.encryptedValue,
|
||||||
@@ -91,11 +101,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().optional()
|
expiresAfterViews: z.number().min(1).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -104,14 +114,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
|
||||||
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||||
encryptedValue,
|
...req.body,
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
hashedHex,
|
|
||||||
expiresAt,
|
|
||||||
expiresAfterViews,
|
|
||||||
accessType: SecretSharingAccessType.Anyone
|
accessType: SecretSharingAccessType.Anyone
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
@@ -126,12 +130,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
name: z.string().max(50).optional(),
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number().optional(),
|
expiresAfterViews: z.number().min(1).optional(),
|
||||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -142,20 +147,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
|
||||||
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
encryptedValue,
|
...req.body
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
hashedHex,
|
|
||||||
expiresAt,
|
|
||||||
expiresAfterViews,
|
|
||||||
accessType: req.body.accessType
|
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,25 @@ export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory
|
|||||||
export const secretSharingDALFactory = (db: TDbClient) => {
|
export const secretSharingDALFactory = (db: TDbClient) => {
|
||||||
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
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) => {
|
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -19,8 +38,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
.update({
|
.update({
|
||||||
encryptedValue: "",
|
encryptedValue: "",
|
||||||
tag: "",
|
tag: "",
|
||||||
iv: "",
|
iv: ""
|
||||||
hashedHex: ""
|
|
||||||
});
|
});
|
||||||
return docs;
|
return docs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -50,8 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
await sharedSecretOrm.updateById(id, {
|
await sharedSecretOrm.updateById(id, {
|
||||||
encryptedValue: "",
|
encryptedValue: "",
|
||||||
iv: "",
|
iv: "",
|
||||||
tag: "",
|
tag: ""
|
||||||
hashedHex: ""
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({
|
throw new DatabaseError({
|
||||||
@@ -63,6 +80,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedSecretOrm,
|
...sharedSecretOrm,
|
||||||
|
countAllUserOrgSharedSecrets,
|
||||||
pruneExpiredSharedSecrets,
|
pruneExpiredSharedSecrets,
|
||||||
softDeleteById,
|
softDeleteById,
|
||||||
findActiveSharedSecrets
|
findActiveSharedSecrets
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
TCreatePublicSharedSecretDTO,
|
TCreatePublicSharedSecretDTO,
|
||||||
TCreateSharedSecretDTO,
|
TCreateSharedSecretDTO,
|
||||||
TDeleteSharedSecretDTO,
|
TDeleteSharedSecretDTO,
|
||||||
TSharedSecretPermission
|
TGetActiveSharedSecretByIdDTO,
|
||||||
|
TGetSharedSecretsDTO
|
||||||
} from "./secret-sharing-types";
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
@@ -24,21 +25,21 @@ export const secretSharingServiceFactory = ({
|
|||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL
|
orgDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
const createSharedSecret = async ({
|
||||||
const {
|
actor,
|
||||||
actor,
|
actorId,
|
||||||
actorId,
|
orgId,
|
||||||
orgId,
|
actorAuthMethod,
|
||||||
actorAuthMethod,
|
actorOrgId,
|
||||||
actorOrgId,
|
encryptedValue,
|
||||||
encryptedValue,
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
accessType,
|
name,
|
||||||
hashedHex,
|
accessType,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews
|
expiresAfterViews
|
||||||
} = createSharedSecretInput;
|
}: TCreateSharedSecretDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
|
||||||
@@ -60,10 +61,11 @@ export const secretSharingServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
name,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
hashedHex,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
@@ -74,8 +76,15 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
const createPublicSharedSecret = async ({
|
||||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput;
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews,
|
||||||
|
accessType
|
||||||
|
}: TCreatePublicSharedSecretDTO) => {
|
||||||
if (new Date(expiresAt) < new Date()) {
|
if (new Date(expiresAt) < new Date()) {
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
}
|
}
|
||||||
@@ -95,9 +104,9 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
|
hashedHex,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
hashedHex,
|
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
accessType
|
accessType
|
||||||
@@ -105,43 +114,93 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
const getSharedSecrets = async ({
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
actor,
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
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" });
|
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 getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
const sharedSecret = await secretSharingDAL.findOne({
|
||||||
if (!sharedSecret) return;
|
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 : "";
|
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) {
|
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
|
||||||
return {
|
throw new UnauthorizedError();
|
||||||
...sharedSecret,
|
|
||||||
encryptedValue: "",
|
if (expiresAt !== null && expiresAt < new Date()) {
|
||||||
iv: "",
|
// check lifetime expiry
|
||||||
tag: "",
|
await secretSharingDAL.softDeleteById(sharedSecretId);
|
||||||
orgName
|
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) {
|
if (expiresAfterViews) {
|
||||||
await secretSharingDAL.softDeleteById(sharedSecretId);
|
// decrement view count if view count expiry set
|
||||||
return;
|
|
||||||
}
|
|
||||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||||
}
|
}
|
||||||
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) {
|
|
||||||
return { ...sharedSecret, orgName };
|
await secretSharingDAL.updateById(sharedSecretId, {
|
||||||
}
|
lastViewedAt: new Date()
|
||||||
return { ...sharedSecret, orgName: undefined };
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sharedSecret,
|
||||||
|
orgName:
|
||||||
|
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||||
|
? orgName
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
||||||
@@ -157,6 +216,6 @@ export const secretSharingServiceFactory = ({
|
|||||||
createPublicSharedSecret,
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getActiveSharedSecretByIdAndHashedHex
|
getActiveSharedSecretById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type TGetSharedSecretsDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TSharedSecretPermission = {
|
export type TSharedSecretPermission = {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -9,18 +14,25 @@ export type TSharedSecretPermission = {
|
|||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
accessType?: SecretSharingAccessType;
|
accessType?: SecretSharingAccessType;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatePublicSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
|
hashedHex: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
expiresAfterViews?: number;
|
expiresAfterViews?: number;
|
||||||
accessType: SecretSharingAccessType;
|
accessType: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetActiveSharedSecretByIdDTO = {
|
||||||
|
sharedSecretId: string;
|
||||||
|
hashedHex: string;
|
||||||
|
orgId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export * from "./secretFolders";
|
|||||||
export * from "./secretImports";
|
export * from "./secretImports";
|
||||||
export * from "./secretRotation";
|
export * from "./secretRotation";
|
||||||
export * from "./secrets";
|
export * from "./secrets";
|
||||||
|
export * from "./secretSharing";
|
||||||
export * from "./secretSnapshots";
|
export * from "./secretSnapshots";
|
||||||
export * from "./serverDetails";
|
export * from "./serverDetails";
|
||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { secretSharingKeys } from "./queries";
|
||||||
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
||||||
|
|
||||||
export const useCreateSharedSecret = () => {
|
export const useCreateSharedSecret = () => {
|
||||||
@@ -11,7 +12,7 @@ export const useCreateSharedSecret = () => {
|
|||||||
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ export const useCreatePublicSharedSecret = () => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,8 +39,6 @@ export const useDeleteSharedSecret = () => {
|
|||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => queryClient.invalidateQueries(secretSharingKeys.allSharedSecrets())
|
||||||
queryClient.invalidateQueries(["sharedSecrets"]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,24 +2,59 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
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({
|
return useQuery({
|
||||||
queryKey: ["sharedSecrets"],
|
queryKey: secretSharingKeys.specificSharedSecrets({ offset, limit }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<TSharedSecret[]>("/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;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
export const useGetActiveSharedSecretById = ({
|
||||||
|
sharedSecretId,
|
||||||
|
hashedHex
|
||||||
|
}: {
|
||||||
|
sharedSecretId: string;
|
||||||
|
hashedHex: string;
|
||||||
|
}) => {
|
||||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
|
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
|
||||||
queryFn: async () => {
|
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<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||||
|
{
|
||||||
|
params
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
encryptedValue: data.encryptedValue,
|
encryptedValue: data.encryptedValue,
|
||||||
|
|||||||
@@ -4,16 +4,24 @@ export type TSharedSecret = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
} & TCreateSharedSecretRequest;
|
name: string | null;
|
||||||
|
lastViewedAt?: Date;
|
||||||
export type TCreateSharedSecretRequest = {
|
expiresAt: Date;
|
||||||
|
expiresAfterViews: number | null;
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretRequest = {
|
||||||
|
name?: string;
|
||||||
|
encryptedValue: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews?: number;
|
expiresAfterViews?: number;
|
||||||
accessType: SecretSharingAccessType;
|
accessType?: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const ShareNewPublicSecretPage = () => {
|
|||||||
<meta name="og:description" content="" />
|
<meta name="og:description" content="" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="dark h-full">
|
<div className="dark h-full">
|
||||||
<ShareSecretPublicPage isNewSession />
|
<ShareSecretPublicPage />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
import { ViewSecretPublicPage } from "@app/views/ViewSecretPublicPage";
|
||||||
|
|
||||||
const SecretSharedPublicPage = () => {
|
const SecretSharedPublicPage = () => {
|
||||||
return (
|
return (
|
||||||
@@ -12,9 +12,7 @@ const SecretSharedPublicPage = () => {
|
|||||||
<meta property="og:title" content="" />
|
<meta property="og:title" content="" />
|
||||||
<meta name="og:description" content="" />
|
<meta name="og:description" content="" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="dark h-full">
|
<ViewSecretPublicPage />
|
||||||
<ShareSecretPublicPage isNewSession={false} />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -404,12 +404,7 @@ export const SecretListView = ({
|
|||||||
isOpen={popUp.createTag.isOpen}
|
isOpen={popUp.createTag.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||||
/>
|
/>
|
||||||
<AddShareSecretModal
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
isPublic={false}
|
|
||||||
inModal
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<typeof schema>;
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<form
|
|
||||||
className="flex w-full max-w-7xl flex-col items-center"
|
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-full ${
|
|
||||||
!inModal && "rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="value"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Your Secret"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-2"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
disabled={isInputDisabled}
|
|
||||||
placeholder="Enter sensitive data to share via an encrypted link..."
|
|
||||||
{...field}
|
|
||||||
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 py-1.5 px-2 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresInValue"
|
|
||||||
defaultValue="3600000"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{expiresInOptions.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresAfterViews"
|
|
||||||
defaultValue="-1"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{viewLimitOptions.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value || "")} key={label}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!isPublic && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="accessType"
|
|
||||||
defaultValue="organization"
|
|
||||||
render={({ field: { onChange, ...field } }) => (
|
|
||||||
<FormControl label="General Access">
|
|
||||||
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
|
|
||||||
<SelectItem value="organization">People within your organization</SelectItem>
|
|
||||||
<SelectItem value="anyone">Anyone</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={`flex items-center space-x-4 pt-2 ${!inModal && ""}`}>
|
|
||||||
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
|
||||||
{inModal ? "Create" : "Create secret link"}
|
|
||||||
</Button>
|
|
||||||
{inModal && (
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,22 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
import { useTimedReset } from "@app/hooks";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
import { ShareSecretForm } from "@app/views/ShareSecretPublicPage/components";
|
||||||
import { AddShareSecretForm } from "./AddShareSecretForm";
|
|
||||||
import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
|
|
||||||
|
|
||||||
const schema = yup.object({
|
|
||||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
|
||||||
expiresInValue: yup.string().required().label("Expiration Value"),
|
|
||||||
expiresAfterViews: yup.string().required().label("Expires After Views")
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["createSharedSecret"]>;
|
popUp: UsePopUpState<["createSharedSecret"]>;
|
||||||
@@ -24,97 +8,25 @@ type Props = {
|
|||||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
isPublic: boolean;
|
|
||||||
inModal: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
|
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const {
|
return (
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: yupResolver(schema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const [newSharedSecret, setNewSharedSecret] = useState("");
|
|
||||||
const hasSharedSecret = Boolean(newSharedSecret);
|
|
||||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
|
||||||
initialState: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSecretInputDisabled, setIsSecretInputDisabled] = useState(false);
|
|
||||||
|
|
||||||
const copyUrlToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(newSharedSecret);
|
|
||||||
setIsUrlCopied(true);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (isUrlCopied) {
|
|
||||||
setTimeout(() => setIsUrlCopied(false), 2000);
|
|
||||||
}
|
|
||||||
}, [isUrlCopied]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (popUp.createSharedSecret.data) {
|
|
||||||
setValue("value", (popUp.createSharedSecret.data as { value: string }).value);
|
|
||||||
setIsSecretInputDisabled(true);
|
|
||||||
}
|
|
||||||
}, [popUp.createSharedSecret.data]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
return inModal ? (
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.createSharedSecret?.isOpen}
|
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("createSharedSecret", open);
|
handlePopUpToggle("createSharedSecret", isOpen);
|
||||||
reset();
|
|
||||||
setNewSharedSecret("");
|
|
||||||
setIsSecretInputDisabled(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Share a Secret"
|
title="Share a Secret"
|
||||||
subTitle="Once you share a secret, the share link is only accessible once."
|
subTitle="Once you share a secret, the share link is only accessible once."
|
||||||
>
|
>
|
||||||
{!hasSharedSecret ? (
|
<ShareSecretForm
|
||||||
<AddShareSecretForm
|
isPublic={false}
|
||||||
isPublic={isPublic}
|
value={(popUp.createSharedSecret.data as { value?: string })?.value}
|
||||||
inModal={inModal}
|
/>
|
||||||
control={control}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
setNewSharedSecret={setNewSharedSecret}
|
|
||||||
isInputDisabled={isSecretInputDisabled}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ViewAndCopySharedSecret
|
|
||||||
inModal={inModal}
|
|
||||||
newSharedSecret={newSharedSecret}
|
|
||||||
isUrlCopied={isUrlCopied}
|
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : !hasSharedSecret ? (
|
|
||||||
<AddShareSecretForm
|
|
||||||
isPublic={isPublic}
|
|
||||||
inModal={inModal}
|
|
||||||
control={control}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
setNewSharedSecret={setNewSharedSecret}
|
|
||||||
isInputDisabled={isSecretInputDisabled}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ViewAndCopySharedSecret
|
|
||||||
inModal={inModal}
|
|
||||||
newSharedSecret={newSharedSecret}
|
|
||||||
isUrlCopied={isUrlCopied}
|
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,12 +59,7 @@ export const ShareSecretSection = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<AddShareSecretModal
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
isPublic={false}
|
|
||||||
inModal
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||||
title={`Delete ${
|
title={`Delete ${
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faEnvelope, faEnvelopeOpen, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { IconButton, Td, Tr } from "@app/components/v2";
|
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
// import { useToggle } from "@app/hooks";
|
||||||
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
import { TSharedSecret } from "@app/hooks/api/secretSharing";
|
import { TSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -22,26 +24,71 @@ export const ShareSecretsRow = ({
|
|||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
// const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const lastViewedAt = row.lastViewedAt
|
||||||
|
? format(new Date(row.lastViewedAt), "yyyy-MM-dd - HH:mm a")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let isExpired = false;
|
||||||
|
if (row.expiresAfterViews !== null && row.expiresAfterViews <= 0) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.expiresAt !== null && new Date(row.expiresAt) < new Date()) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={row.id} className="h-10">
|
<>
|
||||||
<Td>{`${row.encryptedValue.substring(0, 5)}...`}</Td>
|
<Tr
|
||||||
<Td>{format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}</Td>
|
key={row.id}
|
||||||
<Td>{format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a")}</Td>
|
// className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||||
<Td>{row.expiresAfterViews ? row.expiresAfterViews : "-"}</Td>
|
// onClick={() => setIsRowExpanded.toggle()}
|
||||||
<Td>
|
>
|
||||||
<IconButton
|
<Td>
|
||||||
onClick={() =>
|
<Tooltip content={lastViewedAt ? `Last opened at ${lastViewedAt}` : "Not yet opened"}>
|
||||||
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
<FontAwesomeIcon icon={lastViewedAt ? faEnvelopeOpen : faEnvelope} />
|
||||||
name: "delete",
|
</Tooltip>
|
||||||
id: row.id
|
</Td>
|
||||||
})
|
<Td>{row.name ? `${row.name}` : "-"}</Td>
|
||||||
}
|
<Td>
|
||||||
variant="plain"
|
<Badge variant={isExpired ? "danger" : "success"}>
|
||||||
ariaLabel="delete"
|
{isExpired ? "Expired" : "Active"}
|
||||||
>
|
</Badge>
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
</Td>
|
||||||
</IconButton>
|
<Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td>
|
||||||
</Td>
|
<Td>{format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a")}</Td>
|
||||||
</Tr>
|
<Td>{row.expiresAfterViews !== null ? row.expiresAfterViews : "-"}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
||||||
|
name: "delete",
|
||||||
|
id: row.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="delete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{/* {isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
colSpan={6}
|
||||||
|
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>Test 1</div>
|
||||||
|
<div>Test 2</div>
|
||||||
|
<div>Test 3</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)} */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
@@ -30,34 +31,49 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const { isLoading, data = [] } = useGetSharedSecrets();
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(10);
|
||||||
|
const { isLoading, data } = useGetSharedSecrets({
|
||||||
|
offset: (page - 1) * perPage,
|
||||||
|
limit: perPage
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Encrypted Secret</Th>
|
<Th className="w-5" />
|
||||||
<Th>Created</Th>
|
<Th>Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
<Th>Valid Until</Th>
|
<Th>Valid Until</Th>
|
||||||
<Th>Views Left</Th>
|
<Th>Views Left</Th>
|
||||||
<Th aria-label="button" className="w-5" />
|
<Th aria-label="button" className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="shared-secrets" />}
|
{isLoading && <TableSkeleton columns={7} innerKey="shared-secrets" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data?.map((row) => (
|
data?.secrets?.map((row) => (
|
||||||
<ShareSecretsRow key={row.id} row={row} handlePopUpOpen={handlePopUpOpen} />
|
<ShareSecretsRow key={row.id} row={row} handlePopUpOpen={handlePopUpOpen} />
|
||||||
))}
|
))}
|
||||||
{!isLoading && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
|
||||||
<EmptyState title="No secrets shared yet" icon={faKey} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isLoading &&
|
||||||
|
data?.secrets &&
|
||||||
|
data.secrets.length >= perPage &&
|
||||||
|
data?.totalCount !== undefined && (
|
||||||
|
<Pagination
|
||||||
|
count={data.totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && !data?.secrets?.length && (
|
||||||
|
<EmptyState title="No secrets shared yet" icon={faKey} />
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { IconButton } from "@app/components/v2";
|
|
||||||
|
|
||||||
export const ViewAndCopySharedSecret = ({
|
|
||||||
inModal,
|
|
||||||
newSharedSecret,
|
|
||||||
isUrlCopied,
|
|
||||||
copyUrlToClipboard
|
|
||||||
}: {
|
|
||||||
inModal: boolean;
|
|
||||||
newSharedSecret: string;
|
|
||||||
isUrlCopied: boolean;
|
|
||||||
copyUrlToClipboard: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={`flex w-full justify-center px-6 ${!inModal ? "mx-auto max-w-2xl" : ""}`}>
|
|
||||||
<div className={`${!inModal ? "border border-mineshaft-600 bg-mineshaft-800 rounded-md p-4" : ""}`}>
|
|
||||||
<div className="my-2 flex items-center justify-end rounded-md border border-mineshaft-500 bg-mineshaft-700 p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{newSharedSecret}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyUrlToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Click to Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,67 +1,16 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
import { ShareSecretForm } from "./components";
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { usePopUp, useTimedReset } from "@app/hooks";
|
|
||||||
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
|
||||||
|
|
||||||
import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
|
||||||
import { SecretTable } from "./components";
|
|
||||||
|
|
||||||
// note: isNewSession: controls if the user is sharing a new secret or viewing a shared secret
|
|
||||||
|
|
||||||
export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { id, key: urlEncodedPublicKey } = router.query;
|
|
||||||
const [hashedHex, key] = urlEncodedPublicKey
|
|
||||||
? urlEncodedPublicKey.toString().split("-")
|
|
||||||
: ["", ""];
|
|
||||||
|
|
||||||
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
|
||||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
|
||||||
id as string,
|
|
||||||
hashedHex as string
|
|
||||||
);
|
|
||||||
const accessType = data?.accessType;
|
|
||||||
const orgName = data?.orgName;
|
|
||||||
|
|
||||||
const decryptedSecret = useMemo(() => {
|
|
||||||
if (data && data.encryptedValue && publicKey) {
|
|
||||||
const res = decryptSymmetric({
|
|
||||||
ciphertext: data.encryptedValue,
|
|
||||||
iv: data.iv,
|
|
||||||
tag: data.tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}, [data, publicKey]);
|
|
||||||
|
|
||||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
|
||||||
initialState: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const copyUrlToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(decryptedSecret);
|
|
||||||
setIsUrlCopied(true);
|
|
||||||
};
|
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
|
|
||||||
|
|
||||||
|
export const ShareSecretPublicPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col overflow-y-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||||
<Head>
|
<div />
|
||||||
<title>Infisical | Secret Sharing</title>
|
<div className="mx-auto w-full max-w-xl px-4">
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<div className="mb-8 text-center">
|
||||||
</Head>
|
|
||||||
<div className="flex w-full flex-grow items-center justify-center dark:[color-scheme:dark]">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="mb-4 flex justify-center pt-8">
|
<div className="mb-4 flex justify-center pt-8">
|
||||||
<Link href="https://infisical.com">
|
<Link href="https://infisical.com">
|
||||||
<Image
|
<Image
|
||||||
@@ -73,108 +22,68 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-center">
|
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
|
||||||
<h1
|
Share a secret
|
||||||
className={`${
|
</h1>
|
||||||
id ? "mb-4 max-w-sm" : "mt-4 mb-6 max-w-md"
|
<p className="text-md">
|
||||||
} bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-3xl font-medium text-transparent`}
|
Powered by{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
>
|
>
|
||||||
{id
|
Infisical →
|
||||||
? "Someone shared a secret via Infisical with you"
|
</a>
|
||||||
: "Share a secret via Infisical"}
|
</p>
|
||||||
</h1>
|
</div>
|
||||||
</div>
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
<div className="m-auto mt-4 flex w-full max-w-2xl justify-center px-6">
|
<ShareSecretForm isPublic />
|
||||||
{id && (
|
</div>
|
||||||
<SecretTable
|
<div className="m-auto my-8 flex w-full">
|
||||||
isLoading={isLoading}
|
<div className="w-full border-t border-mineshaft-600" />
|
||||||
decryptedSecret={decryptedSecret}
|
</div>
|
||||||
isUrlCopied={isUrlCopied}
|
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||||
accessType={accessType}
|
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||||
orgName={orgName}
|
Open source{" "}
|
||||||
/>
|
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||||
)}
|
secret management
|
||||||
</div>
|
</span>{" "}
|
||||||
{isNewSession && (
|
for developers
|
||||||
<div className="px-0 sm:px-6">
|
</p>
|
||||||
<AddShareSecretModal
|
<div className="flex items-center">
|
||||||
popUp={popUp}
|
<p className="md:text-md text-md mr-4">
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
<a
|
||||||
inModal={false}
|
href="https://github.com/infisical/infisical"
|
||||||
isPublic
|
target="_blank"
|
||||||
/>
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
)}
|
|
||||||
{!isNewSession && (
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center px-6 pt-4">
|
|
||||||
<a
|
|
||||||
href="https://share.infisical.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {}}
|
|
||||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
|
||||||
>
|
>
|
||||||
Share your own Secret
|
Infisical
|
||||||
</Button>
|
</a>{" "}
|
||||||
</a>
|
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||||
</div>
|
and certificates across your team and infrastructure.
|
||||||
)}
|
|
||||||
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-4 sm:my-8">
|
|
||||||
<div className="w-full border-t border-mineshaft-600" />
|
|
||||||
</div>
|
|
||||||
<div className="m-auto flex max-w-2xl flex-col items-center justify-center px-4 sm:px-6">
|
|
||||||
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
|
||||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
|
||||||
Open source{" "}
|
|
||||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
|
||||||
secret management
|
|
||||||
</span>{" "}
|
|
||||||
for developers
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-x-4 sm:flex-row">
|
<div className="cursor-pointer">
|
||||||
<p className="md:text-md text-md">
|
|
||||||
<a
|
|
||||||
href="https://github.com/infisical/infisical"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
|
||||||
>
|
|
||||||
Infisical
|
|
||||||
</a>{" "}
|
|
||||||
is the all-in-one secret management platform to securely manage secrets, configs,
|
|
||||||
and certificates across your team and infrastructure.
|
|
||||||
</p>
|
|
||||||
<Link href="https://infisical.com">
|
<Link href="https://infisical.com">
|
||||||
<span className="mt-4 h-min w-[17.5rem] cursor-pointer rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||||
Try Infisical <FontAwesomeIcon icon={faArrowRight} className="pl-1" />
|
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||||
</span>
|
<FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddShareSecretModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
isPublic
|
|
||||||
inModal
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto flex w-full items-center justify-center bg-mineshaft-600 p-2">
|
|
||||||
|
<div className="w-full bg-mineshaft-600 p-2">
|
||||||
<p className="text-center text-sm text-mineshaft-300">
|
<p className="text-center text-sm text-mineshaft-300">
|
||||||
© 2024{" "}
|
Made with ❤️ by{" "}
|
||||||
<a className="text-primary" href="https://infisical.com">
|
<a className="text-primary" href="https://infisical.com">
|
||||||
Infisical
|
Infisical
|
||||||
</a>
|
</a>
|
||||||
. All rights reserved.
|
|
||||||
<br />
|
<br />
|
||||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { faArrowRight, faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Button, EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isLoading: boolean;
|
|
||||||
decryptedSecret: string;
|
|
||||||
isUrlCopied: boolean;
|
|
||||||
copyUrlToClipboard: () => void;
|
|
||||||
accessType?: SecretSharingAccessType;
|
|
||||||
orgName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceContentWithDot = (str: string) => {
|
|
||||||
let finalStr = "";
|
|
||||||
for (let i = 0; i < str.length; i += 1) {
|
|
||||||
const char = str.at(i);
|
|
||||||
finalStr += char === "\n" ? "\n" : "*";
|
|
||||||
}
|
|
||||||
return finalStr;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SecretTable = ({
|
|
||||||
isLoading,
|
|
||||||
decryptedSecret,
|
|
||||||
isUrlCopied,
|
|
||||||
copyUrlToClipboard,
|
|
||||||
accessType,
|
|
||||||
orgName
|
|
||||||
}: Props) => {
|
|
||||||
const [isVisible, setIsVisible] = useToggle(false);
|
|
||||||
const title = orgName
|
|
||||||
? (<p>Someone from <strong>{orgName}</strong> organization has shared a secret with you</p>)
|
|
||||||
: (<p>You need to be logged in to view this secret</p>);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
|
||||||
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
|
||||||
{!isLoading && !decryptedSecret && accessType !== SecretSharingAccessType.Organization && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
|
||||||
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
{!isLoading && !decryptedSecret && accessType === SecretSharingAccessType.Organization && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-4000">
|
|
||||||
<EmptyState title={title} icon={faKey}>
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center pt-6">
|
|
||||||
<a
|
|
||||||
href="/login"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {}}
|
|
||||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="ml-2" />}
|
|
||||||
>
|
|
||||||
Login into <strong>{orgName}</strong> to view this secret
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</EmptyState>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
{!isLoading && decryptedSecret && (
|
|
||||||
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
|
|
||||||
<div
|
|
||||||
className={`thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll ${
|
|
||||||
isVisible ? "break-words" : "break-all"
|
|
||||||
} pr-4 dark:[color-scheme:dark]`}
|
|
||||||
>
|
|
||||||
<div className="align-center flex w-full min-w-full whitespace-pre-line">
|
|
||||||
{isVisible ? decryptedSecret : replaceContentWithDot(decryptedSecret)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-1 right-0 mx-1 flex max-h-8 sm:top-2 sm:right-5">
|
|
||||||
<IconButton
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="primary"
|
|
||||||
ariaLabel="copy to clipboard"
|
|
||||||
onClick={copyUrlToClipboard}
|
|
||||||
className="mr-1 flex max-h-8 items-center rounded"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="primary"
|
|
||||||
ariaLabel="toggle visibility"
|
|
||||||
onClick={() => setIsVisible.toggle()}
|
|
||||||
className="flex max-h-8 items-center rounded"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
|
||||||
|
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
secret: z.string(),
|
||||||
|
expiresIn: z.string(),
|
||||||
|
viewLimit: z.string(),
|
||||||
|
accessType: z.nativeEnum(SecretSharingAccessType).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isPublic: boolean; // whether or not this is a public (non-authenticated) secret sharing form
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||||
|
const [secretLink, setSecretLink] = useState("");
|
||||||
|
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||||
|
initialState: "Copy to clipboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||||
|
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||||
|
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
secret: value || ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, secret, expiresIn, viewLimit, accessType }: FormData) => {
|
||||||
|
try {
|
||||||
|
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: secret,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await createSharedSecret.mutateAsync({
|
||||||
|
name,
|
||||||
|
encryptedValue: ciphertext,
|
||||||
|
hashedHex,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
||||||
|
accessType
|
||||||
|
});
|
||||||
|
|
||||||
|
setSecretLink(
|
||||||
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||||
|
hashedHex
|
||||||
|
)}-${encodeURIComponent(key)}`
|
||||||
|
);
|
||||||
|
reset();
|
||||||
|
|
||||||
|
setCopyTextSecret("secret");
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created a shared secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create a shared secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSecretLink = Boolean(secretLink);
|
||||||
|
|
||||||
|
return !hasSecretLink ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{!isPublic && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Name (Optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="API Key" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secret"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Your Secret"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-2"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
placeholder="Enter sensitive data to share via an encrypted link..."
|
||||||
|
{...field}
|
||||||
|
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 py-1.5 px-2 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
|
||||||
|
disabled={value !== undefined}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue="3600000"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expiresInOptions.map(({ label, value: expiresInValue }) => (
|
||||||
|
<SelectItem value={String(expiresInValue || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="viewLimit"
|
||||||
|
defaultValue="-1"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
|
||||||
|
<SelectItem value={String(viewLimitValue || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!isPublic && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessType"
|
||||||
|
defaultValue={SecretSharingAccessType.Organization}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="General Access" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
|
||||||
|
<SelectItem value={SecretSharingAccessType.Organization}>
|
||||||
|
People within your organization
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create secret link
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{secretLink}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(secretLink);
|
||||||
|
setCopyTextSecret("Copied");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSecretLink("")}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||||
|
>
|
||||||
|
Share another secret
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
export { SecretTable } from "./SecretTable";
|
export { ShareSecretForm } from "./ShareSecretForm";
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
import { SecretContainer, SecretErrorContainer } from "./components";
|
||||||
|
|
||||||
|
export const ViewSecretPublicPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id, key: urlEncodedPublicKey } = router.query;
|
||||||
|
|
||||||
|
const [hashedHex, key] = urlEncodedPublicKey
|
||||||
|
? urlEncodedPublicKey.toString().split("-")
|
||||||
|
: ["", ""];
|
||||||
|
|
||||||
|
const { data: secret, error } = useGetActiveSharedSecretById({
|
||||||
|
sharedSecretId: id as string,
|
||||||
|
hashedHex
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||||
|
<div />
|
||||||
|
<div className="mx-auto w-full max-w-xl px-4 ">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<div className="mb-4 flex justify-center pt-8">
|
||||||
|
<Link href="https://infisical.com">
|
||||||
|
<Image
|
||||||
|
src="/images/gradientLogo.svg"
|
||||||
|
height={90}
|
||||||
|
width={120}
|
||||||
|
alt="Infisical logo"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
|
||||||
|
View shared secret
|
||||||
|
</h1>
|
||||||
|
<p className="text-md">
|
||||||
|
Powered by{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
Infisical →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{secret && key && <SecretContainer secret={secret} secretKey={key} />}
|
||||||
|
{error && <SecretErrorContainer />}
|
||||||
|
<div className="m-auto my-8 flex w-full">
|
||||||
|
<div className="w-full border-t border-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
|
||||||
|
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||||
|
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||||
|
Open source{" "}
|
||||||
|
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||||
|
secret management
|
||||||
|
</span>{" "}
|
||||||
|
for developers
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<p className="md:text-md text-md mr-4">
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
Infisical
|
||||||
|
</a>{" "}
|
||||||
|
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||||
|
and certificates across your team and infrastructure.
|
||||||
|
</p>
|
||||||
|
<div className="cursor-pointer">
|
||||||
|
<Link href="https://infisical.com">
|
||||||
|
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||||
|
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-mineshaft-600 p-2">
|
||||||
|
<p className="text-center text-sm text-mineshaft-300">
|
||||||
|
© 2024{" "}
|
||||||
|
<a className="text-primary" href="https://infisical.com">
|
||||||
|
Infisical
|
||||||
|
</a>
|
||||||
|
. All rights reserved.
|
||||||
|
<br />
|
||||||
|
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
faArrowRight,
|
||||||
|
faCheck,
|
||||||
|
faCopy,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { Button, IconButton } from "@app/components/v2";
|
||||||
|
import { useTimedReset, useToggle } from "@app/hooks";
|
||||||
|
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: TViewSharedSecretResponse;
|
||||||
|
secretKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||||
|
const [isVisible, setIsVisible] = useToggle(false);
|
||||||
|
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||||
|
initialState: "Copy to clipboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecret = useMemo(() => {
|
||||||
|
if (secret && secret.encryptedValue && key) {
|
||||||
|
const res = decryptSymmetric({
|
||||||
|
ciphertext: secret.encryptedValue,
|
||||||
|
iv: secret.iv,
|
||||||
|
tag: secret.tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [secret, key]);
|
||||||
|
|
||||||
|
const hiddenSecret = decryptedSecret ? "*".repeat(decryptedSecret.length) : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex items-center justify-between rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
||||||
|
<p className="whitespace-pre-wrap break-all">
|
||||||
|
{isVisible ? decryptedSecret : hiddenSecret}
|
||||||
|
</p>
|
||||||
|
<div className="flex">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(decryptedSecret);
|
||||||
|
setCopyTextSecret("Copied");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative ml-2"
|
||||||
|
onClick={() => setIsVisible.toggle()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||||
|
>
|
||||||
|
Share your own secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export const SecretErrorContainer = () => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<FontAwesomeIcon icon={faKey} size="2x" />
|
||||||
|
<p className="mt-4">The secret you are looking is missing or has expired</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { SecretContainer } from "./SecretContainer";
|
||||||
|
export { SecretErrorContainer } from "./SecretErrorContainer";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ViewSecretPublicPage } from "./ViewSecretPublicPage";
|
||||||
Reference in New Issue
Block a user