This commit is contained in:
Tuan Dang
2023-01-09 10:38:51 +07:00
47 changed files with 3668 additions and 2913 deletions

View File

@ -21,7 +21,7 @@
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
</a>
<a href="">
<a href="https://github.com/Infisical/infisical/issues">
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
@ -40,13 +40,15 @@
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables
- **Personal overrides** for environment variables
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** - check the history of change for any secret
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation**
- 🔜 **2FA**
- 🔜 **Access Logs**
- 🔜 **Slack Integration & MS Teams** integrations
And more.
@ -65,7 +67,7 @@ To quickly get started, visit our [get started guide](https://infisical.com/docs
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
According to a [report](https://www.ekransystem.com/en/blog/secrets-management), only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
If you care about efficiency and security, then Infisical is right for you.
@ -319,7 +321,7 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
## 🚨 Stay Up-to-Date
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot of new features coming very frequently. Watch **releases** of this repository to be notified about future updates:
![infisical-star-github](https://github.com/Infisical/infisical/blob/main/.github/images/star-infisical.gif?raw=true)
@ -331,7 +333,7 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations

View File

@ -1,7 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express';
import express, { Request, Response } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
@ -43,6 +43,8 @@ import {
apiKeyData as v2APIKeyDataRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
@ -101,6 +103,10 @@ app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
// Server status
app.use('/api', healthCheck)
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();

View File

@ -7,6 +7,39 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { validateMembership } from "../../helpers/membership";
import { ADMIN, MEMBER } from '../../variables';
export const createSingleSecret = async (req: Request, res: Response) => {
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environmentName } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
secretKeyTag: secretToCreate.secretKeyTag,
secretKeyHash: secretToCreate.secretKeyHash,
secretValueCiphertext: secretToCreate.secretValueCiphertext,
secretValueIV: secretToCreate.secretValueIV,
secretValueTag: secretToCreate.secretValueTag,
secretValueHash: secretToCreate.secretValueHash,
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
secretCommentIV: secretToCreate.secretCommentIV,
secretCommentTag: secretToCreate.secretCommentTag,
secretCommentHash: secretToCreate.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment: environmentName,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id)
}
const [error, newlyCreatedSecret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
res.status(200).send()
}
export const batchCreateSecrets = async (req: Request, res: Response) => {
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
@ -48,16 +81,6 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
res.status(200).send()
}
export const createSingleSecret = async (req: Request, res: Response) => {
try {
const secretFromDB = await Secret.findById(req.params.secretId)
return res.status(200).send(secretFromDB);
} catch (e) {
throw BadRequestError({ message: "Unable to find the requested secret" })
}
}
export const batchDeleteSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@ -90,6 +113,33 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
res.status(200).send()
}
export const deleteSingleSecret = async (req: Request, res: Response) => {
const { secretId } = req.params;
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
}
if (singleSecretRetrieved) {
const [membershipValidationError, membership] = await to(validateMembership({
userId: req.user._id,
workspaceId: singleSecretRetrieved.workspace._id.toString(),
acceptedRoles: [ADMIN, MEMBER]
}))
if (membershipValidationError || !membership) {
throw UnauthorizedRequestError()
}
await Secret.findByIdAndDelete(secretId)
res.status(200).send()
} else {
throw BadRequestError()
}
}
export const batchModifySecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
@ -101,7 +151,6 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
const updateOperationsToPerform: any = []
secretsModificationsRequested.forEach(userModifiedSecret => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
@ -138,6 +187,38 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
return res.status(200).send()
}
export const modifySingleSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
secretKeyIV: secretModificationsRequested.secretKeyIV,
secretKeyTag: secretModificationsRequested.secretKeyTag,
secretKeyHash: secretModificationsRequested.secretKeyHash,
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
secretValueIV: secretModificationsRequested.secretValueIV,
secretValueTag: secretModificationsRequested.secretValueTag,
secretValueHash: secretModificationsRequested.secretValueHash,
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash,
}
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
}
return res.status(200).send(singleModificationUpdate)
}
export const fetchAllSecrets = async (req: Request, res: Response) => {
const { environment } = req.query;
const { workspaceId } = req.params;
@ -165,4 +246,31 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
}
return res.json(allSecrets)
}
export const fetchSingleSecret = async (req: Request, res: Response) => {
const { secretId } = req.params;
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
}
if (singleSecretRetrieved) {
const [membershipValidationError, membership] = await to(validateMembership({
userId: req.user._id,
workspaceId: singleSecretRetrieved.workspace._id.toString(),
acceptedRoles: [ADMIN, MEMBER]
}))
if (membershipValidationError || !membership) {
throw UnauthorizedRequestError()
}
res.json(singleSecretRetrieved)
} else {
throw BadRequestError()
}
}

View File

@ -6,7 +6,9 @@ const apiLimiter = rateLimit({
max: 450,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => request.path === '/healthcheck'
skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status'
}
});
// 5 requests per hour

View File

@ -0,0 +1,5 @@
import healthCheck from './status';
export {
healthCheck
}

View File

@ -0,0 +1,15 @@
import express, { Request, Response } from 'express';
const router = express.Router();
router.get(
'/status',
(req: Request, res: Response) => {
res.status(200).json({
date: new Date(),
message: 'Ok',
})
}
);
export default router

View File

@ -4,7 +4,7 @@ import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
import { fetchAllSecrets } from '../../controllers/v2/secretController';
import { fetchAllSecrets, fetchSingleSecret } from '../../controllers/v2/secretController';
const router = express.Router();
@ -26,6 +26,24 @@ router.post(
secretController.batchCreateSecrets
);
/**
* Create single secret for a given workspace and environmentName
*/
router.post(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secret').exists().isObject(),
validateRequest,
secretController.createSingleSecret
);
/**
* Get all secrets for a given environment and workspace id
*/
@ -43,6 +61,18 @@ router.get(
fetchAllSecrets
);
/**
* Get single secret by id
*/
router.get(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
validateRequest,
fetchSingleSecret
);
/**
* Batch delete secrets in a given workspace and environment name
*/
@ -62,6 +92,19 @@ router.delete(
);
/**
* delete single secret by id
*/
router.delete(
'/:secretId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('secretId').isMongoId(),
validateRequest,
secretController.deleteSingleSecret
);
/**
* Apply modifications to many existing secrets in a given workspace and environment
*/
@ -80,4 +123,22 @@ router.patch(
secretController.batchModifySecrets
);
/**
* Apply modifications to single existing secret in a given workspace and environment
*/
router.patch(
'/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('secret').isObject(),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.modifySingleSecrets
);
export default router;

