feat(secret-sharing): secret requests

This commit is contained in:
Daniel Hougaard
2025-02-26 23:29:19 +09:00
parent 2aa6fdf983
commit 7aaf0f4ed3
40 changed files with 1915 additions and 49 deletions

View File

@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (!hasSharingTypeColumn) {
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
if (hasSharingTypeColumn) {
table.dropColumn("type");
}
});
}

View File

@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),

View File

@@ -250,6 +250,7 @@ export enum EventType {
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
CREATE_SHARED_SECRET = "create-shared-secret",
CREATE_SECRET_REQUEST = "create-secret-request",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret",
GET_SECRET_SYNCS = "get-secret-syncs",
@@ -2020,6 +2021,15 @@ interface CreateSharedSecretEvent {
};
}
interface CreateSecretRequestEvent {
type: EventType.CREATE_SECRET_REQUEST;
metadata: {
id: string;
accessType: string;
name?: string;
};
}
interface DeleteSharedSecretEvent {
type: EventType.DELETE_SHARED_SECRET;
metadata: {
@@ -2470,4 +2480,5 @@ export type Event =
| KmipOperationActivateEvent
| KmipOperationRevokeEvent
| KmipOperationLocateEvent
| KmipOperationRegisterEvent;
| KmipOperationRegisterEvent
| CreateSecretRequestEvent;

View File

@@ -1096,7 +1096,9 @@ export const registerRoutes = async (
permissionService,
secretSharingDAL,
orgDAL,
kmsService
kmsService,
smtpService,
userDAL
});
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({

View File

@@ -37,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerSecretFolderRouter } from "./secret-folder-router";
import { registerSecretImportRouter } from "./secret-import-router";
import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router";
@@ -110,7 +111,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
await server.register(registerIdentityRouter, { prefix: "/identities" });
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
await server.register(
async (secretSharingRouter) => {
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
},
{ prefix: "/secret-sharing" }
);
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" });

View File

@@ -0,0 +1,272 @@
import { z } from "zod";
import { SecretSharingSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretSharingAccessType } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
isSecretValueSet: z.boolean(),
requester: z.object({
organizationName: z.string(),
firstName: z.string().nullish(),
lastName: z.string().nullish(),
username: z.string()
})
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
id: req.params.id,
actorOrgId: req.permission?.orgId,
orgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/set-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
secretValue: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission?.orgId,
orgId: req.permission?.orgId,
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod,
secretValue: req.body.secretValue
});
return { secretRequest };
}
});
server.route({
method: "POST",
url: "/:id/reveal-value",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
}).extend({
secretValue: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
id: req.params.id,
actorOrgId: req.permission.orgId,
orgId: req.permission.orgId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod
});
return { secretRequest };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
secretRequest: SecretSharingSchema.omit({
encryptedSecret: true,
tag: true,
iv: true,
encryptedValue: true
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
sharedSecretId: req.params.id,
orgId: req.permission.orgId,
actor: req.permission.type,
type: SecretSharingType.Request
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestDeleted,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: req.params.id,
organizationId: req.permission.orgId,
...req.auditLogInfo
}
});
return { secretRequest };
}
});
server.route({
method: "GET",
url: "/",
config: {
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.object({
secrets: z.array(SecretSharingSchema),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
type: SecretSharingType.Request,
...req.query
});
return {
secrets,
totalCount
};
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
name: z.string().max(50).optional(),
expiresAt: z.string(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRET_REQUEST,
metadata: {
accessType: req.body.accessType,
name: req.body.name,
id: shareRequest.id
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretRequestCreated,
distinctId: getTelemetryDistinctId(req),
properties: {
secretRequestId: shareRequest.id,
organizationId: req.permission.orgId,
secretRequestName: req.body.name,
...req.auditLogInfo
}
});
return { id: shareRequest.id };
}
});
};

View File

