mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-21 05:08:50 +00:00
Compare commits
22 Commits
create-pul
...
daniel/go-
Author | SHA1 | Date | |
---|---|---|---|
|
df3c58bc2a | ||
|
db726128f1 | ||
|
ae177343d5 | ||
|
0342ba0890 | ||
|
a332019c25 | ||
|
8039b3f21e | ||
|
c9f7f6481f | ||
|
39df6ce086 | ||
|
de3e23ecfa | ||
|
17a79fb621 | ||
|
abd4b411fa | ||
|
9e4b248794 | ||
|
f6e44463c4 | ||
|
1a6b710138 | ||
|
43a3731b62 | ||
|
24b8b64d3b | ||
|
263d321d75 | ||
|
a6e71c98a6 | ||
|
0e86d5573a | ||
|
6c0ab43c97 | ||
|
d743537284 | ||
|
5df53a25fc |
@@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
if (hasOrgIdColumn) t.uuid("orgId").nullable().alter();
|
||||
if (hasUserIdColumn) t.uuid("userId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId");
|
||||
const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId");
|
||||
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
if (hasOrgIdColumn) t.uuid("orgId").notNullable().alter();
|
||||
if (hasUserIdColumn) t.uuid("userId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
@@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
expiresAfterViews: z.number().nullable().optional()
|
||||
|
@@ -143,7 +143,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query,
|
||||
startDate: req.query.endDate || getLastMidnightDateISO(),
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
actor: req.permission.type
|
||||
});
|
||||
|
@@ -70,8 +70,15 @@ export const creationLimit: RateLimitOptions = {
|
||||
|
||||
// Public endpoints to avoid brute force attacks
|
||||
export const publicEndpointLimit: RateLimitOptions = {
|
||||
// Shared Secrets
|
||||
// Read Shared Secrets
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const publicSecretShareCreationLimit: RateLimitOptions = {
|
||||
// Create Shared Secrets
|
||||
timeWindow: 60 * 1000,
|
||||
max: 5,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingSchema } from "@app/db/schemas";
|
||||
import { publicEndpointLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import {
|
||||
publicEndpointLimit,
|
||||
publicSecretShareCreationLimit,
|
||||
readLimit,
|
||||
writeLimit
|
||||
} from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -72,7 +77,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
url: "/public",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -82,9 +87,42 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z
|
||||
.string()
|
||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews
|
||||
});
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: publicSecretShareCreationLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number()
|
||||
}),
|
||||
response: {
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
||||
import {
|
||||
TCreatePublicSharedSecretDTO,
|
||||
TCreateSharedSecretDTO,
|
||||
TDeleteSharedSecretDTO,
|
||||
TSharedSecretPermission
|
||||
} from "./secret-sharing-types";
|
||||
|
||||
type TSecretSharingServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
@@ -31,6 +36,24 @@ export const secretSharingServiceFactory = ({
|
||||
} = createSharedSecretInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
|
||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||
if (encryptedValue.length > 13000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
encryptedValue,
|
||||
iv,
|
||||
@@ -44,6 +67,36 @@ export const secretSharingServiceFactory = ({
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||
}
|
||||
|
||||
// Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext)
|
||||
if (encryptedValue.length > 13000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@@ -54,6 +107,7 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||
if (!sharedSecret) return;
|
||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||
return;
|
||||
}
|
||||
@@ -77,6 +131,7 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
return {
|
||||
createSharedSecret,
|
||||
createPublicSharedSecret,
|
||||
getSharedSecrets,
|
||||
deleteSharedSecretById,
|
||||
getActiveSharedSecretByIdAndHashedHex
|
||||
|
@@ -8,14 +8,16 @@ export type TSharedSecretPermission = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = {
|
||||
export type TCreatePublicSharedSecretDTO = {
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number;
|
||||
} & TSharedSecretPermission;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||
|
||||
export type TDeleteSharedSecretDTO = {
|
||||
sharedSecretId: string;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 467 KiB After Width: | Height: | Size: 1.2 MiB |
@@ -3,7 +3,48 @@ title: "Ansible"
|
||||
description: "Learn how to use Infisical for secret management in Ansible."
|
||||
---
|
||||
|
||||
The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
|
||||
You can find the Infisical Ansible collection on [Ansible Galaxy](https://galaxy.ansible.com/ui/repo/published/infisical/vault/).
|
||||
|
||||
|
||||
This Ansible Infisical collection includes a variety of Ansible content to help automate the management of Infisical services. This collection is maintained by the Infisical team.
|
||||
|
||||
|
||||
## Ansible version compatibility
|
||||
Tested with the Ansible Core >= 2.12.0 versions, and the current development version of Ansible. Ansible Core versions prior to 2.12.0 have not been tested.
|
||||
|
||||
## Python version compatibility
|
||||
This collection depends on the Infisical SDK for Python.
|
||||
|
||||
Requires Python 3.7 or greater.
|
||||
|
||||
## Installing this collection
|
||||
You can install the Infisical collection with the Ansible Galaxy CLI:
|
||||
|
||||
```bash
|
||||
$ ansible-galaxy collection install infisical.vault
|
||||
```
|
||||
|
||||
The python module dependencies are not installed by ansible-galaxy. They can be manually installed using pip:
|
||||
|
||||
```bash
|
||||
$ pip install infisical-python
|
||||
```
|
||||
|
||||
## Using this collection
|
||||
|
||||
You can either call modules by their Fully Qualified Collection Name (FQCN), such as `infisical.vault.read_secrets`, or you can call modules by their short name if you list the `infisical.vault` collection in the playbook's collections keyword:
|
||||
|
||||
|
||||
```bash
|
||||
---
|
||||
vars:
|
||||
read_all_secrets_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', url='https://spotify.infisical.com') }}"
|
||||
# [{ "key": "HOST", "value": "google.com" }, { "key": "SMTP", "value": "gmail.smtp.edu" }]
|
||||
|
||||
read_secret_by_name_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', secret_name='HOST', url='https://spotify.infisical.com') }}"
|
||||
# [{ "key": "HOST", "value": "google.com" }]
|
||||
```
|
||||
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
|
@@ -25,15 +25,10 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
client, err := infisical.NewInfisicalClient(infisical.Config{
|
||||
client := infisical.NewInfisicalClient(infisical.Config{
|
||||
SiteUrl: "https://app.infisical.com", // Optional, default is https://app.infisical.com
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
|
||||
|
||||
if err != nil {
|
||||
@@ -74,14 +69,9 @@ $ go get github.com/infisical/go-sdk
|
||||
Import the SDK and create a client instance.
|
||||
|
||||
```go
|
||||
client, err := infisical.NewInfisicalClient(infisical.Config{
|
||||
client := infisical.NewInfisicalClient(infisical.Config{
|
||||
SiteUrl: "https://app.infisical.com", // Optional, default is https://api.infisical.com
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
```
|
||||
|
||||
### ClientSettings methods
|
||||
@@ -435,4 +425,146 @@ Delete a secret in Infisical.
|
||||
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Working with folders
|
||||
|
||||
|
||||
### client.Folders().List(options)
|
||||
|
||||
```go
|
||||
folders, err := client.Folders().List(infisical.ListFoldersOptions{
|
||||
ProjectID: "PROJECT_ID",
|
||||
Environment: "dev",
|
||||
Path: "/",
|
||||
})
|
||||
```
|
||||
|
||||
Retrieve all within the Infisical project and environment that client is connected to.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object">
|
||||
<Expandable title="properties">
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where folders should be fetched from.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="ProjectID" type="string">
|
||||
The project ID where the folder lives in.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where folders should be fetched from.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
|
||||
</ParamField>
|
||||
|
||||
### client.Folders().Create(options)
|
||||
|
||||
```go
|
||||
folder, err := client.Folders().Create(infisical.CreateFolderOptions{
|
||||
ProjectID: "PROJECT_ID",
|
||||
Name: "new=folder-name",
|
||||
Environment: "dev",
|
||||
Path: "/",
|
||||
})
|
||||
```
|
||||
|
||||
Create a new folder in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be created.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment where the folder will be created.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path to create the folder in. The root path is `/`.
|
||||
</ParamField>
|
||||
<ParamField query="Name" type="string" optional>
|
||||
The name of the folder to create.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
|
||||
|
||||
### client.Folders().Update(options)
|
||||
|
||||
```go
|
||||
folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
|
||||
ProjectID: "PROJECT_ID",
|
||||
Environment: "dev",
|
||||
Path: "/",
|
||||
FolderID: "FOLDER_ID_TO_UPDATE",
|
||||
NewName: "new-folder-name",
|
||||
})
|
||||
```
|
||||
|
||||
Update an existing folder in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder will be updated.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where the folder lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where the folder should be updated.
|
||||
</ParamField>
|
||||
<ParamField query="FolderID" type="string" required>
|
||||
The ID of the folder to update.
|
||||
</ParamField>
|
||||
<ParamField query="NewName" type="string" required>
|
||||
The new name of the folder.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
### client.Folders().Delete(options)
|
||||
|
||||
```go
|
||||
deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
|
||||
// Either folder ID or folder name is required.
|
||||
FolderName: "name-of-folder-to-delete",
|
||||
FolderID: "folder-id-to-delete",
|
||||
ProjectID: "PROJECT_ID",
|
||||
Environment: "dev",
|
||||
Path: "/",
|
||||
})
|
||||
```
|
||||
|
||||
Delete a folder in Infisical.
|
||||
|
||||
#### Parameters
|
||||
|
||||
<ParamField query="Parameters" type="object" optional>
|
||||
<Expandable title="properties">
|
||||
<ParamField query="FolderName" type="string" optional>
|
||||
The name of the folder to delete. Note that either `FolderName` or `FolderID` is required.
|
||||
</ParamField>
|
||||
<ParamField query="FolderID" type="string" optional>
|
||||
The ID of the folder to delete. Note that either `FolderName` or `FolderID` is required.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="ProjectID" type="string" required>
|
||||
The ID of the project where the folder lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Environment" type="string" required>
|
||||
The slug name (dev, prod, etc) of the environment from where the folder lives in.
|
||||
</ParamField>
|
||||
<ParamField query="Path" type="string" optional>
|
||||
The path from where the folder should be deleted.
|
||||
</ParamField>
|
||||
</Expandable>
|
||||
</ParamField>
|
@@ -24,7 +24,8 @@ export const publicPaths = [
|
||||
"/login/provider/error", // TODO: change
|
||||
"/login/sso",
|
||||
"/admin/signup",
|
||||
"/shared/secret/[id]"
|
||||
"/shared/secret/[id]",
|
||||
"/share-secret"
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
|
@@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePublicSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TSharedSecret,
|
||||
{ message: string },
|
||||
{ sharedSecretId: string }
|
||||
>({
|
||||
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||
|
@@ -17,6 +17,7 @@ export const useGetSharedSecrets = () => {
|
||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||
queryFn: async () => {
|
||||
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
|
||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||
);
|
||||
|
24
frontend/src/pages/share-secret/index.tsx
Normal file
24
frontend/src/pages/share-secret/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||
|
||||
const ShareNewPublicSecretPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<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="" />
|
||||
</Head>
|
||||
<div className="dark h-full">
|
||||
<ShareSecretPublicPage isNewSession />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareNewPublicSecretPage;
|
||||
|
||||
ShareNewPublicSecretPage.requireAuth = false;
|
@@ -2,7 +2,7 @@ import Head from "next/head";
|
||||
|
||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||
|
||||
const SecretApproval = () => {
|
||||
const SecretSharedPublicPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -12,13 +12,13 @@ const SecretApproval = () => {
|
||||
<meta property="og:title" content="" />
|
||||
<meta name="og:description" content="" />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
<ShareSecretPublicPage />
|
||||
<div className="dark h-full">
|
||||
<ShareSecretPublicPage isNewSession={false} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretApproval;
|
||||
export default SecretSharedPublicPage;
|
||||
|
||||
SecretApproval.requireAuth = false;
|
||||
SecretSharedPublicPage.requireAuth = false;
|
||||
|
@@ -179,8 +179,9 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
onChange={(pickedDate) => {
|
||||
pickedDate?.setHours(23, 59, 59, 999); // we choose the end of today not the start of it (going off of aws cloud watch)
|
||||
onChange(pickedDate);
|
||||
setIsEndDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
|
@@ -23,7 +23,8 @@ export const LogsSection = () => {
|
||||
defaultValues: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1))
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
||||
endDate: new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -0,0 +1,229 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
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,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
|
||||
const schema = yup.object({
|
||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
||||
expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
|
||||
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
||||
expiresInUnit: yup.string().required().label("Expiration Unit")
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
export const AddShareSecretForm = ({
|
||||
isPublic,
|
||||
inModal,
|
||||
handleSubmit,
|
||||
control,
|
||||
isSubmitting,
|
||||
setNewSharedSecret
|
||||
}: {
|
||||
isPublic: boolean;
|
||||
inModal: boolean;
|
||||
handleSubmit: any;
|
||||
control: any;
|
||||
isSubmitting: boolean;
|
||||
setNewSharedSecret: (value: string) => void;
|
||||
}) => {
|
||||
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||
|
||||
const expirationUnitsAndActions = [
|
||||
{
|
||||
unit: "Minutes",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Hours",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Days",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Weeks",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
|
||||
}
|
||||
];
|
||||
const onFormSubmit = async ({
|
||||
value,
|
||||
expiresInValue,
|
||||
expiresInUnit,
|
||||
expiresAfterViews
|
||||
}: FormData) => {
|
||||
try {
|
||||
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 expiresAt = new Date();
|
||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||
(item) => item.unit === expiresInUnit
|
||||
)?.action;
|
||||
if (updateExpiresAt && expiresInValue) {
|
||||
updateExpiresAt(expiresAt, expiresInValue);
|
||||
}
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
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 flex-col items-center" onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className={`${!inModal && "border border-mineshaft-600 bg-mineshaft-800 p-4"}`}>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
isVisible
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<div className="w-2/7 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresAfterViews"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-4 w-full"
|
||||
label="Expires After Views"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid number of views"
|
||||
>
|
||||
<Input {...field} type="number" min={1} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/7 flex items-center justify-center px-2">
|
||||
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||
</div>
|
||||
<div className="w-4/7 flex">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-2/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expires after Time"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid time duration"
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-3/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Unit" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirationUnitsAndActions.map(({ unit }) => (
|
||||
<SelectItem value={unit} key={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center ${!inModal && "justify-left pt-1"}`}>
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
{inModal ? "Create" : "Share Secret"}
|
||||
</Button>
|
||||
{inModal && (
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@@ -1,54 +1,14 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
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,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirationUnitsAndActions = [
|
||||
{
|
||||
unit: "Minutes",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Hours",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Days",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Weeks",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
|
||||
}
|
||||
];
|
||||
import { AddShareSecretForm } from "./AddShareSecretForm";
|
||||
import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
|
||||
|
||||
const schema = yup.object({
|
||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
||||
@@ -65,9 +25,11 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
isPublic: boolean;
|
||||
inModal: boolean;
|
||||
};
|
||||
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@@ -76,9 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const createSharedSecret = useCreateSharedSecret();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||
|
||||
const [newSharedSecret, setNewSharedSecret] = useState("");
|
||||
const hasSharedSecret = Boolean(newSharedSecret);
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false
|
||||
@@ -94,199 +55,54 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}, [isUrlCopied]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
value,
|
||||
expiresInValue,
|
||||
expiresInUnit,
|
||||
expiresAfterViews
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
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 expiresAt = new Date();
|
||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||
(item) => item.unit === expiresInUnit
|
||||
)?.action;
|
||||
if (updateExpiresAt && expiresInValue) {
|
||||
updateExpiresAt(expiresAt, expiresInValue);
|
||||
}
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
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 (
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return inModal ? (
|
||||
<Modal
|
||||
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle("createSharedSecret", open);
|
||||
reset();
|
||||
setnewSharedSecret("");
|
||||
setNewSharedSecret("");
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Share a Secret"
|
||||
subTitle="This link is only accessible once. Please share this link with intended recipients. "
|
||||
subTitle="Once you share a secret, the share link is only accessible once."
|
||||
>
|
||||
{!hasSharedSecret ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
isVisible={false}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="w-2/7 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresAfterViews"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-4 w-full"
|
||||
label="Expires After Views"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid number of views"
|
||||
>
|
||||
<Input {...field} type="number" min={1} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/7 flex items-center justify-center px-2">
|
||||
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||
</div>
|
||||
<div className="w-4/7 flex">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-2/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expires after Time"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid time duration"
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-3/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Unit"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirationUnitsAndActions.map(({ unit }) => (
|
||||
<SelectItem value={unit} key={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
<AddShareSecretForm
|
||||
isPublic={isPublic}
|
||||
inModal={inModal}
|
||||
control={control}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
setNewSharedSecret={setNewSharedSecret}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] 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>
|
||||
<ViewAndCopySharedSecret
|
||||
inModal={inModal}
|
||||
newSharedSecret={newSharedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : !hasSharedSecret ? (
|
||||
<AddShareSecretForm
|
||||
isPublic={isPublic}
|
||||
inModal={inModal}
|
||||
control={control}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
setNewSharedSecret={setNewSharedSecret}
|
||||
/>
|
||||
) : (
|
||||
<ViewAndCopySharedSecret
|
||||
inModal={inModal}
|
||||
newSharedSecret={newSharedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@@ -22,7 +22,7 @@ export const ShareSecretSection = () => {
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteSharedSecret.mutateAsync({
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted shared secret",
|
||||
@@ -40,7 +40,6 @@ export const ShareSecretSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Head>
|
||||
<title>Secret Sharing</title>
|
||||
@@ -60,14 +59,18 @@ export const ShareSecretSection = () => {
|
||||
Share Secret
|
||||
</Button>
|
||||
</div>
|
||||
<ShareSecretsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
isPublic={false}
|
||||
inModal
|
||||
/>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
||||
@@ -75,4 +78,4 @@ export const ShareSecretSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@@ -0,0 +1,37 @@
|
||||
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 ${!inModal ? "mx-auto max-w-[40rem]" : ""}`}>
|
||||
<div className={`${!inModal ? "border border-mineshaft-600 bg-mineshaft-800 p-4" : ""}`}>
|
||||
<div className="my-2 flex items-center justify-end rounded-md bg-white/[0.07] 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,26 +1,27 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
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";
|
||||
|
||||
export const ShareSecretPublicPage = () => {
|
||||
export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => {
|
||||
const router = useRouter();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-");
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
|
||||
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||
useEffect(() => {
|
||||
if (!id || !publicKey) {
|
||||
router.push("/404");
|
||||
}
|
||||
}, [id, publicKey]);
|
||||
|
||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
||||
id as string,
|
||||
hashedHex as string
|
||||
@@ -53,35 +54,107 @@ export const ShareSecretPublicPage = () => {
|
||||
navigator.clipboard.writeText(decryptedSecret);
|
||||
setIsUrlCopied(true);
|
||||
};
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
||||
<div className="h-screen bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200">
|
||||
<Head>
|
||||
<title>Secret Shared | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
|
||||
<div className="my-4 flex justify-center md:my-8">
|
||||
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
||||
</div>
|
||||
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
||||
A secret has been shared with you securely via Infisical
|
||||
</p>
|
||||
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
||||
{/* <DragonMainImage /> */}
|
||||
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
|
||||
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
|
||||
Shared Secret
|
||||
</p>
|
||||
<div className="mb-4 rounded-lg md:p-2">
|
||||
<div className="h-screen w-full flex-col items-center justify-center dark:[color-scheme:dark]">
|
||||
<div className="mb-4 flex justify-center pt-8 md:pt-16">
|
||||
<Link href="https://infisical.com">
|
||||
<Image
|
||||
src="/images/gradientLogo.svg"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="Infisical logo"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="mt-6 mb-4 bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-2xl font-medium text-transparent">
|
||||
{id ? "Someone shared a secret on Infisical with you." : "Share Secrets with Infisical"}
|
||||
</h1>
|
||||
<div className="m-auto mt-8 flex w-full max-w-xl justify-center px-4">
|
||||
{id && (
|
||||
<SecretTable
|
||||
isLoading={isLoading}
|
||||
decryptedSecret={decryptedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isNewSession && (
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
inModal={false}
|
||||
isPublic
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-8 px-4 sm:my-8">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
|
||||
<div className="m-auto max-w-xl px-4">
|
||||
{!isNewSession && (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-4">
|
||||
<Button
|
||||
className="bg-mineshaft-700 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createSharedSecret");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Share your own Secret
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-auto mb-8 flex flex max-w-xl flex-col justify-center gap-2 rounded-md border border-primary-500/30 bg-primary/5 p-6">
|
||||
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||
Safe, Secure, & Open Source
|
||||
</p>
|
||||
<p className="md:text-md text-sm">
|
||||
Infisical is the #1 {" "}
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
open source
|
||||
</a>{" "}
|
||||
secrets management platform for developers. <br className="hidden md:inline" />
|
||||
<div className="pb-2" />
|
||||
Infisical Secret Sharing uses end-to-end encrypted architecture to ensure that your secrets are truly private, even from our servers.
|
||||
</p>
|
||||
<Link href="https://infisical.com">
|
||||
<span className="mt-4 cursor-pointer duration-200 hover:text-primary">
|
||||
Learn More <FontAwesomeIcon icon={faArrowRight} />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-0 flex w-full items-center justify-center bg-mineshaft-600 p-2 sm:absolute">
|
||||
<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>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic inModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,14 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export const DragonMainImage = () => {
|
||||
return (
|
||||
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
|
||||
<Image
|
||||
src="/images/dragon-book.svg"
|
||||
height={1000}
|
||||
width={1413}
|
||||
alt="Infisical Dragon - Came to send you a secret!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, IconButton, SecretInput, Td, Tr } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
isLoading: boolean;
|
||||
@@ -16,7 +16,7 @@ export const SecretTable = ({
|
||||
isUrlCopied,
|
||||
copyUrlToClipboard
|
||||
}: Props) => (
|
||||
<div className="flex items-center rounded border border-solid border-mineshaft-700 bg-mineshaft-800 p-4">
|
||||
<div className="flex w-full items-center justify-center rounded 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 && (
|
||||
<Tr>
|
||||
@@ -26,19 +26,23 @@ export const SecretTable = ({
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading && decryptedSecret && (
|
||||
<>
|
||||
<div className="min-w-[12rem] max-w-[20rem] flex-1 break-words pr-4">
|
||||
<SecretInput isVisible value={decryptedSecret} readOnly />
|
||||
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 p-2">
|
||||
<div className="thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll break-words pr-4 dark:[color-scheme:dark]">
|
||||
<div className="align-center flex w-full min-w-full whitespace-pre-line">
|
||||
{decryptedSecret}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
ariaLabel="copy to clipboard"
|
||||
onClick={copyUrlToClipboard}
|
||||
className="rounded p-2 hover:bg-gray-700"
|
||||
className="mx-1 flex max-h-8 items-center rounded"
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
|
||||
</IconButton>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,2 +1 @@
|
||||
export { DragonMainImage } from "./MainImage";
|
||||
export { SecretTable } from "./SecretTable";
|
||||
|
Reference in New Issue
Block a user