View File

@ -313,7 +313,7 @@ func init() {
secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd)
secretsCmd.AddCommand(secretsDeleteCmd)
secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()

View File

@ -55,12 +55,15 @@ func GetKeyRing() (keyring.Keyring, error) {
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
return password, nil
} else {
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
}
fmt.Fprintf(os.Stderr, "%s: ", prompt)
fmt.Fprintf(os.Stderr, "%s:", prompt)
b, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
}
fmt.Println("")
return string(b), nil
}

View File

@ -31,7 +31,6 @@ Inject environment variables from the platform into an application process.
| Option | Description | Default value |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |

View File

@ -10,7 +10,7 @@ infisical secrets
This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment.
### Sub-commands
<Accordion title="infisical secrets">
<Accordion title="infisical secrets" defaultOpen="true">
Use this command to print out all of the secrets in your project
```
@ -33,6 +33,12 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
Default value: `true`
</Accordion>
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>
<Accordion title="infisical secrets get">
@ -52,7 +58,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
```
### Flags
None
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>
<Accordion title="infisical secrets set">
@ -74,7 +84,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
```
### Flags
None
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>
<Accordion title="infisical secrets delete">
@ -89,5 +103,9 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
```
### Flags
None
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>

View File

@ -14,8 +14,6 @@ export const initPostHog = () => {
api_host: POSTHOG_HOST
});
}
console.log("Outside of posthog")
}
return posthog;

View File