@@ -11,6 +11,7 @@ import {
} from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
type: SecretSharingType.Share,
...req.query
});
@@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
sharedSecretId
sharedSecretId,
type: SecretSharingType.Share
});
await server.services.auditLog.createAuditLog({

View File

@@ -20,7 +20,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
queueService: TQueueServiceFactory;
};
@@ -45,6 +45,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await secretSharingDAL.pruneExpiredSecretRequests();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();

View File

@@ -7,12 +7,58 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
import { SecretSharingType } from "./secret-sharing-types";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
export const secretSharingDALFactory = (db: TDbClient) => {
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
const getSecretRequestById = async (id: string) => {
const repDb = db.replicaNode();
const secretRequest = await repDb(TableName.SecretSharing)
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`)
.where(`${TableName.SecretSharing}.id`, id)
.where(`${TableName.SecretSharing}.type`, SecretSharingType.Request)
.select(
repDb.ref("name").withSchema(TableName.Organization).as("orgName"),
repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"),
repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"),
repDb.ref("username").withSchema(TableName.Users).as("requesterUsername")
)
.select(selectAllTableCols(TableName.SecretSharing))
.first();
if (!secretRequest) {
throw new DatabaseError({
error: new Error("Get Secret Request By Id, Not found"),
message: "Get Secret Request By Id, Not found",
name: "GetSecretRequestById"
});
}
return {
...secretRequest,
requester: {
organizationName: secretRequest.orgName,
firstName: secretRequest.requesterFirstName,
lastName: secretRequest.requesterLastName,
username: secretRequest.requesterUsername
}
};
};
const countAllUserOrgSharedSecrets = async ({
orgId,
userId,
type
}: {
orgId: string;
userId: string;
type: SecretSharingType;
}) => {
try {
interface CountResult {
count: string;
@@ -22,6 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.SecretSharing)
.where(`${TableName.SecretSharing}.orgId`, orgId)
.where(`${TableName.SecretSharing}.userId`, userId)
.where(`${TableName.SecretSharing}.type`, type)
.count("*")
.first();
@@ -38,6 +85,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
const docs = await (tx || db)(TableName.SecretSharing)
.where("expiresAt", "<", today)
.andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.update({
encryptedValue: "",
tag: "",
@@ -50,6 +98,26 @@ export const secretSharingDALFactory = (db: TDbClient) => {
}
};
const pruneExpiredSecretRequests = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
.whereNotNull("expiresAt")
.andWhere("expiresAt", "<", today)
.andWhere("encryptedSecret", null)
.andWhere("type", SecretSharingType.Request)
.delete();
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" });
}
};
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
try {
const now = new Date();
@@ -57,6 +125,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
.where(filters)
.andWhere("expiresAt", ">", now)
.andWhere("encryptedValue", "<>", "")
.andWhere("type", SecretSharingType.Share)
.select(selectAllTableCols(TableName.SecretSharing))
.orderBy("expiresAt", "asc");
} catch (error) {
@@ -86,7 +155,9 @@ export const secretSharingDALFactory = (db: TDbClient) => {
...sharedSecretOrm,
countAllUserOrgSharedSecrets,
pruneExpiredSharedSecrets,
pruneExpiredSecretRequests,
softDeleteById,
findActiveSharedSecrets
findActiveSharedSecrets,
getSecretRequestById
};
};

View File

@@ -4,26 +4,36 @@ import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import {
SecretSharingType,
TCreatePublicSharedSecretDTO,
TCreateSecretRequestDTO,
TCreateSharedSecretDTO,
TDeleteSharedSecretDTO,
TGetActiveSharedSecretByIdDTO,
TGetSharedSecretsDTO
TGetSecretRequestByIdDTO,
TGetSharedSecretsDTO,
TRevealSecretRequestValueDTO,
TSetSecretRequestValueDTO
} from "./secret-sharing-types";
type TSecretSharingServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
secretSharingDAL: TSecretSharingDALFactory;
orgDAL: TOrgDALFactory;
userDAL: TUserDALFactory;
kmsService: TKmsServiceFactory;
smtpService: TSmtpService;
};
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
@@ -32,7 +42,9 @@ export const secretSharingServiceFactory = ({
permissionService,
secretSharingDAL,
orgDAL,
kmsService
kmsService,
smtpService,
userDAL
}: TSecretSharingServiceFactoryDep) => {
const $validateSharedSecretExpiry = (expiresAt: string) => {
if (new Date(expiresAt) < new Date()) {
@@ -75,7 +87,6 @@ export const secretSharingServiceFactory = ({
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const id = crypto.randomBytes(32).toString("hex");
@@ -88,6 +99,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null,
encryptedSecret,
name,
type: SecretSharingType.Share,
password: hashedPassword,
expiresAt: new Date(expiresAt),
expiresAfterViews,
@@ -101,6 +113,193 @@ export const secretSharingServiceFactory = ({
return { id: idToReturn };
};
const createSecretRequest = async ({
actor,
accessType,
expiresAt,
name,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TCreateSecretRequestDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
$validateSharedSecretExpiry(expiresAt);
const newSecretRequest = await secretSharingDAL.create({
type: SecretSharingType.Request,
userId: actorId,
orgId,
name,
encryptedSecret: null,
accessType,
expiresAt: new Date(expiresAt)
});
return { id: newSecretRequest.id };
};
const revealSecretRequestValue = async ({
id,
actor,
actorId,
actorOrgId,
orgId,
actorAuthMethod
}: TRevealSecretRequestValueDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
if (!secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request has no value set" });
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret);
return { ...secretRequest, secretValue: decryptedSecret.toString() };
};
const getSecretRequestById = async ({
id,
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TGetSecretRequestByIdDTO) => {
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
if (orgId === undefined) {
throw new UnauthorizedError();
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
return {
...secretRequest,
isSecretValueSet: Boolean(secretRequest.encryptedSecret)
};
};
const setSecretRequestValue = async ({
id,
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId,
secretValue
}: TSetSecretRequestValueDTO) => {
const appCfg = getConfig();
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
if (!secretRequest) {
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
}
let respondentUsername: string | undefined;
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
);
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
if (!orgId) {
throw new UnauthorizedError();
}
if (secretRequest.orgId !== orgId) {
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
}
const user = await userDAL.findById(actorId);
if (!user) {
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
}
respondentUsername = user.username;
}
if (secretRequest.encryptedSecret) {
throw new BadRequestError({ message: "Secret request already has a value set" });
}
if (secretValue.length > 10_000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
throw new ForbiddenRequestError({
message: "Access denied: Secret request has expired"
});
}
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
const request = await secretSharingDAL.transaction(async (tx) => {
const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx);
await smtpService.sendMail({
recipients: [secretRequest.requesterUsername],
subjectLine: "Secret Request Completed",
substitutions: {
name: secretRequest.name,
respondentUsername,
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
},
template: SmtpTemplates.SecretRequestCompleted
});
return updatedRequest;
});
return request;
};
const createPublicSharedSecret = async ({
password,
secretValue,
@@ -121,6 +320,7 @@ export const secretSharingServiceFactory = ({
encryptedValue: null,
iv: null,
tag: null,
type: SecretSharingType.Share,
encryptedSecret,
password: hashedPassword,
expiresAt: new Date(expiresAt),
@@ -137,7 +337,8 @@ export const secretSharingServiceFactory = ({
actorAuthMethod,
actorOrgId,
offset,
limit
limit,
type
}: TGetSharedSecretsDTO) => {
if (!actorOrgId) throw new ForbiddenRequestError();
@@ -153,14 +354,16 @@ export const secretSharingServiceFactory = ({
const secrets = await secretSharingDAL.find(
{
userId: actorId,
orgId: actorOrgId
orgId: actorOrgId,
type
},
{ offset, limit, sort: [["createdAt", "desc"]] }
);
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
orgId: actorOrgId,
userId: actorId
userId: actorId,
type
});
return {
@@ -187,9 +390,11 @@ export const secretSharingServiceFactory = ({
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findOne({
id: sharedSecretId,
type: SecretSharingType.Share,
hashedHex
})
: await secretSharingDAL.findOne({
type: SecretSharingType.Share,
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
});
@@ -254,7 +459,7 @@ export const secretSharingServiceFactory = ({
secret: {
...sharedSecret,
...(decryptedSecretValue && {
secretValue: Buffer.from(decryptedSecretValue).toString()
secretValue: decryptedSecretValue.toString()
}),
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
@@ -270,8 +475,8 @@ export const secretSharingServiceFactory = ({
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findById(sharedSecretId)
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
: await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
@@ -286,6 +491,11 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getSharedSecretById
getSharedSecretById,
createSecretRequest,
getSecretRequestById,
setSecretRequestValue,
revealSecretRequestValue
};
};

View File

@@ -1,8 +1,14 @@
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export enum SecretSharingType {
Share = "share",
Request = "request"
}
export type TGetSharedSecretsDTO = {
type: SecretSharingType;
offset: number;
limit: number;
} & TGenericPermission;
@@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
export type TCreateSecretRequestDTO = {
name?: string;
accessType: SecretSharingAccessType;
expiresAt: string;
} & TOrgPermission;
export type TRevealSecretRequestValueDTO = {
id: string;
} & TOrgPermission;
export type TGetSecretRequestByIdDTO = {
id: string;
} & TOrgPermission;
export type TSetSecretRequestValueDTO = {
id: string;
secretValue: string;
} & TOrgPermission;
export type TDeleteSharedSecretDTO = {
sharedSecretId: string;
type: SecretSharingType;
} & TSharedSecretPermission;

View File

@@ -39,7 +39,8 @@ export enum SmtpTemplates {
SecretSyncFailed = "secretSyncFailed.handlebars",
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
ExternalImportFailed = "externalImportFailed.handlebars",
ExternalImportStarted = "externalImportStarted.handlebars"
ExternalImportStarted = "externalImportStarted.handlebars",
SecretRequestCompleted = "secretRequestCompleted.handlebars"
}
export enum SmtpHost {

View File

@@ -0,0 +1,33 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Request Completed</title>
</head>
<body>
<h2>Infisical</h2>
<h2>A secret has been shared with you</h2>
{{#if name}}
<p>Secret request name: {{name}}</p>
{{/if}}
{{#if respondentUsername}}
<p>Shared by: {{respondentUsername}}</p>
{{/if}}
<br />
<br/>
<p>
You can access the secret by clicking the link below.
</p>
<p>
<a href="{{secretRequestUrl}}">Access Secret</a>
</p>
{{emailFooter}}
</body>
</html>

View File

@@ -13,7 +13,9 @@ export enum PostHogEventTypes {
IntegrationCreated = "Integration Created",
MachineIdentityCreated = "Machine Identity Created",
UserOrgInvitation = "User Org Invitation",
TelemetryInstanceStats = "Self Hosted Instance Stats"
TelemetryInstanceStats = "Self Hosted Instance Stats",
SecretRequestCreated = "Secret Request Created",
SecretRequestDeleted = "Secret Request Deleted"
}
export type TSecretModifiedEvent = {
@@ -120,6 +122,23 @@ export type TTelemetryInstanceStatsEvent = {
};
};
export type TSecretRequestCreatedEvent = {
event: PostHogEventTypes.SecretRequestCreated;
properties: {
secretRequestId: string;
organizationId: string;
secretRequestName?: string;
};
};
export type TSecretRequestDeletedEvent = {
event: PostHogEventTypes.SecretRequestDeleted;
properties: {
secretRequestId: string;
organizationId: string;
};
};
export type TPostHogEvent = { distinctId: string } & (
| TSecretModifiedEvent
| TAdminInitEvent
@@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & (
| TIntegrationCreatedEvent
| TProjectCreateEvent
| TTelemetryInstanceStatsEvent
| TSecretRequestCreatedEvent
| TSecretRequestDeletedEvent
);

View File

@@ -25,6 +25,7 @@ export const publicPaths = [
"/login/sso",
"/admin/signup",
"/shared/secret/[id]",
"/secret-request/secret/[id]",
"/share-secret"
];

View File

@@ -21,6 +21,10 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
),
SecretSharing: setRoute(
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
),
SettingsPage: setRoute(
"/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
@@ -285,6 +289,10 @@ export const ROUTE_PATHS = Object.freeze({
)
},
Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId")
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
ViewSecretRequestByIDPage: setRoute(
"/secret-request/secret/$secretRequestId",
"/secret-request/secret/$secretRequestId"
)
}
});

View File

@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
import { secretSharingKeys } from "./queries";
import {
TCreatedSharedSecret,
TCreateSecretRequestRequest,
TCreateSharedSecretRequest,
TDeleteSharedSecretRequest,
TDeleteSecretRequestDTO,
TDeleteSharedSecretRequestDTO,
TRevealedSecretRequest,
TRevealSecretRequestValueRequest,
TSetSecretRequestValueRequest,
TSharedSecret
} from "./types";
@@ -15,7 +20,7 @@ export const useCreateSharedSecret = () => {
return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing",
"/api/v1/secret-sharing/shared",
inputData
);
return data;
@@ -30,7 +35,7 @@ export const useCreatePublicSharedSecret = () => {
return useMutation({
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/public",
"/api/v1/secret-sharing/shared/public",
inputData
);
return data;
@@ -40,12 +45,50 @@ export const useCreatePublicSharedSecret = () => {
});
};
export const useCreateSecretRequest = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (inputData: TCreateSecretRequestRequest) => {
const { data } = await apiRequest.post<TCreatedSharedSecret>(
"/api/v1/secret-sharing/requests",
inputData
);
return data;
},
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() })
});
};
export const useSetSecretRequestValue = () => {
return useMutation({
mutationFn: async (inputData: TSetSecretRequestValueRequest) => {
const { data } = await apiRequest.post<TSharedSecret>(
`/api/v1/secret-sharing/requests/${inputData.id}/set-value`,
inputData
);
return data;
}
});
};
export const useRevealSecretRequestValue = () => {
return useMutation({
mutationFn: async (inputData: TRevealSecretRequestValueRequest) => {
const { data } = await apiRequest.post<TRevealedSecretRequest>(
`/api/v1/secret-sharing/requests/${inputData.id}/reveal-value`,
inputData
);
return data.secretRequest;
}
});
};
export const useDeleteSharedSecret = () => {
const queryClient = useQueryClient();
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequestDTO) => {
const { data } = await apiRequest.delete<TSharedSecret>(
`/api/v1/secret-sharing/${sharedSecretId}`
`/api/v1/secret-sharing/shared/${sharedSecretId}`
);
return data;
},
@@ -53,3 +96,19 @@ export const useDeleteSharedSecret = () => {
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() })
});
};
export const useDeleteSecretRequest = () => {
const queryClient = useQueryClient();
return useMutation<TSharedSecret, unknown, TDeleteSecretRequestDTO>({
mutationFn: async ({ secretRequestId }: TDeleteSecretRequestDTO) => {
const { data } = await apiRequest.delete<TSharedSecret>(
`/api/v1/secret-sharing/requests/${secretRequestId}`
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() });
}
});
};

View File

@@ -2,16 +2,20 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
import { TGetSecretRequestByIdResponse, TSharedSecret, TViewSharedSecretResponse } from "./types";
export const secretSharingKeys = {
allSharedSecrets: () => ["sharedSecrets"] as const,
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
allSecretRequests: () => ["secretRequests"] as const,
specificSecretRequests: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSecretRequests(), { offset, limit }] as const,
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
"shared-secret",
arg
]
],
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
};
export const useGetSharedSecrets = ({
@@ -30,7 +34,7 @@ export const useGetSharedSecrets = ({
});
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
"/api/v1/secret-sharing/",
"/api/v1/secret-sharing/shared",
{
params
}
@@ -40,6 +44,29 @@ export const useGetSharedSecrets = ({
});
};
export const useGetSecretRequests = ({
offset = 0,
limit = 25
}: {
offset: number;
limit: number;
}) => {
return useQuery({
queryKey: secretSharingKeys.specificSecretRequests({ offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
"/api/v1/secret-sharing/requests",
{
params: {
offset: String(offset),
limit: String(limit)
}
}
);
return data;
}
});
};
export const useGetActiveSharedSecretById = ({
sharedSecretId,
hashedHex,
@@ -53,7 +80,7 @@ export const useGetActiveSharedSecretById = ({
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
queryFn: async () => {
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${sharedSecretId}`,
`/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
{
...(hashedHex && { hashedHex }),
password
@@ -65,3 +92,16 @@ export const useGetActiveSharedSecretById = ({
enabled: Boolean(sharedSecretId)
});
};
export const useGetSecretRequestById = ({ secretRequestId }: { secretRequestId: string }) => {
return useQuery({
queryKey: secretSharingKeys.getSecretRequestById({ id: secretRequestId }),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretRequestByIdResponse>(
`/api/v1/secret-sharing/requests/${secretRequestId}`
);
return data.secretRequest;
}
});
};

View File

@@ -9,10 +9,17 @@ export type TSharedSecret = {
expiresAt: Date;
expiresAfterViews: number | null;
encryptedValue: string;
encryptedSecret: string;
iv: string;
tag: string;
};
export type TRevealedSecretRequest = {
secretRequest: {
secretValue: string;
} & TSharedSecret;
};
export type TCreatedSharedSecret = {
id: string;
};
@@ -26,6 +33,21 @@ export type TCreateSharedSecretRequest = {
accessType?: SecretSharingAccessType;
};
export type TCreateSecretRequestRequest = {
name?: string;
accessType?: SecretSharingAccessType;
expiresAt: Date;
};
export type TSetSecretRequestValueRequest = {
secretValue: string;
id: string;
};
export type TRevealSecretRequestValueRequest = {
id: string;
};
export type TViewSharedSecretResponse = {
isPasswordProtected: boolean;
secret: {
@@ -38,10 +60,27 @@ export type TViewSharedSecretResponse = {
};
};
export type TDeleteSharedSecretRequest = {
export type TGetSecretRequestByIdResponse = {
secretRequest: {
isSecretValueSet: boolean;
accessType: SecretSharingAccessType;
requester: {
organizationName: string;
username: string;
firstName?: string;
lastName?: string;
};
};
};
export type TDeleteSharedSecretRequestDTO = {
sharedSecretId: string;
};
export type TDeleteSecretRequestDTO = {
secretRequestId: string;
};
export enum SecretSharingAccessType {
Anyone = "anyone",
Organization = "organization"

View File

@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { ShareSecretSection } from "./components";
import { ShareSecretSection } from "./ShareSecretSection";
export const SecretSharingPage = () => {
const { t } = useTranslation();

View File

@@ -0,0 +1,75 @@
import { Helmet } from "react-helmet";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { usePopUp } from "@app/hooks";
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
type DeleteModalData = { name: string; id: string };
enum SecretSharingPageTabs {
ShareSecret = "share-secret",
RequestSecret = "request-secret"
}
export const ShareSecretSection = () => {
const deleteSharedSecret = useDeleteSharedSecret();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"createSharedSecret",
"deleteSharedSecretConfirmation",
"createSecretRequest",
"deleteSecretRequestConfirmation",
"revealSecretRequestValue"
] as const);
const navigate = useNavigate();
const { selectedTab } = useSearch({
from: ROUTE_PATHS.Organization.SecretSharing.id
});
const updateSelectedTab = (tab: string) => {
navigate({
to: ROUTE_PATHS.Organization.SecretSharing.path,
search: (prev) => ({ ...prev, selectedTab: tab as SecretSharingPageTabs })
});
};
return (
<div>
<Helmet>
<title>Secret Sharing</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab value={SecretSharingPageTabs.ShareSecret}>Share Secrets</Tab>
<Tab value={SecretSharingPageTabs.RequestSecret}>Request Secrets</Tab>
</TabList>
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
<ShareSecretTab
handlePopUpOpen={handlePopUpOpen}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
handlePopUpClose={handlePopUpClose}
/>
</TabPanel>
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
<RequestSecretTab
handlePopUpOpen={handlePopUpOpen}
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
handlePopUpClose={handlePopUpClose}
/>
</TabPanel>
</Tabs>
</div>
);
};

View File

@@ -0,0 +1,30 @@
import { Modal, ModalContent } from "@app/components/v2";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { RequestSecretForm } from "./RequestSecretForm";
type Props = {
popUp: UsePopUpState<["createSecretRequest"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["createSecretRequest"]>,
state?: boolean
) => void;
};
export const AddSecretRequestModal = ({ popUp, handlePopUpToggle }: Props) => {
return (
<Modal
isOpen={popUp?.createSecretRequest?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("createSecretRequest", isOpen);
}}
>
<ModalContent
title="Request a Secret"
subTitle="Securely request one off secrets from your team or people outside your organization."
>
<RequestSecretForm />
</ModalContent>
</Modal>
);
};

View File

@@ -0,0 +1,182 @@
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 { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { SecretSharingAccessType, useCreateSecretRequest } from "@app/hooks/api/secretSharing";
const schema = z.object({
name: z.string().optional(),
accessType: z
.nativeEnum(SecretSharingAccessType)
.default(SecretSharingAccessType.Anyone)
.optional(),
expiresIn: z.string()
});
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 }
];
export type FormData = z.infer<typeof schema>;
export const RequestSecretForm = () => {
const [secretLink, setSecretLink] = useState("");
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const { mutateAsync: createSecretRequest } = useCreateSecretRequest();
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const onFormSubmit = async ({ name, accessType, expiresIn }: FormData) => {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
try {
const { id } = await createSecretRequest({
name,
accessType,
expiresAt
});
const link = `${window.location.origin}/secret-request/secret/${id}`;
setSecretLink(link);
reset();
navigator.clipboard.writeText(link);
setCopyTextSecret("secret");
createNotification({
text: "Shared secret link copied to clipboard.",
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)}>
<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="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="accessType"
defaultValue={SecretSharingAccessType.Organization}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
tooltipText="Select who is able to input the secret"
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 Request 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" />}
>
Request Another Secret
</Button>
</>
);
};

View File

@@ -0,0 +1,97 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2";
import { useDeleteSecretRequest } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { AddSecretRequestModal } from "./AddSecretRequestModal";
import { RequestedSecretsTable } from "./RequestedSecretsTable";
import { RevealSecretValueModal } from "./RevealSecretValueModal";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>,
data?: any
) => void;
popUp: UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
>,
state?: boolean
) => void;
handlePopUpClose: (
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>
) => void;
};
type DeleteModalData = { name: string; id: string };
export const RequestSecretTab = ({
handlePopUpOpen,
popUp,
handlePopUpToggle,
handlePopUpClose
}: Props) => {
const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest();
const onDeleteApproved = async () => {
try {
await deleteSecretRequest({
secretRequestId: popUp.deleteSecretRequestConfirmation.data?.id
});
createNotification({
text: "Successfully deleted secret request",
type: "success"
});
handlePopUpClose("deleteSecretRequestConfirmation");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete shared secret",
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Secret Requests</p>
<Button
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("createSecretRequest");
}}
>
Request Secret
</Button>
</div>
<RequestedSecretsTable handlePopUpOpen={handlePopUpOpen} />
<AddSecretRequestModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<RevealSecretValueModal
isOpen={popUp.revealSecretRequestValue.isOpen}
popUp={popUp}
onOpenChange={(isOpen) => handlePopUpToggle("revealSecretRequestValue", isOpen)}
/>
<DeleteActionModal
isOpen={popUp.deleteSecretRequestConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name || " "
} secret request?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretRequestConfirmation", isOpen)}
deleteKey={(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteSecretRequestConfirmation")}
onDeleteApproved={onDeleteApproved}
/>
</div>
);
};

View File

@@ -0,0 +1,122 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-extra-boolean-cast */
import { faCopy, faEye, faSpinner, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { TSharedSecret, useRevealSecretRequestValue } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp";
export const RequestedSecretsRow = ({
row,
handlePopUpOpen
}: {
row: TSharedSecret;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
data: unknown
) => void;
}) => {
const { mutateAsync: revealSecretValue, isPending } = useRevealSecretRequestValue();
let isExpired = false;
if (row.expiresAt !== null && new Date(row.expiresAt) < new Date()) {
isExpired = true;
}
return (
<Tr key={row.id}>
<Td>{row.name ? `${row.name}` : "-"}</Td>
<Td>
{isExpired && !row.encryptedSecret ? (
<Badge variant="danger">Expired</Badge>
) : (
<Badge variant={row.encryptedSecret ? "success" : "primary"}>
{row.encryptedSecret ? "Secret Provided" : "Pending Secret"}
</Badge>
)}
</Td>
<Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td>
<Td>{row.expiresAt ? format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a") : "-"}</Td>
<Td>
<div className="flex items-center gap-2">
<Tooltip
content={
row.encryptedSecret
? "Reveal shared secret"
: "Secret value must be provided before it can be viewed."
}
>
<IconButton
isDisabled={!row.encryptedSecret}
className={row.encryptedSecret ? "" : "opacity-50"}
onClick={async (e) => {
e.stopPropagation();
const secretRequest = await revealSecretValue({
id: row.id
});
console.log("revealSecretRequestValue", {
secretValue: secretRequest.secretValue,
secretRequestName: secretRequest.name
});
handlePopUpOpen("revealSecretRequestValue", {
secretValue: secretRequest.secretValue,
secretRequestName: secretRequest.name
});
}}
variant="plain"
ariaLabel="reveal"
>
<FontAwesomeIcon
className={isPending ? "animate-spin" : ""}
icon={!isPending ? faEye : faSpinner}
/>
</IconButton>
</Tooltip>
<IconButton
isDisabled={Boolean(row.encryptedSecret) || isExpired}
className={Boolean(row.encryptedSecret) || isExpired ? "opacity-50" : ""}
onClick={async (e) => {
e.stopPropagation();
navigator.clipboard.writeText(
`${window.location.origin}/secret-request/secret/${row.id}`
);
createNotification({
text: "Shared secret link copied to clipboard.",
type: "success"
});
}}
variant="plain"
ariaLabel="copy link"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
<Tooltip content="Delete Secret Request">
<IconButton
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteSecretRequestConfirmation", {
name: "delete",
id: row.id
});
}}
variant="plain"
ariaLabel="delete"
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
</Td>
</Tr>
);
};

View File

@@ -0,0 +1,71 @@
import { useState } from "react";
import { faKey } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { useGetSecretRequests } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { RequestedSecretsRow } from "./RequestedSecretsRow";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
data: unknown
) => void;
};
export const RequestedSecretsTable = ({ handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const { isPending, data } = useGetSecretRequests({
offset: (page - 1) * perPage,
limit: perPage
});
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Status</Th>
<Th>Created At</Th>
<Th>Valid Until</Th>
<Th aria-label="button" className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={7} innerKey="shared-secrets" />}
{!isPending &&
data?.secrets?.map((row) => (
<RequestedSecretsRow key={row.id} row={row} handlePopUpOpen={handlePopUpOpen} />
))}
</TBody>
</Table>
{!isPending &&
data?.secrets &&
data?.totalCount >= perPage &&
data?.totalCount !== undefined && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && !data?.secrets?.length && (
<EmptyState title="No secrets shared yet" icon={faKey} />
)}
</TableContainer>
);
};

View File

@@ -0,0 +1,73 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, IconButton, Modal, ModalClose, ModalContent, Tooltip } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
isOpen: boolean;
onOpenChange?: (isOpen: boolean) => void;
popUp: UsePopUpState<["revealSecretRequestValue"]>;
};
type ContentProps = {
secretValue: string;
secretRequestName?: string;
};
const Content = ({ secretValue, secretRequestName }: ContentProps) => {
const [isSecretValueCopied, setIsSecretValueCopied] = useToggle(false);
return (
<>
{secretRequestName && (
<p className="mb-8 text-sm text-mineshaft-200">
Shared secret value for <strong>{secretRequestName}</strong>
</p>
)}
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{secretValue}</p>
<Tooltip content="Click to copy">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(secretValue);
setIsSecretValueCopied.on();
}}
>
<FontAwesomeIcon icon={isSecretValueCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
<div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild>
<Button colorSchema="primary">Close</Button>
</ModalClose>
</div>
</>
);
};
export const RevealSecretValueModal = ({ isOpen, onOpenChange, popUp }: Props) => {
const data = popUp.revealSecretRequestValue.data as {
secretValue: string;
secretRequestName?: string;
};
const title = data?.secretRequestName
? `Shared secret value for secret request ${data.secretRequestName}`
: "Shared secret value";
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={title}>
<Content secretRequestName={data?.secretRequestName} secretValue={data?.secretValue} />
</ModalContent>
</Modal>
);
};

View File

@@ -1,27 +1,40 @@
import { Helmet } from "react-helmet";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
import { useDeleteSharedSecret } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { AddShareSecretModal } from "./AddShareSecretModal";
import { ShareSecretsTable } from "./ShareSecretsTable";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
data?: any
) => void;
popUp: UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
state?: boolean
) => void;
handlePopUpClose: (popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>) => void;
};
type DeleteModalData = { name: string; id: string };
export const ShareSecretSection = () => {
const deleteSharedSecret = useDeleteSharedSecret();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"createSharedSecret",
"deleteSharedSecretConfirmation"
] as const);
export const ShareSecretTab = ({
handlePopUpOpen,
popUp,
handlePopUpToggle,
handlePopUpClose
}: Props) => {
const deleteSecretShare = useDeleteSharedSecret();
const onDeleteApproved = async () => {
try {
deleteSharedSecret.mutateAsync({
deleteSecretShare.mutateAsync({
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
});
createNotification({
@@ -41,11 +54,6 @@ export const ShareSecretSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<Helmet>
<title>Secret Sharing</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
<Button

View File

@@ -1,13 +1,24 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretSharingPage } from "./SecretSharingPage";
const SecretSharingQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
)({
component: SecretSharingPage,
validateSearch: zodValidator(SecretSharingQueryParams),
search: {
middlewares: [stripSearchParams({ selectedTab: "" })]
},
context: () => ({
breadcrumbs: [
{
@@ -16,7 +27,7 @@ export const Route = createFileRoute(
link: linkOptions({ to: "/" })
},
{
label: "secret sharing"
label: "Secret Sharing"
}
]
})

View File

@@ -0,0 +1,184 @@
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { addSeconds, formatISO } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { SessionStorageKeys } from "@app/const";
import { ROUTE_PATHS } from "@app/const/routes";
import { useGetSecretRequestById } from "@app/hooks/api/secretSharing";
import { SecretRequestErrorContainer } from "./components/SecretErrorContainer";
import { SecretRequestContainer } from "./components/SecretRequestContainer";
import { SecretRequestSuccessContainer } from "./components/SecretRequestSuccessContainer";
import { SecretValueAlreadySharedContainer } from "./components/SecretValueAlreadySharedContainer";
export const ViewSecretRequestByIDPage = () => {
const id = useParams({
from: ROUTE_PATHS.Public.ViewSecretRequestByIDPage.id,
select: (el) => el.secretRequestId
});
const [step, setStep] = useState<"set-value" | "success">("set-value");
const {
data: secretRequest,
error,
isPending
} = useGetSecretRequestById({
secretRequestId: id
});
const navigate = useNavigate();
const statusCode = ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode;
const message = ((error as AxiosError)?.response?.data as { message: string })?.message;
const isUnauthorized = statusCode === 401;
const isForbidden = statusCode === 403;
const isInvalidCredential = message === "Invalid credentials";
useEffect(() => {
if (isUnauthorized && !isInvalidCredential) {
// persist current URL in session storage so that we can come back to this after successful login
sessionStorage.setItem(
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 60)),
data: window.location.href
})
);
createNotification({
type: "info",
text: "Login is required in order to access the shared secret."
});
navigate({
to: "/login"
});
}
if (isForbidden) {
createNotification({
type: "error",
text: "You do not have access to this shared secret."
});
}
}, [error]);
return (
<>
<Helmet>
<title>Securely Share Secrets | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="" />
<meta name="og:description" content="" />
</Helmet>
<div className="flex h-screen flex-col justify-between overflow-auto 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 py-4 md:px-0">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<img
src="/images/gradientLogo.svg"
height={90}
width={120}
alt="Infisical logo"
className="cursor-pointer"
/>
</a>
</div>
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
{step === "set-value" ? "Secret Request" : "Secret request shared"}
</h1>
<p className="text-sm text-mineshaft-300">
Secret requested by {secretRequest?.requester.username} from the{" "}
{secretRequest?.requester.organizationName} organization
</p>
<p className="text-md mt-2">
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 &rarr;
</a>
</p>
</div>
{!isPending && (
<>
{!error &&
secretRequest &&
step === "set-value" &&
!secretRequest.isSecretValueSet && (
<SecretRequestContainer
onSuccess={() => setStep("success")}
secretRequestId={id}
/>
)}
{secretRequest?.isSecretValueSet && <SecretValueAlreadySharedContainer />}
{error && !isInvalidCredential && !isUnauthorized && <SecretRequestErrorContainer />}
{step === "success" && (
<SecretRequestSuccessContainer
requesterUsername={secretRequest!.requester.username}
/>
)}
</>
)}
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex w-full flex-col 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 flex-col items-start sm:flex-row sm: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="mt-4 cursor-pointer sm:mt-0">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 px-3 py-2 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>
</a>
</div>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2">
<p className="text-center text-sm text-mineshaft-300">
Made with ❤️ by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
<br />
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
</p>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,13 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const SecretRequestErrorContainer = () => {
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 request you are looking is missing or has expired.</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,80 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faArrowRight } 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 { Button, FormControl, TextArea } from "@app/components/v2";
import { useSetSecretRequestValue } from "@app/hooks/api";
const formSchema = z.object({
secretValue: z.string().min(1)
});
type FormData = z.infer<typeof formSchema>;
type Props = {
onSuccess: () => void;
secretRequestId: string;
};
export const SecretRequestContainer = ({ onSuccess, secretRequestId }: Props) => {
const form = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const { mutateAsync: setSecretValue, isPending } = useSetSecretRequestValue();
const onSubmit = async (data: FormData) => {
await setSecretValue({
id: secretRequestId,
secretValue: data.secretValue
});
createNotification({
title: "Secret request value shared",
text: "The secret request value has been shared",
type: "success"
});
onSuccess();
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<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">
<div className="w-full">
<Controller
control={form.control}
name="secretValue"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
isRequired
label="Secret Value"
>
<TextArea {...field} rows={10} reSize="none" />
</FormControl>
)}
/>
<Button
isLoading={isPending}
isDisabled={isPending}
colorSchema="secondary"
className="w-full"
type="submit"
>
Share Secret
<FontAwesomeIcon className="ml-2" icon={faArrowRight} />
</Button>
</div>
</div>
</div>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,23 @@
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = {
requesterUsername: string;
};
export const SecretRequestSuccessContainer = ({ requesterUsername }: Props) => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
<FontAwesomeIcon icon={faCheck} size="2x" className="text-primary-500" />
</div>
<p className="text-md mt-2 font-semibold">Secret Shared</p>
<p className="mt-2 text-sm text-mineshaft-300">
<strong>{requesterUsername}</strong> has now been notified of your shared secret, and will
be able to access it shortly.
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,18 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const SecretValueAlreadySharedContainer = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
<FontAwesomeIcon icon={faKey} size="2x" className="text-primary-500" />
</div>
<p className="text-md mt-2 font-semibold">Secret Already Shared</p>
<p className="mt-2 text-sm text-mineshaft-300">
A secret value has already been shared for this secret request.
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { ViewSecretRequestByIDPage } from "./ViewSecretRequestByIDPage";
export const Route = createFileRoute("/secret-request/secret/$secretRequestId")({
component: ViewSecretRequestByIDPage,
beforeLoad: async ({ context }) => {
await context.queryClient
.ensureQueryData({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
})
.catch(() => undefined);
}
});

View File

@@ -28,6 +28,7 @@ import { Route as authPasswordSetupPageRouteImport } from './pages/auth/Password
import { Route as userLayoutImport } from './pages/user/layout'
import { Route as organizationLayoutImport } from './pages/organization/layout'
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
import { Route as publicViewSecretRequestByIDPageRouteImport } from './pages/public/ViewSecretRequestByIDPage/route'
import { Route as authSignUpSsoPageRouteImport } from './pages/auth/SignUpSsoPage/route'
import { Route as authLoginSsoPageRouteImport } from './pages/auth/LoginSsoPage/route'
import { Route as authSelectOrgPageRouteImport } from './pages/auth/SelectOrgPage/route'
@@ -354,6 +355,13 @@ const publicViewSharedSecretByIDPageRouteRoute =
getParentRoute: () => rootRoute,
} as any)
const publicViewSecretRequestByIDPageRouteRoute =
publicViewSecretRequestByIDPageRouteImport.update({
id: '/secret-request/secret/$secretRequestId',
path: '/secret-request/secret/$secretRequestId',
getParentRoute: () => rootRoute,
} as any)
const authSignUpSsoPageRouteRoute = authSignUpSsoPageRouteImport.update({
id: '/sso',
path: '/sso',
@@ -1760,6 +1768,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authSignUpSsoPageRouteImport
parentRoute: typeof RestrictLoginSignupSignupImport
}
'/secret-request/secret/$secretRequestId': {
id: '/secret-request/secret/$secretRequestId'
path: '/secret-request/secret/$secretRequestId'
fullPath: '/secret-request/secret/$secretRequestId'
preLoaderRoute: typeof publicViewSecretRequestByIDPageRouteImport
parentRoute: typeof rootRoute
}
'/shared/secret/$secretId': {
id: '/shared/secret/$secretId'
path: '/shared/secret/$secretId'
@@ -3643,6 +3658,7 @@ export interface FileRoutesByFullPath {
'/login/select-organization': typeof authSelectOrgPageRouteRoute
'/login/sso': typeof authLoginSsoPageRouteRoute
'/signup/sso': typeof authSignUpSsoPageRouteRoute
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
'/admin': typeof adminLayoutRouteWithChildren
'/personal-settings/': typeof userPersonalSettingsPageRouteRoute
@@ -3817,6 +3833,7 @@ export interface FileRoutesByTo {
'/login/select-organization': typeof authSelectOrgPageRouteRoute
'/login/sso': typeof authLoginSsoPageRouteRoute
'/signup/sso': typeof authSignUpSsoPageRouteRoute
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
'/admin': typeof adminOverviewPageRouteRoute
'/login/provider/error': typeof authProviderErrorPageRouteRoute
@@ -3991,6 +4008,7 @@ export interface FileRoutesById {
'/_restrict-login-signup/login/select-organization': typeof authSelectOrgPageRouteRoute
'/_restrict-login-signup/login/sso': typeof authLoginSsoPageRouteRoute
'/_restrict-login-signup/signup/sso': typeof authSignUpSsoPageRouteRoute
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout': typeof organizationLayoutRouteWithChildren
'/_authenticate/_inject-org-details/admin': typeof AuthenticateInjectOrgDetailsAdminRouteWithChildren
@@ -4176,6 +4194,7 @@ export interface FileRouteTypes {
| '/login/select-organization'
| '/login/sso'
| '/signup/sso'
| '/secret-request/secret/$secretRequestId'
| '/shared/secret/$secretId'
| '/admin'
| '/personal-settings/'
@@ -4349,6 +4368,7 @@ export interface FileRouteTypes {
| '/login/select-organization'
| '/login/sso'
| '/signup/sso'
| '/secret-request/secret/$secretRequestId'
| '/shared/secret/$secretId'
| '/admin'
| '/login/provider/error'
@@ -4521,6 +4541,7 @@ export interface FileRouteTypes {
| '/_restrict-login-signup/login/select-organization'
| '/_restrict-login-signup/login/sso'
| '/_restrict-login-signup/signup/sso'
| '/secret-request/secret/$secretRequestId'
| '/shared/secret/$secretId'
| '/_authenticate/_inject-org-details/_org-layout'
| '/_authenticate/_inject-org-details/admin'
@@ -4689,6 +4710,7 @@ export interface RootRouteChildren {
publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute
middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren
middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren
publicViewSecretRequestByIDPageRouteRoute: typeof publicViewSecretRequestByIDPageRouteRoute
publicViewSharedSecretByIDPageRouteRoute: typeof publicViewSharedSecretByIDPageRouteRoute
}
@@ -4699,6 +4721,8 @@ const rootRouteChildren: RootRouteChildren = {
middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren,
middlewaresRestrictLoginSignupRoute:
middlewaresRestrictLoginSignupRouteWithChildren,
publicViewSecretRequestByIDPageRouteRoute:
publicViewSecretRequestByIDPageRouteRoute,
publicViewSharedSecretByIDPageRouteRoute:
publicViewSharedSecretByIDPageRouteRoute,
}
@@ -4718,6 +4742,7 @@ export const routeTree = rootRoute
"/share-secret",
"/_authenticate",
"/_restrict-login-signup",
"/secret-request/secret/$secretRequestId",
"/shared/secret/$secretId"
]
},
@@ -4843,6 +4868,9 @@ export const routeTree = rootRoute
"filePath": "auth/SignUpSsoPage/route.tsx",
"parent": "/_restrict-login-signup/signup"
},
"/secret-request/secret/$secretRequestId": {
"filePath": "public/ViewSecretRequestByIDPage/route.tsx"
},
"/shared/secret/$secretId": {
"filePath": "public/ViewSharedSecretByIDPage/route.tsx"
},

View File

@@ -316,6 +316,7 @@ const sshRoutes = route("/ssh/$projectId", [
export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
route("/secret-request/secret/$secretRequestId", "public/ViewSecretRequestByIDPage/route.tsx"),
route("/share-secret", "public/ShareSecretPage/route.tsx"),
route("/cli-redirect", "auth/CliRedirectPage/route.tsx"),
middleware("restrict-login-signup.tsx", [