mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical
This commit is contained in:
14
README.md
14
README.md
@ -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:
|
||||
|
||||

|
||||
|
||||
@ -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
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
5
backend/src/routes/status/index.ts
Normal file
5
backend/src/routes/status/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import healthCheck from './status';
|
||||
|
||||
export {
|
||||
healthCheck
|
||||
}
|
15
backend/src/routes/status/status.ts
Normal file
15
backend/src/routes/status/status.ts
Normal 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
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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` |
|
||||
|
@ -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>
|
@ -14,8 +14,6 @@ export const initPostHog = () => {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Outside of posthog")
|
||||
}
|
||||
|
||||
return posthog;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -109,7 +109,7 @@ const AddProjectMemberDialog = ({
|
||||
selected={email ? email : data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
width="full"
|
||||
isFull={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
77
frontend/components/basic/dialog/DeleteEnvVar.tsx
Normal file
77
frontend/components/basic/dialog/DeleteEnvVar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
33
frontend/components/dashboard/DeleteActionButton.tsx
Normal file
33
frontend/components/dashboard/DeleteActionButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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"
|
||||
/>
|
||||
|
@ -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>
|
||||
};
|
||||
|
@ -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">
|
||||
|
136
frontend/components/signup/CodeInputStep.tsx
Normal file
136
frontend/components/signup/CodeInputStep.tsx
Normal 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>
|
||||
);
|
||||
}
|
60
frontend/components/signup/DonwloadBackupPDFStep.tsx
Normal file
60
frontend/components/signup/DonwloadBackupPDFStep.tsx
Normal 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>
|
||||
);
|
||||
}
|
97
frontend/components/signup/EnterEmailStep.tsx
Normal file
97
frontend/components/signup/EnterEmailStep.tsx
Normal 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>
|
||||
);
|
||||
}
|
72
frontend/components/signup/TeamInviteStep.tsx
Normal file
72
frontend/components/signup/TeamInviteStep.tsx
Normal 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>
|
||||
);
|
||||
}
|
306
frontend/components/signup/UserInfoStep.tsx
Normal file
306
frontend/components/signup/UserInfoStep.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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;
|
66
frontend/components/utilities/parseDotEnv.ts
Normal file
66
frontend/components/utilities/parseDotEnv.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
4391
frontend/package-lock.json
generated
4391
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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"]);
|
@ -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}
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -349,7 +349,7 @@ export default function SignupInvite() {
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push('/dashboard/');
|
||||
router.push('/noprojects/');
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
|
@ -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>
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 |
Reference in New Issue
Block a user