@ -4,13 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Error({ text }: { text: string }): JSX.Element {
return (
<div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4">
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="text-white mt-1.5 mb-2 mx-2"
className="text-red mt-1.5 mb-2 mx-2"
/>
{text && (
<p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p>
<p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>
)}
</div>
);

View File

@ -55,7 +55,7 @@ export default function EventFilter({
{selected != '' ? (
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
) : (
<p className="select-none">Select an event</p>
<p className="select-none">{String(t("common:select-event"))}</p>
)}
{selected != '' ? (
<FontAwesomeIcon

View File

@ -11,7 +11,6 @@ import {
faKey,
faMobile,
faPlug,
faTimeline,
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
@ -184,6 +183,7 @@ export default function Layout({ children }: LayoutProps) {
if (
userWorkspaces.length == 0 &&
router.asPath != "/noprojects" &&
!router.asPath.includes("home")&&
!router.asPath.includes("settings")
) {
router.push("/noprojects");

View File

@ -109,7 +109,7 @@ const AddProjectMemberDialog = ({
selected={email ? email : data[0]}
onChange={setEmail}
data={data}
width="full"
isFull={true}
/>
)}
</div>

View File

@ -0,0 +1,77 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Dialog, Transition } from "@headlessui/react";
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
type Props = {
isOpen: boolean
onClose: () => void
onSubmit: () => void
}
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
const { t } = useTranslation()
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {}}>
<div className="fixed inset-0 overflow-y-auto">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
</Transition.Child>
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
{t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
</p>
</div>
<div className="mt-6 flex justify-start">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onSubmit}
>
Delete
</button>
<button
type="button"
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onClose}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};

View File

@ -117,13 +117,13 @@ const UserTable = ({
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-14 bg-white/5"></div>
<table className="w-full my-1">
<thead className="text-gray-400">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5"></div>
<table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light">
<tr>
<th className="text-left pl-6 py-3.5">First Name</th>
<th className="text-left pl-6 py-3.5">Last Name</th>
<th className="text-left pl-6 py-3.5">Email</th>
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
<th className="text-left pl-6 py-3.5">LAST NAME</th>
<th className="text-left pl-6 py-3.5">EMAIL</th>
<th></th>
</tr>
</thead>

View File

@ -0,0 +1,33 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next';
import Button from '../basic/buttons/Button';
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
type Props = {
onSubmit: () => void
}
export function DeleteActionButton({ onSubmit }: Props) {
const { t } = useTranslation();
const [open, setOpen] = useState(false)
return (
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
<Button
text={String(t("Delete"))}
// onButtonPressed={onSubmit}
color="red"
size="md"
onButtonPressed={() => setOpen(true)}
/>
<DeleteEnvVar
isOpen={open}
onClose={() => {
setOpen(false)
}}
onSubmit={onSubmit}
/>
</div>
)
}

View File

@ -3,10 +3,11 @@ import Image from "next/image";
import { useTranslation } from "next-i18next";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { parseDocument, Scalar, YAMLMap } from 'yaml';
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import parse from "../utilities/file";
import { parseDotEnv } from '../utilities/parseDotEnv';
import guidGenerator from "../utilities/randomId";
interface DropZoneProps {
@ -51,6 +52,53 @@ const DropZone = ({
const [loading, setLoading] = useState(false);
const getSecrets = (file: ArrayBuffer, fileType: string) => {
let secrets;
switch (fileType) {
case 'env': {
const keyPairs = parseDotEnv(file);
secrets = Object.keys(keyPairs).map((key, index) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared',
};
});
break;
}
case 'yml': {
const parsedFile = parseDocument(file.toString());
const keyPairs = parsedFile.contents!.toJSON();
secrets = Object.keys(keyPairs).map((key, index) => {
const fileContent = parsedFile.contents as YAMLMap<Scalar, Scalar>;
const comment =
fileContent!.items
.find((item) => item.key.value === key)
?.key?.commentBefore?.split('\n')
.map((comment) => comment.trim())
.join('\n') ?? '';
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment,
type: 'shared',
};
});
break;
}
default:
secrets = '';
break;
}
return secrets;
};
// This function function immediately parses the file after it is dropped
const handleDrop = async (e: DragEvent) => {
setLoading(true);
@ -61,20 +109,12 @@ const DropZone = ({
const file = e.dataTransfer.files[0];
const reader = new FileReader();
const fileType = file.name.split('.')[1];
reader.onload = (event) => {
if (event.target === null || event.target.result === null) return;
// parse function's argument looks like to be ArrayBuffer
const keyPairs = parse(event.target.result as Buffer);
const newData = Object.keys(keyPairs).map((key, index) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs],
type: "shared",
};
});
const newData = getSecrets(event.target.result as ArrayBuffer, fileType);
setData(newData);
setButtonReady(true);
};
@ -95,25 +135,14 @@ const DropZone = ({
setTimeout(() => setLoading(false), 5000);
if (e.currentTarget.files === null) return;
const file = e.currentTarget.files[0];
const fileType = file.name.split('.')[1];
const reader = new FileReader();
reader.onload = (event) => {
if (event.target === null || event.target.result === null) return;
const { result } = event.target;
if (typeof result === "string") {
const newData = result
.split("\n")
.map((line: string, index: number) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: line.split("=")[0],
value: line.split("=").slice(1, line.split("=").length).join("="),
type: "shared",
};
});
setData(newData);
setButtonReady(true);
}
const newData = getSecrets(result as ArrayBuffer, fileType);
setData(newData);
setButtonReady(true);
};
reader.readAsText(file);
};
@ -139,7 +168,7 @@ const DropZone = ({
id="fileSelect"
type="file"
className="opacity-0 absolute w-full h-full"
accept=".txt,.env"
accept=".txt,.env,.yml"
onChange={handleFileSelect}
/>
{errorDragAndDrop ? (
@ -176,7 +205,7 @@ const DropZone = ({
id="fileSelect"
type="file"
className="opacity-0 absolute w-full h-full"
accept=".txt,.env"
accept=".txt,.env,.yml"
onChange={handleFileSelect}
/>
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
@ -187,7 +216,7 @@ const DropZone = ({
<div className="z-10 mb-6">
<Button
color="mineshaft"
text="Create a new .env file"
text={String(t("dashboard:add-secret"))}
onButtonPressed={createNewFile}
size="md"
/>

View File

@ -9,6 +9,7 @@ import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
import GenerateSecretMenu from './GenerateSecretMenu';
@ -28,6 +29,10 @@ interface OverrideProps {
pos: number;
comment: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
}
interface SideBarProps {
toggleSidebar: (value: string) => void;
@ -41,7 +46,7 @@ interface SideBarProps {
savePush: () => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
deleteRow: any;
deleteRow: (props: DeleteRowFunctionProps) => void;
}
/**
@ -77,7 +82,7 @@ const SideBar = ({
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Image
@ -170,14 +175,9 @@ const SideBar = ({
active={buttonReady}
textDisabled="Saved"
/>
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
<Button
text={String(t("Delete"))}
onButtonPressed={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
color="red"
size="md"
/>
</div>
<DeleteActionButton
onSubmit={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
/>
</div>
</div>
};

View File

@ -13,6 +13,7 @@ import {
faGear,
faPlus,
faRightFromBracket,
faUpRightFromSquare,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, Transition } from "@headlessui/react";
@ -107,6 +108,14 @@ export default function Navbar() {
</div>
</div>
<div className="relative flex justify-start items-center mx-2 z-40">
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="text-gray-200 hover:text-primary duration-200">
Docs
<FontAwesomeIcon icon={faUpRightFromSquare} className="text-xs mb-[0.1rem] mr-5 ml-1.5" />
</a>
<Menu as="div" className="relative inline-block text-left">
<div className="mr-4">
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">

View File

@ -0,0 +1,136 @@
import React, { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useTranslation } from "next-i18next";
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
// The style for the verification code input
const props = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
},
} as const;
const propsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
},
} as const;
interface CodeInputStepProps {
email: string;
incrementStep: () => void;
setCode: (value: string) => void;
codeError: boolean;
}
/**
* This is the second step of sign up where users need to verify their email
* @param {object} obj
* @param {string} obj.email - user's email to which we just sent a verification email
* @param {function} obj.incrementStep - goes to the next step of signup
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
* @returns
*/
export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] =
useState(false);
const { t } = useTranslation();
const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true);
setIsLoading(true);
sendVerificationEmail(email);
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 2000);
};
return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">
{"We've"} sent a verification email to{" "}
</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
{email}{" "}
</p>
<div className="hidden md:block">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...props}
className="mt-6 mb-2"
/>
</div>
<div className="block md:hidden">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...propsPhone}
className="mt-2 mb-6"
/>
</div>
{codeError && <Error text={t("signup:step2-code-error")} />}
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button
text={t("signup:verify") ?? ""}
onButtonPressed={incrementStep}
size="lg"
/>
</div>
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">
Not seeing an email?
</span>
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? "Resending..." : "Resend"}
</button>
</u>
</div>
<p className="text-sm text-bunker-400 pb-2">
{t("signup:step2-spam-alert")}
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,60 @@
import React from "react";
import { useTranslation } from "next-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "../basic/buttons/Button";
import issueBackupKey from "../utilities/cryptography/issueBackupKey";
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
email: string;
password: string;
name: string;
}
/**
* This is the step of the signup flow where the user downloads the backup pdf
* @param {object} obj
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
* @param {string} obj.email - user's email
* @param {string} obj.password - user's password
* @param {string} obj.name - user's name
* @returns
*/
export default function DonwloadBackupPDFStep({ incrementStep, email, password, name }: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
return (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t("signup:step4-message")}
</p>
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
<div>{t("signup:step4-description1")}</div>
<div className="mt-3">{t("signup:step4-description2")}</div>
</div>
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t("signup:step4-description3")}
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button
text="Download PDF"
onButtonPressed={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: (value: boolean) => {},
setBackupKeyIssued: (value: boolean) => {},
});
incrementStep();
}}
size="lg"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,97 @@
import React, { useState } from "react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
import Button from "../basic/buttons/Button";
import InputField from "../basic/InputField";
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
email: string;
setEmail: (value: string) => void;
}
/**
* This is the first step of the sign up process - users need to enter their email
* @param {object} obj
* @param {string} obj.email - email of a user signing up
* @param {function} obj.setEmail - funciton that manages the state of the email variable
* @param {function} obj.incrementStep - function to go to the next step of the signup flow
* @returns
*/
export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element {
const [emailError, setEmailError] = useState(false);
const [emailErrorMessage, setEmailErrorMessage] = useState("");
const { t } = useTranslation();
/**
* Verifies if the entered email "looks" correct
*/
const emailCheck = () => {
let emailCheckBool = false;
if (!email) {
setEmailError(true);
setEmailErrorMessage("Please enter your email.");
emailCheckBool = true;
} else if (
!email.includes("@") ||
!email.includes(".") ||
!/[a-z]/.test(email)
) {
setEmailError(true);
setEmailErrorMessage("Please enter a valid email.");
emailCheckBool = true;
} else {
setEmailError(false);
}
// If everything is correct, go to the next step
if (!emailCheckBool) {
sendVerificationEmail(email);
incrementStep();
}
};
return (
<div>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-4xl font-semibold flex justify-center text-primary">
{'Let\''}s get started
</p>
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
<InputField
label="Email"
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
error={emailError}
errorText={emailErrorMessage}
autoComplete="username"
/>
</div>
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
<p className="text-gray-400 mt-2 md:mx-0.5">
{t("signup:step1-privacy")}
</p>
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
<Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" />
</div>
</div>
</div>
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
<Link href="/login">
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
<u className="font-normal text-sm text-primary-500">
{t("signup:already-have-account")}
</u>
</button>
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,72 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import addUserToOrg from "~/pages/api/organization/addUserToOrg";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import Button from "../basic/buttons/Button";
/**
* This is the last step of the signup flow. People can optionally invite their teammates here.
*/
export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState("");
const { t } = useTranslation();
const router = useRouter();
// Redirect user to the getting started page
const redirectToHome = async () => {
const userWorkspaces = await getWorkspaces();
const userWorkspace = userWorkspaces[0]._id;
router.push("/home/" + userWorkspace);
}
const inviteUsers = async ({ emails }: { emails: string; }) => {
emails
.split(',')
.map(email => email.trim())
.map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
await redirectToHome();
}
return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
<p className="text-4xl font-semibold flex justify-center text-primary">
{t("signup:step5-invite-team")}
</p>
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
{t("signup:step5-subtitle")}
</p>
<div>
<div className="overflow-auto bg-bunker-800">
<div className="whitespace-pre-wrap break-words bg-transparent">
</div>
</div>
<textarea
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={emails}
onChange={(e) => setEmails(e.target.value)}
placeholder="email@example.com, email2@example.com..."
/>
</div>
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<div
className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
onClick={redirectToHome}
>
{t("signup:step5-skip")}
</div>
<Button
text={t("signup:step5-send-invites") ?? ""}
onButtonPressed={() => inviteUsers({ emails})}
size="lg"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,306 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import completeAccountInformationSignup from "~/pages/api/auth/CompleteAccountInformationSignup";
import Button from "../basic/buttons/Button";
import InputField from "../basic/InputField";
import attemptLogin from "../utilities/attemptLogin";
import passwordCheck from "../utilities/checks/PasswordCheck";
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
const nacl = require("tweetnacl");
const jsrp = require("jsrp");
nacl.util = require("tweetnacl-util");
const client = new jsrp.client();
interface UserInfoStepProps {
verificationToken: string;
incrementStep: () => void;
email: string;
password: string;
setPassword: (value: string) => void;
firstName: string;
setFirstName: (value: string) => void;
lastName: string;
setLastName: (value: string) => void;
}
/**
* This is the step of the sign up flow where people provife their name/surname and password
* @param {object} obj
* @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
* @param {string} obj.incrementStep - a function to move to the next signup step
* @param {string} obj.email - email of a user who is signing up
* @param {string} obj.password - user's password
* @param {string} obj.setPassword - function managing the state of user's password
* @param {string} obj.firstName - user's first name
* @param {string} obj.setFirstName - function managing the state of user's first name
* @param {string} obj.lastName - user's lastName
* @param {string} obj.setLastName - function managing the state of user's last name
*/
export default function UserInfoStep({
verificationToken,
incrementStep,
email,
password,
setPassword,
firstName,
setFirstName,
lastName,
setLastName
}: UserInfoStepProps): JSX.Element {
const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const router = useRouter();
// Verifies if the information that the users entered (name, workspace)
// is there, and if the password matches the criteria.
const signupErrorCheck = async () => {
setIsLoading(true);
let errorCheck = false;
if (!firstName) {
setFirstNameError(true);
errorCheck = true;
} else {
setFirstNameError(false);
}
if (!lastName) {
setLastNameError(true);
errorCheck = true;
} else {
setLastNameError(false);
}
errorCheck = passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: errorCheck,
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size),
"0"
),
}) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
client.init(
{
username: email,
password: password,
},
async () => {
client.createVerifier(
async (err: any, result: { salt: string; verifier: string }) => {
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
organizationName: firstName + "'s organization",
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken,
});
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
localStorage.setItem("publicKey", PUBLIC_KEY);
localStorage.setItem("encryptedPrivateKey", ciphertext);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
try {
await attemptLogin(
email,
password,
(value: boolean) => {},
router,
true,
false
);
incrementStep();
} catch (error) {
setIsLoading(false);
}
}
}
);
}
);
} else {
setIsLoading(false);
}
};
return (
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t("signup:step3-message")}
</p>
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
<InputField
label={t("common:first-name")}
onChangeHandler={setFirstName}
type="name"
value={firstName}
isRequired
errorText={
t("common:validate-required", {
name: t("common:first-name"),
}) as string
}
error={firstNameError}
autoComplete="given-name"
/>
</div>
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
<InputField
label={t("common:last-name")}
onChangeHandler={setLastName}
type="name"
value={lastName}
isRequired
errorText={
t("common:validate-required", {
name: t("common:last-name"),
}) as string
}
error={lastNameError}
autoComplete="family-name"
/>
</div>
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
<InputField
label={t("section-password:password")}
onChangeHandler={(password: string) => {
setPassword(password);
passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: false,
});
}}
type="password"
value={password}
isRequired
error={
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase
}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength ||
passwordErrorLowerCase ||
passwordErrorNumber ? (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
<div className={`text-gray-400 text-sm mb-1`}>
{t("section-password:validate-base")}
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLength ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLength ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-length")}
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLowerCase ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-case")}
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorNumber ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-number")}
</div>
</div>
</div>
) : (
<div className="py-2"></div>
)}
</div>
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
<Button
text={t("signup:signup") ?? ""}
loading={isLoading}
onButtonPressed={signupErrorCheck}
size="lg"
/>
</div>
</div>
)
}

View File

@ -1,47 +0,0 @@
const LINE =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
/**
* Return text that is the buffer parsed
* @param {Buffer} src - source buffer
* @returns {String} text - text of buffer
*/
function parse(src: Buffer) {
const obj: Record<string, string> = {};
// Convert buffer to string
let lines = src.toString();
// Convert line breaks to same format
lines = lines.replace(/\r\n?/gm, '\n');
let match;
while ((match = LINE.exec(lines)) != null) {
const key = match[1];
// Default undefined or null to empty string
let value = match[2] || '';
// Remove whitespace
value = value.trim();
// Check if double quoted
const maybeQuote = value[0];
// Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2');
// Expand newlines if double quoted
if (maybeQuote === '"') {
value = value.replace(/\\n/g, '\n');
value = value.replace(/\\r/g, '\r');
}
// Add to object
obj[key] = value;
}
return obj;
}
export default parse;

View File

@ -0,0 +1,66 @@
const LINE =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
/**
* Return text that is the buffer parsed
* @param {ArrayBuffer} src - source buffer
* @returns {String} text - text of buffer
*/
export function parseDotEnv(src: ArrayBuffer) {
const object: {
[key: string]: { value: string; comments: string[] };
} = {};
// Convert buffer to string
let lines = src.toString();
// Convert line breaks to same format
lines = lines.replace(/\r\n?/gm, '\n');
let comments: string[] = [];
lines
.split('\n')
.map((line) => {
// collect comments of each env variable
if (line.startsWith('#')) {
comments.push(line.replace('#', '').trim());
} else if (line) {
let match;
let item: [string, string, string[]] | [] = [];
while ((match = LINE.exec(line)) !== null) {
const key = match[1];
// Default undefined or null to empty string
let value = match[2] || '';
// Remove whitespace
value = value.trim();
// Check if double quoted
const maybeQuote = value[0];
// Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2');
// Expand newlines if double quoted
if (maybeQuote === '"') {
value = value.replace(/\\n/g, '\n');
value = value.replace(/\\r/g, '\r');
}
item = [key, value, comments];
}
comments = [];
return item;
}
return [];
})
.filter((line) => line.length > 1)
.forEach((line) => {
const [key, value, comments] = line;
object[key as string] = { value, comments };
});
return object;
}

View File

@ -29,7 +29,7 @@ class Capturer {
}
class Telemetry {
export default class Telemetry {
constructor() {
if (!Telemetry.instance) {
Telemetry.instance = new Capturer();
@ -40,5 +40,3 @@ class Telemetry {
return Telemetry.instance;
}
}
module.exports = Telemetry;

View File

@ -132,7 +132,7 @@ const ActivitySideBar = ({
getLogData();
}, [currentAction]);
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from "next-i18next";
import {
faAngleDown,
@ -69,23 +69,25 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>Timestamp</td>
<td>{String(t("common:timestamp"))}</td>
<td>{row.createdAt}</td>
</tr>}
{payloadOpened &&
row.payload?.map((action, index) =>
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
</td>
</tr>)}
row.payload?.map((action, index) => {
action.secretVersions.length > 0 &&
<tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
</td>
</tr>
})}
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>IP Address</td>
<td>{String(t("common:ip-address"))}</td>
<td>{row.ipAddress}</td>
</tr>}
</>
@ -97,9 +99,12 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
* @param {object} obj
* @param {logData} obj.data - data for user activity logs
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
* @returns
*/
const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => {
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => {
const { t } = useTranslation();
return (
<div className="w-full px-6 mt-8">
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
@ -108,10 +113,10 @@ const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar
<thead className="text-bunker-300">
<tr className='text-sm'>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left font-semibold pt-2.5 pb-3">EVENT</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">USER</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">SOURCE</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">TIME</th>
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th>
<th></th>
</tr>
</thead>
@ -122,6 +127,12 @@ const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar
</tbody>
</table>
</div>
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="loading animation"
></Image></div>}
</div>
);
};

View File

@ -114,7 +114,7 @@ const PITRecoverySidebar = ({
setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between`}>
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@
"@headlessui/react": "^1.6.6",
"@reduxjs/toolkit": "^1.8.3",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@stripe/stripe-js": "^1.46.0",
"add": "^2.0.6",
"axios": "^0.27.2",
"axios-auth-refresh": "^3.3.3",
@ -53,7 +53,8 @@
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"uuid": "^8.3.2",
"uuidv4": "^6.2.13"
"uuidv4": "^6.2.13",
"yaml": "^2.2.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.4",

View File

@ -20,7 +20,7 @@ export default function Custom404() {
src="/images/dragon-404.svg"
height={554}
width={942}
alt="google logo"
alt="infisical dragon - page not found"
></Image>
</div>
</div>

View File

@ -51,6 +51,7 @@ export default function Activity() {
const router = useRouter();
const [eventChosen, setEventChosen] = useState('');
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>()
@ -60,6 +61,7 @@ export default function Activity() {
useEffect(() => {
setCurrentOffset(0);
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(tempLogsData.map((log: logData) => {
return {
@ -77,6 +79,7 @@ export default function Activity() {
})
}
}))
setIsLoading(false);
}
getLogData();
}, [eventChosen]);
@ -84,6 +87,7 @@ export default function Activity() {
// this use effect adds more data in case 'View More' button is clicked
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
return {
@ -101,6 +105,7 @@ export default function Activity() {
})
}
})))
setIsLoading(false);
}
getLogData();
}, [currentLimit, currentOffset]);
@ -115,10 +120,10 @@ export default function Activity() {
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
<div className="flex flex-row justify-start items-center text-3xl">
<p className="font-semibold mr-4 text-bunker-100">Activity Logs</p>
<p className="font-semibold mr-4 text-bunker-100">{t("activity:title")}</p>
</div>
<p className="mr-4 text-base text-gray-400">
Event history for this Infisical project.
{t("activity:subtitle")}
</p>
</div>
<div className="px-6 h-8 mt-2">
@ -130,10 +135,11 @@ export default function Activity() {
<ActivityTable
data={logsData}
toggleSidebar={toggleSidebar}
isLoading={isLoading}
/>
<div className='flex justify-center w-full mb-6'>
<div className='items-center w-60'>
<Button text="View More" textDisabled="End of History" active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
<Button text={String(t("common:view-more"))} textDisabled={String(t("common:end-of-history"))} active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
</div>
</div>
</div>
@ -142,4 +148,4 @@ export default function Activity() {
Activity.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["activity"]);
export const getServerSideProps = getTranslatedServerSideProps(["activity", "common"]);

View File

@ -17,8 +17,10 @@ import {
faPlus,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount';
import PITRecoverySidebar from 'ee/components/PITRecoverySidebar';
import { Document, YAMLSeq } from 'yaml';
import Button from '~/components/basic/buttons/Button';
import ListBox from '~/components/basic/Listbox';
@ -157,7 +159,6 @@ export default function Dashboard() {
useEffect(() => {
(async () => {
try {
console.log(1, 'reloaded')
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) })
setNumSnapshots(tempNumSnapshots);
const userWorkspaces = await getWorkspaces();
@ -396,7 +397,7 @@ export default function Dashboard() {
}
// increasing the number of project commits
setNumSnapshots(numSnapshots ?? 0 + 1);
setNumSnapshots((numSnapshots ?? 0) + 1);
};
const addData = (newData: SecretDataProps[]) => {
@ -426,16 +427,75 @@ export default function Dashboard() {
setData(sortedData);
};
// check if there are secrets with an override
const checkOverrides = (data: SecretDataProps[]) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => secret.type === 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
);
secrets![index].value = secret.value;
});
secrets = secrets!.filter((secret) => secret.type === 'shared');
}
return secrets;
};
// This function downloads the secrets as a .env file
const download = () => {
const file = data!
.map((item: SecretDataProps) => [item.key, item.value].join('='))
const downloadDotEnv = () => {
if (!data) return;
const secrets = checkOverrides(data)
const file = secrets!
.map(
(item: SecretDataProps) =>
`${
item.comment
? item.comment
.split('\n')
.map((comment) => '# '.concat(comment))
.join('\n') + '\n'
: ''
}` + [item.key, item.value].join('=')
)
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.click();
};
// This function downloads the secrets as a .yml file
const downloadYaml = () => {
if (!data) return;
const doc = new Document(new YAMLSeq());
const secrets = checkOverrides(data);
secrets.forEach((secret) => {
const pair = doc.createNode({ [secret.key]: secret.value });
pair.commentBefore = secret.comment
.split('\n')
.map((line) => (line ? ' '.concat(line) : ''))
.join('\n');
doc.add(pair);
});
const file = doc
.toString()
.split('\n')
.map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env';
alink.download = envMapping[env] + '.yml';
alink.click();
};
@ -615,12 +675,50 @@ export default function Dashboard() {
/>
</div>}
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
onButtonPressed={download}
color="mineshaft"
size="icon-md"
icon={faDownload}
/>
<Menu
as="div"
className="relative inline-block text-left"
>
<Menu.Button
as="div"
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<Button
color="mineshaft"
size="icon-md"
icon={faDownload}
onButtonPressed={() => {}}
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={downloadDotEnv}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={downloadYaml}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>}
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
@ -673,7 +771,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
@ -695,7 +795,9 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}

View File

@ -1,7 +1,7 @@
import React from 'react';
import Head from 'next/head';
export default function Activity() {
export default function EmailNotFeriviedPage() {
return (
<div className="bg-bunker-800 md:h-screen flex flex-col justify-between">
<Head>

View File

@ -1,67 +1,24 @@
import { useEffect, useState } from "react";
import ReactCodeInput from "react-code-input";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faCheck, faWarning, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "~/components/basic/buttons/Button";
import Error from "~/components/basic/Error";
import InputField from "~/components/basic/InputField";
import Aes256Gcm from "~/components/utilities/cryptography/aes-256-gcm";
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
import CodeInputStep from "~/components/signup/CodeInputStep";
import DownloadBackupPDF from "~/components/signup/DonwloadBackupPDFStep";
import EnterEmailStep from "~/components/signup/EnterEmailStep";
import TeamInviteStep from "~/components/signup/TeamInviteStep";
import UserInfoStep from "~/components/signup/UserInfoStep";
import { getTranslatedStaticProps } from "~/components/utilities/withTranslateProps";
import attemptLogin from "~/utilities/attemptLogin";
import passwordCheck from "~/utilities/checks/PasswordCheck";
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
import completeAccountInformationSignup from "./api/auth/CompleteAccountInformationSignup";
import sendVerificationEmail from "./api/auth/SendVerificationEmail";
import getWorkspaces from "./api/workspace/getWorkspaces";
// const ReactCodeInput = dynamic(import("react-code-input"));
const nacl = require("tweetnacl");
const jsrp = require("jsrp");
nacl.util = require("tweetnacl-util");
const client = new jsrp.client();
// The stye for the verification code input
const props = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid gray",
textAlign: "center",
},
} as const;
const propsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid gray",
textAlign: "center",
},
} as const;
/**
* @returns the signup page
*/
export default function SignUp() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@ -69,25 +26,9 @@ export default function SignUp() {
const [lastName, setLastName] = useState("");
const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false);
const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] =
useState(false);
const [emailError, setEmailError] = useState(false);
const [emailErrorMessage, setEmailErrorMessage] = useState("");
const [step, setStep] = useState(1);
const router = useRouter();
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] =
useState(false);
const [backupKeyError, setBackupKeyError] = useState(false);
const [verificationToken, setVerificationToken] = useState("");
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const { t } = useTranslation();
@ -104,458 +45,28 @@ export default function SignUp() {
}, []);
/**
* Goes to the following step (out of 3) of the signup process.
* Goes to the following step (out of 5) of the signup process.
* Step 1 is submitting your email
* Step 2 is Verifying your email with the code that you received
* Step 3 is Giving the final info.
* Step 3 is asking the final info.
* Step 4 is downloading a backup pdf
* Step 5 is inviting users
*/
const incrementStep = async () => {
if (step == 1) {
setStep(2);
if (step == 1 || step == 3 || step == 4) {
setStep(step + 1);
} else if (step == 2) {
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200 || code == "111222") {
if (response.status === 200) {
setVerificationToken((await response.json()).token);
setStep(3);
} else {
setCodeError(true);
}
} else if (step == 3) {
setStep(4);
}
};
/**
* Verifies if the entered email "looks" correct
*/
const emailCheck = () => {
let emailCheckBool = false;
if (!email) {
setEmailError(true);
setEmailErrorMessage("Please enter your email.");
emailCheckBool = true;
} else if (
!email.includes("@") ||
!email.includes(".") ||
!/[a-z]/.test(email)
) {
setEmailError(true);
setEmailErrorMessage("Please enter a valid email.");
emailCheckBool = true;
} else {
setEmailError(false);
}
// If everything is correct, go to the next step
if (!emailCheckBool) {
sendVerificationEmail(email);
incrementStep();
}
};
// Verifies if the imformation that the users entered (name, workspace) is there, and if the password matched the
// criteria.
const signupErrorCheck = async () => {
setIsLoading(true);
let errorCheck = false;
if (!firstName) {
setFirstNameError(true);
errorCheck = true;
} else {
setFirstNameError(false);
}
if (!lastName) {
setLastNameError(true);
errorCheck = true;
} else {
setLastNameError(false);
}
errorCheck = passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: errorCheck,
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size),
"0"
),
}) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem("PRIVATE_KEY", PRIVATE_KEY);
client.init(
{
username: email,
password: password,
},
async () => {
client.createVerifier(
async (err: any, result: { salt: string; verifier: string }) => {
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
organizationName: firstName + "'s organization",
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken,
});
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
localStorage.setItem("publicKey", PUBLIC_KEY);
localStorage.setItem("encryptedPrivateKey", ciphertext);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
try {
await attemptLogin(
email,
password,
setErrorLogin,
router,
true,
false
);
incrementStep();
} catch (error) {
setIsLoading(false);
}
}
}
);
}
);
} else {
setIsLoading(false);
}
};
const resendVerificationEmail = async () => {
setIsResendingVerificationEmail(true);
setIsLoading(true);
await sendVerificationEmail(email);
setTimeout(() => {
setIsLoading(false);
setIsResendingVerificationEmail(false);
}, 2000);
};
// Step 1 of the sign up process (enter the email or choose google authentication)
const step1 = (
<div>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-4xl font-semibold flex justify-center text-primary">
{'Let\''}s get started
</p>
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
<InputField
label="Email"
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
error={emailError}
errorText={emailErrorMessage}
autoComplete="username"
/>
</div>
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
<Checkbox className="mr-4"/>
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
</div> */}
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
<p className="text-gray-400 mt-2 md:mx-0.5">
{t("signup:step1-privacy")}
</p>
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
<Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" />
</div>
</div>
</div>
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
<Link href="/login">
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
<u className="font-normal text-sm text-primary-500">
{t("signup:already-have-account")}
</u>
</button>
</Link>
</div>
</div>
);
// Step 2 of the signup process (enter the email verification code)
const step2 = (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-gray-400">
{"We've"} sent a verification email to{" "}
</p>
<p className="text-l flex justify-center font-semibold my-2 text-gray-400">
{email}{" "}
</p>
<div className="hidden md:block">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...props}
className="mt-6 mb-2"
/>
</div>
<div className="block md:hidden">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...propsPhone}
className="mt-2 mb-6"
/>
</div>
{codeError && <Error text={t("signup:step2-code-error")} />}
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button
text={t("signup:verify") ?? ""}
onButtonPressed={incrementStep}
size="lg"
/>
</div>
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-gray-400">
Not seeing an email?
</span>
<u className={`font-normal ${isResendingVerificationEmail ? 'text-gray-400' : 'text-primary-500 hover:opacity-90 duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? "Resending..." : "Resend"}
</button>
</u>
</div>
<p className="text-sm text-gray-500 pb-2">
{t("signup:step2-spam-alert")}
</p>
</div>
</div>
);
// Step 3 of the signup process (enter the rest of the impformation)
const step3 = (
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t("signup:step3-message")}
</p>
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
<InputField
label={t("common:first-name")}
onChangeHandler={setFirstName}
type="name"
value={firstName}
isRequired
errorText={
t("common:validate-required", {
name: t("common:first-name"),
}) as string
}
error={firstNameError}
autoComplete="given-name"
/>
</div>
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
<InputField
label={t("common:last-name")}
onChangeHandler={setLastName}
type="name"
value={lastName}
isRequired
errorText={
t("common:validate-required", {
name: t("common:last-name"),
}) as string
}
error={lastNameError}
autoComplete="family-name"
/>
</div>
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
<InputField
label={t("section-password:password")}
onChangeHandler={(password: string) => {
setPassword(password);
passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: false,
});
}}
type="password"
value={password}
isRequired
error={
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase
}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength ||
passwordErrorLowerCase ||
passwordErrorNumber ? (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
<div className={`text-gray-400 text-sm mb-1`}>
{t("section-password:validate-base")}
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLength ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLength ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-length")}
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLowerCase ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLowerCase ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-case")}
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon
icon={faX}
className="text-md text-red mr-2.5"
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorNumber ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
{t("section-password:validate-number")}
</div>
</div>
</div>
) : (
<div className="py-2"></div>
)}
</div>
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
<Button
text={t("signup:signup") ?? ""}
loading={isLoading}
onButtonPressed={signupErrorCheck}
size="lg"
/>
</div>
</div>
);
// Step 4 of the sign up process (download the emergency kit pdf)
const step4 = (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t("signup:step4-message")}
</p>
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
<div>{t("signup:step4-description1")}</div>
<div className="mt-3">{t("signup:step4-description2")}</div>
</div>
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t("signup:step4-description3")}
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button
text="Download PDF"
onButtonPressed={async () => {
await issueBackupKey({
email,
password,
personalName: firstName + " " + lastName,
setBackupKeyError,
setBackupKeyIssued,
});
const userWorkspaces = await getWorkspaces();
const userWorkspace = userWorkspaces[0]._id;
router.push("/home/" + userWorkspace);
}}
size="lg"
/>
{/* <div
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
onClick={() => {
if (localStorage.getItem("projectData.id")) {
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
} else {
router.push("/noprojects")
}
}}
>
Later
</div> */}
</div>
</div>
);
return (
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
<Head>
@ -580,7 +91,11 @@ export default function SignUp() {
</div>
</Link>
<form onSubmit={(e) => e.preventDefault()}>
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
{step == 1 ? <EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} />
: step == 2 ? <CodeInputStep email={email} incrementStep={incrementStep} setCode={setCode} codeError={codeError}/>
: step == 3 ? <UserInfoStep verificationToken={verificationToken} incrementStep={incrementStep} email={email} password={password} setPassword={setPassword} firstName={firstName} setFirstName={setFirstName} lastName={lastName} setLastName={setLastName}/>
: step == 4 ? <DownloadBackupPDF incrementStep={incrementStep} email={email} password={password} name={firstName + " " + lastName} />
: <TeamInviteStep/>}
</form>
</div>
</div>

View File

@ -349,7 +349,7 @@ export default function SignupInvite() {
setBackupKeyError,
setBackupKeyIssued
});
router.push('/dashboard/');
router.push('/noprojects/');
}}
size="lg"
/>

View File

@ -20,6 +20,22 @@ import addUserToWorkspace from '../api/workspace/addUserToWorkspace';
import getWorkspaceUsers from '../api/workspace/getWorkspaceUsers';
import uploadKeys from '../api/workspace/uploadKeys';
interface UserProps {
firstName: string;
lastName: string;
email: string;
_id: string;
publicKey: string;
}
interface MembershipProps {
user: UserProps
inviteEmail: string;
role: string;
status: string;
_id: string;
}
// #TODO: Update all the workspaceIds
const crypto = require('crypto');
const {
@ -30,10 +46,10 @@ const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
export default function Users() {
let [isAddOpen, setIsAddOpen] = useState(false);
const [isAddOpen, setIsAddOpen] = useState(false);
// let [isDeleteOpen, setIsDeleteOpen] = useState(false);
// let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false);
let [email, setEmail] = useState('');
const [email, setEmail] = useState('');
const [personalEmail, setPersonalEmail] = useState('');
const [searchUsers, setSearchUsers] = useState('');
@ -59,7 +75,7 @@ export default function Users() {
// }
async function submitAddModal() {
let result = await addUserToWorkspace(email, router.query.id);
const result = await addUserToWorkspace(email, String(router.query.id));
if (result?.invitee && result?.latestKey) {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
@ -77,7 +93,7 @@ export default function Users() {
privateKey: PRIVATE_KEY
});
uploadKeys(router.query.id, result.invitee._id, ciphertext, nonce);
uploadKeys(String(router.query.id), result.invitee._id, ciphertext, nonce);
}
setEmail('');
setIsAddOpen(false);
@ -88,41 +104,45 @@ export default function Users() {
setIsAddOpen(true);
}
const [userList, setUserList] = useState();
const [userList, setUserList] = useState([]);
const [orgUserList, setOrgUserList] = useState([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(async () => {
const user = await getUser();
setPersonalEmail(user.email);
workspaceId = router.query.id;
let workspaceUsers = await getWorkspaceUsers({
workspaceId
});
const tempUserList = workspaceUsers.map((user) => ({
key: guidGenerator(),
firstName: user.user?.firstName,
lastName: user.user?.lastName,
email: user.user?.email == null ? user.inviteEmail : user.user?.email,
role: user?.role,
status: user?.status,
userId: user.user?._id,
membershipId: user._id,
publicKey: user.user?.publicKey
}));
setUserList(tempUserList);
const orgUsers = await getOrganizationUsers({
orgId: localStorage.getItem('orgData.id')
});
setOrgUserList(orgUsers);
setEmail(
orgUsers
?.filter((user) => user.status == 'accepted')
.map((user) => user.user.email)
.filter(
(email) => !tempUserList?.map((user1) => user1.email).includes(email)
)[0]
);
useEffect(() => {
(async () => {
const user = await getUser();
setPersonalEmail(user.email);
// This part quiries the current users of a project
const workspaceUsers = await getWorkspaceUsers({
workspaceId: String(router.query.id)
});
const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({
key: guidGenerator(),
firstName: membership.user?.firstName,
lastName: membership.user?.lastName,
email: membership.user?.email == null ? membership.inviteEmail : membership.user?.email,
role: membership?.role,
status: membership?.status,
userId: membership.user?._id,
membershipId: membership._id,
publicKey: membership.user?.publicKey
}));
setUserList(tempUserList);
// This is needed to know wha users from an org (if any), we are able to add to a certain project
const orgUsers = await getOrganizationUsers({
orgId: String(localStorage.getItem('orgData.id'))
});
setOrgUserList(orgUsers);
setEmail(
orgUsers
?.filter((membership: MembershipProps) => membership.status == 'accepted')
.map((membership: MembershipProps) => membership.user.email)
.filter(
(email: string) => !tempUserList?.map((user1: UserProps) => user1.email).includes(email)
)[0]
);
})();
}, []);
return userList ? (
@ -151,17 +171,17 @@ export default function Users() {
submitModal={submitAddModal}
email={email}
data={orgUserList
?.filter((user) => user.status == 'accepted')
.map((user) => user.user.email)
?.filter((membership: MembershipProps) => membership.status == 'accepted')
.map((membership: MembershipProps) => membership.user.email)
.filter(
(email) => !userList?.map((user1) => user1.email).includes(email)
(email) => !userList?.map((user1: UserProps) => user1.email).includes(email)
)}
workspaceId={workspaceId}
setEmail={setEmail}
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="px-2 pb-1 w-full flex flex-row items-start max-w-5xl">
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex flex-row items-center ml-4">
<div className="px-6 pb-1 w-full flex flex-row items-start min-w-6xl max-w-6xl">
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex flex-row items-center">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
icon={faMagnifyingGlass}
@ -170,12 +190,12 @@ export default function Users() {
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
value={searchUsers}
onChange={(e) => setSearchUsers(e.target.value)}
placeholder={t("section-members:search-members")}
placeholder={String(t("section-members:search-members"))}
/>
</div>
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start mr-4">
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
<Button
text={t("section-members:add-member")}
text={String(t("section-members:add-member"))}
onButtonPressed={openAddModal}
color="mineshaft"
size="md"
@ -183,7 +203,7 @@ export default function Users() {
/>
</div>
</div>
<div className="overflow-y-auto max-w-5xl mx-6">
<div className="block overflow-y-auto min-w-6xl max-w-6xl px-6">
<UserTable
userData={userList}
changeData={setUserList}
@ -194,7 +214,6 @@ export default function Users() {
// onClick={openDeleteModal}
// deleteUser={deleteMembership}
// setUserIdToBeDeleted={setUserIdToBeDeleted}
className="w-full mx-6"
/>
</div>
</div>

View File

@ -1,8 +1,11 @@
{
"title": "Activity Logs",
"subtitle": "Event history for this Infisical project.",
"event": {
"readSecrets": "Secrets Viewed",
"updateSecrets": "Secrets Updated",
"addSecrets": "Secrets Added",
"deleteSecrets": "Secrets Deleted"
}
},
"ip-address": "IP Address"
}

View File

@ -13,8 +13,8 @@
"project-id": "Project ID",
"save-changes": "Save Changes",
"saved": "Saved",
"drop-zone": "Drag and drop your .env file here.",
"drop-zone-keys": "Drag and drop your .env file here to add more keys.",
"drop-zone": "Drag and drop a .env or .yml file here.",
"drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.",
"role": "Role",
"role_admin": "admin",
"display-name": "Display Name",
@ -22,5 +22,13 @@
"expired-in": "Expires in",
"language": "Language",
"search": "Search...",
"note": "Note"
"note": "Note",
"view-more": "View More",
"end-of-history": "End of History",
"select-event": "Select an event",
"event": "Event",
"user": "User",
"source": "Source",
"time": "Time",
"timestamp": "Timestamp"
}

View File

@ -10,6 +10,7 @@
"shared-description": "Shared keys are visible to your whole team",
"make-shared": "Make Shared",
"make-personal": "Make Personal",
"add-secret": "Add a new secret",
"check-docs": {
"button": "Check Docs",
"title": "Good job!",
@ -25,6 +26,11 @@
"comments": "Comments & Notes",
"personal-explanation": "This secret is personal. It is not shared with any of your teammates.",
"generate-random-hex": "Generate Random Hex",
"digits": "digits"
"digits": "digits",
"delete-key-dialog": {
"title": "Delete Key",
"confirm-delete-message": "Are you sure you want to delete this secret? This cannot be undone."
}
}
}

View File

@ -17,5 +17,9 @@
"step4-description1": "If you get locked out of your account, your Emergency Kit is the only way to sign in.",
"step4-description2": "We recommend you download it and keep it somewhere safe.",
"step4-description3": "It contains your Secret Key which we cannot access or recover for you if you lose it.",
"step4-download": "Download PDF"
"step4-download": "Download PDF",
"step5-send-invites": "Send Invites",
"step5-invite-team": "Invite your team",
"step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.",
"step5-skip": "Skip"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB