mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-21 06:50:07 +00:00
Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
2473ad03fa | |||
d284040c45 | |||
09a04d456e | |||
a9c73ab04c | |||
a07bfbe4f8 | |||
e3b051226a | |||
0c6dfbe4b4 | |||
0e78336a6e | |||
740100a606 | |||
0a108cbf07 | |||
dcbf8525bd | |||
79ddada537 | |||
44da5da300 | |||
9e28ba9b84 | |||
454d1d304a | |||
8f8c501a64 | |||
5294fe9302 | |||
665c6b1a6d | |||
676f340928 | |||
c556072b5d | |||
a4285df0ff | |||
20050bcba1 | |||
9f724b5ede | |||
8f765cba57 | |||
f642a46924 | |||
f020b553b3 | |||
26fe1dd821 | |||
ff3370819d | |||
f37fc9c59d | |||
bec139315f | |||
890aff813b | |||
142ed1541c | |||
f575ae84e0 | |||
0cb26a9495 | |||
d89af29070 | |||
9c769853b4 | |||
9bbf380741 | |||
f7e3e48038 | |||
c4ebea7422 | |||
dca3bd4fbb | |||
205bf70861 | |||
417eddaeff | |||
6405e9f43f | |||
13e7883373 | |||
d25f4ccc89 | |||
71a7497ea7 | |||
e4e0370dad | |||
87a9a587b9 | |||
092436b6a3 |
1
.github/workflows/release_build.yml
vendored
1
.github/workflows/release_build.yml
vendored
@ -41,6 +41,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
|
@ -30,13 +30,13 @@ builds:
|
||||
- openbsd
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- "386"
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
@ -85,7 +85,7 @@ nfpms:
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
description: The offical Infisical CLI
|
||||
license: Apache 2.0
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
@ -101,7 +101,23 @@ scoop:
|
||||
email: ai@infisical.com
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
license: Apache-2.0
|
||||
license: MIT
|
||||
aurs:
|
||||
-
|
||||
name: infisical-bin
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
maintainers:
|
||||
- Infisical, Inc <support@infisical.com>
|
||||
license: MIT
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
|
||||
package: |-
|
||||
# bin
|
||||
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||
# license
|
||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
|
||||
|
||||
# dockers:
|
||||
# - dockerfile: goreleaser.dockerfile
|
||||
# goos: linux
|
||||
|
@ -321,4 +321,4 @@ 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/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/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/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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||
|
||||
import { patchRouterParam } from './utils/patchAsyncRoutes';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
@ -10,6 +11,11 @@ dotenv.config();
|
||||
import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||
import { apiLimiter } from './helpers/rateLimiter';
|
||||
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
} from './ee/routes';
|
||||
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
@ -29,12 +35,13 @@ import {
|
||||
integration as integrationRouter,
|
||||
integrationAuth as integrationAuthRouter
|
||||
} from './routes';
|
||||
|
||||
import { getLogger } from './utils/logger';
|
||||
import { RouteNotFoundError } from './utils/errors';
|
||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||
|
||||
//* Patch Async route params to handle Promise Rejections
|
||||
patchRouterParam()
|
||||
// patch async route params to handle Promise Rejections
|
||||
patchRouterParam();
|
||||
|
||||
export const app = express();
|
||||
|
||||
@ -56,6 +63,10 @@ if (NODE_ENV === 'production') {
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
// /ee routers
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
|
||||
// routers
|
||||
app.use('/api/v1/signup', signupRouter);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
|
@ -41,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||
const LICENSE_KEY = process.env.LICENSE_KEY!;
|
||||
|
||||
export {
|
||||
PORT,
|
||||
@ -83,5 +84,6 @@ export {
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
TELEMETRY_ENABLED
|
||||
TELEMETRY_ENABLED,
|
||||
LICENSE_KEY
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../models';
|
||||
import { Key, Secret } from '../models';
|
||||
import {
|
||||
pushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
@ -169,9 +169,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
// get (encrypted) secrets from workspace with id [workspaceId]
|
||||
// service token route
|
||||
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
@ -225,4 +222,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
||||
};
|
@ -10,6 +10,7 @@ import {
|
||||
} from '../helpers/signup';
|
||||
import { issueTokens, createToken } from '../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../variables';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
|
||||
token = tokens.token;
|
||||
refreshToken = tokens.refreshToken;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (process.env.LOOPS_API_KEY) {
|
||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
IUser,
|
||||
ServiceToken
|
||||
ServiceToken,
|
||||
} from '../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
|
@ -1,5 +1,9 @@
|
||||
import * as stripeController from './stripeController';
|
||||
import * as secretController from './secretController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
stripeController
|
||||
stripeController,
|
||||
secretController,
|
||||
workspaceController
|
||||
}
|
35
backend/src/ee/controllers/secretController.ts
Normal file
35
backend/src/ee/controllers/secretController.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretVersion } from '../models';
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
let secretVersions;
|
||||
try {
|
||||
const { secretId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret versions'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretVersions
|
||||
});
|
||||
}
|
35
backend/src/ee/controllers/workspaceController.ts
Normal file
35
backend/src/ee/controllers/workspaceController.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
||||
let secretSnapshots;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret snapshots'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshots
|
||||
});
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.licenseKey - Infisical license key
|
||||
*/
|
||||
const checkLicenseKey = ({
|
||||
licenseKey
|
||||
}: {
|
||||
licenseKey: string
|
||||
}) => {
|
||||
try {
|
||||
// TODO
|
||||
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
checkLicenseKey
|
||||
}
|
74
backend/src/ee/helpers/secret.ts
Normal file
74
backend/src/ee/helpers/secret.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
try {
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
if (!latestSecretSnapshot) {
|
||||
// case: no snapshots exist for workspace -> create first snapshot
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// case: snapshots exist for workspace
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot.version + 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
}
|
||||
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
}) => {
|
||||
try {
|
||||
await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add secret versions');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
}
|
9
backend/src/ee/models/index.ts
Normal file
9
backend/src/ee/models/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
ISecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
}
|
109
backend/src/ee/models/secretSnapshot.ts
Normal file
109
backend/src/ee/models/secretSnapshot.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
version: number;
|
||||
secrets: {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
environment: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
secrets: [{
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
||||
|
||||
export default SecretSnapshot;
|
75
backend/src/ee/models/secretVersion.ts
Normal file
75
backend/src/ee/models/secretVersion.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
{
|
||||
secret: { // could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Secret',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||
|
||||
export default SecretVersion;
|
7
backend/src/ee/routes/index.ts
Normal file
7
backend/src/ee/routes/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import secret from './secret';
|
||||
import workspace from './workspace';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
}
|
26
backend/src/ee/routes/secret.ts
Normal file
26
backend/src/ee/routes/secret.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
secretController.getSecretVersions
|
||||
);
|
||||
|
||||
export default router;
|
27
backend/src/ee/routes/workspace.ts
Normal file
27
backend/src/ee/routes/workspace.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { workspaceController } from '../controllers';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceSecretSnapshots
|
||||
);
|
||||
|
||||
|
||||
export default router;
|
19
backend/src/ee/services/EELicenseService.ts
Normal file
19
backend/src/ee/services/EELicenseService.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { LICENSE_KEY } from '../../config';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition license actions
|
||||
*/
|
||||
class EELicenseService {
|
||||
|
||||
private readonly _isLicenseValid: boolean;
|
||||
|
||||
constructor(licenseKey: string) {
|
||||
this._isLicenseValid = true;
|
||||
}
|
||||
|
||||
public get isLicenseValid(): boolean {
|
||||
return this._isLicenseValid;
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService(LICENSE_KEY);
|
47
backend/src/ee/services/EESecretService.ts
Normal file
47
backend/src/ee/services/EESecretService.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { ISecretVersion } from '../models';
|
||||
import {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
} from '../helpers/secret';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition secret actions
|
||||
*/
|
||||
class EESecretService {
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* SecretSnapshot collection.
|
||||
* Requires a valid license key [licenseKey]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
static async takeSecretSnapshot({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await takeSecretSnapshotHelper({ workspaceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {SecretVersion} obj.secretVersions
|
||||
*/
|
||||
static async addSecretVersions({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EESecretService;
|
7
backend/src/ee/services/index.ts
Normal file
7
backend/src/ee/services/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import EELicenseService from "./EELicenseService";
|
||||
import EESecretService from "./EESecretService";
|
||||
|
||||
export {
|
||||
EELicenseService,
|
||||
EESecretService
|
||||
}
|
@ -1,10 +1,20 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
ISecret
|
||||
ISecret,
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
SecretVersion
|
||||
} from '../ee/models';
|
||||
import {
|
||||
takeSecretSnapshotHelper
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
import { LICENSE_KEY } from '../config';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@ -19,7 +29,7 @@ interface PushSecret {
|
||||
}
|
||||
|
||||
interface Update {
|
||||
[index: string]: string;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
@ -46,6 +56,7 @@ const pushSecrets = async ({
|
||||
environment: string;
|
||||
secrets: PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
@ -53,74 +64,124 @@ const pushSecrets = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
|
||||
return { ...accumulator, [s.secretKeyHash]: s };
|
||||
}, {});
|
||||
const newSecretsObj = secrets.reduce((accumulator, s) => {
|
||||
return { ...accumulator, [s.hashKey]: s };
|
||||
}, {});
|
||||
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||
({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
|
||||
, {});
|
||||
|
||||
// handle deleting secrets
|
||||
const toDelete = oldSecrets.filter(
|
||||
(s: ISecret) => !(s.secretKeyHash in newSecretsObj)
|
||||
);
|
||||
const toDelete = oldSecrets
|
||||
.filter(
|
||||
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||
)
|
||||
.map((s) => s._id);
|
||||
if (toDelete.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete.map((s) => s._id) }
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
// handle modifying secrets where type or value changed
|
||||
const operations = secrets
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (s.hashKey in oldSecretsObj) {
|
||||
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) {
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) {
|
||||
// case: filter secrets where value changed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s.type !== oldSecretsObj[s.hashKey].type) {
|
||||
// case: filter secrets where type changed
|
||||
if (!s.version) {
|
||||
// case: filter (legacy) secrets that were not versioned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
})
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
type: s.type,
|
||||
secretValueCiphertext: s.ciphertextValue,
|
||||
secretValueIV: s.ivValue,
|
||||
secretValueTag: s.tagValue,
|
||||
secretValueHash: s.hashValue
|
||||
};
|
||||
secretValueCiphertext: ciphertextValue,
|
||||
secretValueIV: ivValue,
|
||||
secretValueTag: tagValue,
|
||||
secretValueHash: hashValue
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user assocaited with the personal secret
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: workspaceId,
|
||||
_id: oldSecretsObj[s.hashKey]._id
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
const a = await Secret.bulkWrite(operations as any);
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map(({
|
||||
_id,
|
||||
version,
|
||||
type,
|
||||
secretKeyHash,
|
||||
}) => {
|
||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||
return ({
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||
secretKeyIV: newSecret.ivKey,
|
||||
secretKeyTag: newSecret.tagKey,
|
||||
secretKeyHash: newSecret.hashKey,
|
||||
secretValueCiphertext: newSecret.ciphertextValue,
|
||||
secretValueIV: newSecret.ivValue,
|
||||
secretValueTag: newSecret.tagValue,
|
||||
secretValueHash: newSecret.hashValue
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj));
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
await Secret.insertMany(
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map((s, idx) => {
|
||||
let obj: any = {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
@ -141,7 +202,39 @@ const pushSecrets = async ({
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -295,6 +388,8 @@ const decryptSecrets = ({
|
||||
return content;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export {
|
||||
pushSecrets,
|
||||
pullSecrets,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
|
||||
export interface ISecret {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
@ -26,6 +27,10 @@ export interface ISecret {
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
|
@ -45,7 +45,7 @@ function wrap(fn) {
|
||||
return copyFnProps(fn, newFn);
|
||||
}
|
||||
|
||||
export function patchRouterParam() {
|
||||
function patchRouterParam() {
|
||||
const originalParam = Router.prototype.constructor.param;
|
||||
Router.prototype.constructor.param = function param(name, fn) {
|
||||
fn = wrap(fn);
|
||||
@ -62,4 +62,8 @@ Object.defineProperty(Layer.prototype, 'handle', {
|
||||
fn = wrap(fn);
|
||||
this.__handle = fn;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
patchRouterParam
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
|
||||
Short: "Infisical CLI is used to inject environment variables into any process",
|
||||
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
||||
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
|
||||
Version: "0.1.14",
|
||||
Version: "0.1.16",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
@ -60,6 +60,13 @@ var runCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the secret-overriding flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
@ -84,6 +91,10 @@ var runCmd = &cobra.Command{
|
||||
secrets = util.SubstituteSecrets(secrets)
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideWithPersonalSecrets(secrets)
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
err = executeMultipleCommandWithEnvs(command, secrets)
|
||||
@ -108,6 +119,7 @@ func init() {
|
||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets")
|
||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ type ConfigFile struct {
|
||||
type SingleEnvironmentVariable struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type WorkspaceConfigFile struct {
|
||||
|
@ -14,6 +14,9 @@ import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
const PERSONAL_SECRET_TYPE_NAME = "personal"
|
||||
const SHARED_SECRET_TYPE_NAME = "shared"
|
||||
|
||||
func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) {
|
||||
var pullSecretsRequestResponse models.PullSecretsResponse
|
||||
response, err := httpClient.
|
||||
@ -78,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
|
||||
env := models.SingleEnvironmentVariable{
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
}
|
||||
|
||||
listOfEnv = append(listOfEnv, env)
|
||||
@ -187,6 +191,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
|
||||
env := models.SingleEnvironmentVariable{
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
}
|
||||
|
||||
listOfEnv = append(listOfEnv, env)
|
||||
@ -335,9 +340,48 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
|
||||
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: expandedVariable,
|
||||
Type: secret.Type,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return expandedSecrets
|
||||
}
|
||||
|
||||
// if two secrets with the same name are found, the one that has type `personal` will be in the returned list
|
||||
func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
|
||||
personalSecret := make(map[string]models.SingleEnvironmentVariable)
|
||||
sharedSecret := make(map[string]models.SingleEnvironmentVariable)
|
||||
secretsToReturn := []models.SingleEnvironmentVariable{}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if secret.Type == PERSONAL_SECRET_TYPE_NAME {
|
||||
personalSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
}
|
||||
|
||||
if secret.Type == SHARED_SECRET_TYPE_NAME {
|
||||
sharedSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
personalValue, personalExists := personalSecret[secret.Key]
|
||||
sharedValue, sharedExists := sharedSecret[secret.Key]
|
||||
|
||||
if personalExists && sharedExists || personalExists && !sharedExists {
|
||||
secretsToReturn = append(secretsToReturn, personalValue)
|
||||
} else {
|
||||
secretsToReturn = append(secretsToReturn, sharedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return secretsToReturn
|
||||
}
|
||||
|
@ -34,3 +34,4 @@ Inject environment variables from the platform into an application process.
|
||||
| `--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` |
|
||||
|
@ -38,13 +38,7 @@ infisical init
|
||||
infisical run -- [your application start command]
|
||||
```
|
||||
|
||||
Options you can specify:
|
||||
|
||||
| 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` |
|
||||
View all available options for `run` command [here](./commands/run)
|
||||
|
||||
## Examples:
|
||||
|
||||
|
@ -28,6 +28,7 @@ Configuring Infisical requires setting some environment variables. There is a fi
|
||||
| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` |
|
||||
| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` |
|
||||
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
|
||||
| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` |
|
||||
| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` |
|
||||
| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` |
|
||||
| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { memo,useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@ -96,6 +96,7 @@ const InputField = (
|
||||
/>
|
||||
{props.label?.includes('Password') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
}}
|
||||
@ -139,4 +140,4 @@ const InputField = (
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(InputField);
|
||||
export default memo(InputField);
|
||||
|
81
frontend/components/basic/Toggle.tsx
Normal file
81
frontend/components/basic/Toggle.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
id: string;
|
||||
deleteOverride: (id: string) => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||
* @param obj
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {string} obj.keyName - key of a certain secret
|
||||
* @param {string} obj.value - value of a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
#TODO: make the secret id persistent?
|
||||
* @param {string} obj.id - id of a certain secret
|
||||
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle ({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
keyName,
|
||||
value,
|
||||
pos,
|
||||
id,
|
||||
deleteOverride,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: ToggleProps): JSX.Element {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride({ id, keyName, value, pos });
|
||||
setSharedToHide([
|
||||
...sharedToHide!,
|
||||
id
|
||||
])
|
||||
} else {
|
||||
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
|
||||
deleteOverride(id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
className={`${
|
||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className="sr-only">Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import Image from "next/image";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
@ -16,8 +16,9 @@ type ButtonProps = {
|
||||
size: string;
|
||||
icon?: IconProp;
|
||||
active?: boolean;
|
||||
iconDisabled?: string;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>['type'];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -72,15 +73,16 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
props.color == "mineshaft" && "text-gray-400",
|
||||
props.color != "mineshaft" && props.color != "red" && "text-black",
|
||||
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
||||
props.color == "red" && "text-gray-200",
|
||||
activityStatus && props.color != "red" ? "group-hover:text-black" : "",
|
||||
props.color == "none" && "text-gray-200 text-xl",
|
||||
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
||||
|
||||
props.size == "icon" && "flex items-center justify-center"
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"relative duration-200",
|
||||
"relative duration-200 text-center w-full",
|
||||
|
||||
// Show the loading sign if the loading indicator is on
|
||||
props.loading ? "opacity-0" : "opacity-100",
|
||||
@ -91,6 +93,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
const button = (
|
||||
<button
|
||||
disabled={!activityStatus}
|
||||
type={props.type}
|
||||
onClick={props.onButtonPressed}
|
||||
className={styleButton}
|
||||
>
|
||||
|
@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
|
||||
interface PopupProps {
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
titleText: string;
|
||||
emoji: string;
|
||||
textLine1: string;
|
||||
textLine2: string;
|
||||
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||
* @param {object} org
|
||||
@ -23,16 +33,16 @@ export default function BottonRightPopup({
|
||||
textLine1,
|
||||
textLine2,
|
||||
setCheckDocsPopUpVisible,
|
||||
}) {
|
||||
}: PopupProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||
<div>{titleText}</div>
|
||||
<div class="ml-2.5">{emoji}</div>
|
||||
<div className="ml-2.5">{emoji}</div>
|
||||
</div>
|
||||
<button
|
||||
className="mt-1"
|
||||
@ -44,14 +54,14 @@ export default function BottonRightPopup({
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
{textLine1}
|
||||
</div>
|
||||
<div class="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="flex flex-row px-6 w-full">
|
||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
href={buttonLink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { SyntheticEvent, useRef } from 'react';
|
||||
import { memo, SyntheticEvent, useRef } from 'react';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@ -11,19 +11,21 @@ interface DashboardInputFieldProps {
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
value: string;
|
||||
type: 'varName' | 'value';
|
||||
blurred: boolean;
|
||||
duplicates: string[];
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
override?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the input fields on the dashboard
|
||||
* @param {object} obj - the order number of a keyPair
|
||||
* @param {number} obj.pos - the order number of a keyPair
|
||||
* @param {number} obj.position - the order number of a keyPair
|
||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||
* @param {string} obj.value - value of the InputField
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {string[]} obj.duplicates - list of all the duplicated key names on the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@ -33,7 +35,8 @@ const DashboardInputField = ({
|
||||
type,
|
||||
value,
|
||||
blurred,
|
||||
duplicates
|
||||
isDuplicate,
|
||||
override
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
@ -45,8 +48,7 @@ const DashboardInputField = ({
|
||||
|
||||
if (type === 'varName') {
|
||||
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
|
||||
const hasDuplicates = duplicates?.includes(value);
|
||||
const error = startsWithNumber || hasDuplicates;
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
@ -72,7 +74,7 @@ const DashboardInputField = ({
|
||||
Should not start with a number
|
||||
</p>
|
||||
)}
|
||||
{hasDuplicates && !startsWithNumber && (
|
||||
{isDuplicate && !startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Secret names should be unique
|
||||
</p>
|
||||
@ -85,6 +87,7 @@ const DashboardInputField = ({
|
||||
<div
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
||||
>
|
||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
@ -99,10 +102,13 @@ const DashboardInputField = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
blurred
|
||||
blurred && !override
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||
: ''
|
||||
} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
} ${
|
||||
override ? 'text-primary-300' : 'text-gray-400'
|
||||
}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
@ -153,4 +159,8 @@ const DashboardInputField = ({
|
||||
return <>Something Wrong</>;
|
||||
};
|
||||
|
||||
export default React.memo(DashboardInputField);
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
|
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { Fragment,useState } from 'react';
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
|
||||
/**
|
||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||
* @returns the popup-menu for randomly generating secrets
|
||||
*/
|
||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
|
||||
return <Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md 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">
|
||||
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
||||
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<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 px-1 py-1">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
|
||||
export default GenerateSecretMenu;
|
102
frontend/components/dashboard/KeyPair.tsx
Normal file
102
frontend/components/dashboard/KeyPair.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
deleteRow: (id: string) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId
|
||||
}: KeyPairProps) => {
|
||||
return (
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={keyPair.type == "personal"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2"></div>
|
||||
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="none"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(KeyPair);
|
171
frontend/components/dashboard/SideBar.tsx
Normal file
171
frontend/components/dashboard/SideBar.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import { useState } from 'react';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SecretVersionList from 'ee/components/SecretVersionList';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
deleteOverride: (id: string) => void;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
|
||||
* @param {function} obj.modifyKey - function that modifies the secret key
|
||||
* @param {function} obj.modifyValue - function that modifies the secret value
|
||||
* @param {function} obj.addOverride - override a certain secret
|
||||
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
|
||||
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
|
||||
* @param {function} obj.savePush - save changes andp ush secrets
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns the sidebar with 'secret's settings'
|
||||
*/
|
||||
const SideBar = ({
|
||||
toggleSidebar,
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
addOverride,
|
||||
deleteOverride,
|
||||
buttonReady,
|
||||
savePush,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: SideBarProps) => {
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
|
||||
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'>
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">Secret</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>Key</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0].pos}
|
||||
value={data[0].key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>Value</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
|
||||
This secret is personal. It is not shared with any of your teammates.
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0].key}
|
||||
value={data[0].value}
|
||||
pos={data[0].pos}
|
||||
id={data[0].id}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].value : data[0].value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
|
||||
<p className='text-sm text-bunker-200'>Group</p>
|
||||
<ListBox
|
||||
selected={"Database Secrets"}
|
||||
onChange={() => {}}
|
||||
data={["Group1"]}
|
||||
isFull={true}
|
||||
/>
|
||||
</div> */}
|
||||
<div className={`relative mt-4 px-4 pt-4`}>
|
||||
<div className='flex flex-row justify-between'>
|
||||
<p className='text-sm text-bunker-300'>Comments & notes</p>
|
||||
<div className="bg-yellow rounded-md h-min">
|
||||
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
|
||||
Leave your comment here...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SideBar;
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faAngleRight,
|
||||
|
@ -39,7 +39,7 @@ const getSecretsForProject = async ({
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempFileState: { key: string; value: string; type: string }[] = [];
|
||||
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
|
||||
if (file.key) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
@ -97,7 +97,7 @@ const getSecretsForProject = async ({
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
}
|
||||
return true;
|
||||
return [];
|
||||
};
|
||||
|
||||
export default getSecretsForProject;
|
||||
|
@ -51,7 +51,7 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
iv: ivKey,
|
||||
tag: tagKey,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
plaintext: key.slice(1),
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
@ -65,13 +65,13 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = obj[key as keyof typeof obj][1] != null ? obj[key as keyof typeof obj][1] : "personal";
|
||||
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||
|
||||
return {
|
||||
ciphertextKey,
|
||||
ivKey,
|
||||
tagKey,
|
||||
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
|
||||
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
|
44
frontend/ee/components/SecretVersionList.tsx
Normal file
44
frontend/ee/components/SecretVersionList.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SecretVersionListProps {}
|
||||
|
||||
const versionData = [{
|
||||
value: "Value1",
|
||||
date: "Date1",
|
||||
user: "vlad@infisical.com"
|
||||
}, {
|
||||
value: "Value2",
|
||||
date: "Date2",
|
||||
user: "tony@infisical.com"
|
||||
}]
|
||||
|
||||
/**
|
||||
* @returns a list of the versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = () => {
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>Version History</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||
<div className='h-48 overflow-y-scroll overflow-x-none'>
|
||||
{versionData.map((version, index) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>{version.date}</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -6,208 +6,68 @@ import {
|
||||
faArrowDownAZ,
|
||||
faArrowDownZA,
|
||||
faCheck,
|
||||
faCircleInfo,
|
||||
faCopy,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFolderOpen,
|
||||
faMagnifyingGlass,
|
||||
faPeopleGroup,
|
||||
faPerson,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
import DashboardInputField from '~/components/dashboard/DashboardInputField';
|
||||
import DropZone from '~/components/dashboard/DropZone';
|
||||
import KeyPair from '~/components/dashboard/KeyPair';
|
||||
import SideBar from '~/components/dashboard/SideBar';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
||||
import pushKeys from '~/components/utilities/secrets/pushKeys';
|
||||
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import { envMapping } from '../../public/data/frequentConstants';
|
||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyVisibility - switch between public/private visibility
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {string[]} obj.duplicates - list of all the duplicates secret names on the dashboard
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyVisibility,
|
||||
isBlurred,
|
||||
duplicates
|
||||
}) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
|
||||
return (
|
||||
<div className="px-1 flex flex-col items-center ml-1">
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-2">
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex items-center md:px-1 rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
duplicates={duplicates}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.value}
|
||||
blurred={isBlurred}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md 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">
|
||||
<div className="cursor-pointer w-9 h-9 bg-white/10 rounded-md flex flex-row justify-center items-center opacity-50 hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<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 px-1 py-1">
|
||||
<div
|
||||
onClick={() =>
|
||||
modifyVisibility(
|
||||
keyPair.type == 'personal' ? 'shared' : 'personal',
|
||||
keyPair.pos
|
||||
)
|
||||
}
|
||||
className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={keyPair.type == 'personal' ? faPeopleGroup : faPerson}
|
||||
/>
|
||||
<div className="text-sm">
|
||||
{keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
keyPair.pos
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={keyPair.value == '' ? faPlus : faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<div className="w-2"></div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* this function finds the teh duplicates in an array
|
||||
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
|
||||
* @returns - a list with duplicates
|
||||
*/
|
||||
function findDuplicates(arr: any[]) {
|
||||
const map = new Map();
|
||||
return arr.filter((item) => {
|
||||
if (map.has(item)) {
|
||||
map.set(item, false);
|
||||
return true;
|
||||
} else {
|
||||
map.set(item, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets)
|
||||
* @returns
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
const [data, setData] = useState();
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useState('');
|
||||
@ -227,6 +87,8 @@ export default function Dashboard() {
|
||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
@ -249,7 +111,7 @@ export default function Dashboard() {
|
||||
useEffect(() => {
|
||||
const warningText =
|
||||
'Do you want to save your results before leaving this page?';
|
||||
const handleWindowClose = (e) => {
|
||||
const handleWindowClose = (e: any) => {
|
||||
if (!buttonReady) return;
|
||||
e.preventDefault();
|
||||
return (e.returnValue = warningText);
|
||||
@ -265,18 +127,18 @@ export default function Dashboard() {
|
||||
/**
|
||||
* Reorder rows alphabetically or in the opprosite order
|
||||
*/
|
||||
const reorderRows = (dataToReorder) => {
|
||||
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
|
||||
setSortMethod((prevSort) =>
|
||||
prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical'
|
||||
);
|
||||
|
||||
sortValuesHandler(dataToReorder);
|
||||
sortValuesHandler(dataToReorder, undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
||||
if (
|
||||
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
|
||||
@ -288,31 +150,41 @@ export default function Dashboard() {
|
||||
router.push(router.asPath.split('?')[0] + '?' + env);
|
||||
}
|
||||
setBlurred(true);
|
||||
setWorkspaceId(router.query.id);
|
||||
setWorkspaceId(String(router.query.id));
|
||||
|
||||
const dataToSort = await getSecretsForProject({
|
||||
env,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: router.query.id
|
||||
workspaceId: String(router.query.id)
|
||||
});
|
||||
reorderRows(dataToSort);
|
||||
|
||||
setSharedToHide(
|
||||
dataToSort?.filter(row => (dataToSort
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
dataToSort?.map((item) => item.key).indexOf(item)
|
||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
||||
)
|
||||
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(new Date()) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
let userAction = await checkUserAction({
|
||||
const userAction = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
setHasUserEverPushed(userAction ? true : false);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
setData([]);
|
||||
setData(undefined);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -321,10 +193,10 @@ export default function Dashboard() {
|
||||
const addRow = () => {
|
||||
setIsNew(false);
|
||||
setData([
|
||||
...data,
|
||||
...data!,
|
||||
{
|
||||
id: guidGenerator(),
|
||||
pos: data.length,
|
||||
pos: data!.length,
|
||||
key: '',
|
||||
value: '',
|
||||
type: 'shared'
|
||||
@ -332,45 +204,85 @@ export default function Dashboard() {
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteRow = (id) => {
|
||||
setButtonReady(true);
|
||||
setData(data.filter((row) => row.id !== id));
|
||||
interface overrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function add an ovverrided version of a certain secret to the current user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.id - if of this secret that is about to be overriden
|
||||
* @param {string} obj.keyName - key name of this secret
|
||||
* @param {string} obj.value - value of this secret
|
||||
* @param {string} obj.pos - position of this secret on the dashboard
|
||||
*/
|
||||
const addOverride = ({ id, keyName, value, pos }: overrideProps) => {
|
||||
setIsNew(false);
|
||||
const tempdata: SecretDataProps[] | 1 = [
|
||||
...data!,
|
||||
{
|
||||
id: id,
|
||||
pos: pos,
|
||||
key: keyName,
|
||||
value: value,
|
||||
type: 'personal'
|
||||
}
|
||||
];
|
||||
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
||||
};
|
||||
|
||||
const modifyValue = (value, pos) => {
|
||||
const deleteRow = (id: string) => {
|
||||
setButtonReady(true);
|
||||
setData(data!.filter((row: SecretDataProps) => row.id !== id));
|
||||
};
|
||||
|
||||
/**
|
||||
* This function deleted the override of a certain secrer
|
||||
* @param {string} id - id of a secret to be deleted
|
||||
*/
|
||||
const deleteOverride = (id: string) => {
|
||||
setButtonReady(true);
|
||||
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
|
||||
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
|
||||
};
|
||||
|
||||
const modifyValue = (value: string, pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].value = value;
|
||||
return [...oldData];
|
||||
oldData![pos].value = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyKey = (value, pos) => {
|
||||
const modifyKey = (value: string, pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].key = value;
|
||||
return [...oldData];
|
||||
oldData![pos].key = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyVisibility = (value, pos) => {
|
||||
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].type = value;
|
||||
return [...oldData];
|
||||
oldData![pos].type = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
// For speed purposes and better perforamance, we are using useCallback
|
||||
const listenChangeValue = useCallback((value, pos) => {
|
||||
const listenChangeValue = useCallback((value: string, pos: number) => {
|
||||
modifyValue(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeKey = useCallback((value, pos) => {
|
||||
const listenChangeKey = useCallback((value: string, pos: number) => {
|
||||
modifyKey(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeVisibility = useCallback((value, pos) => {
|
||||
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
|
||||
modifyVisibility(value, pos);
|
||||
}, []);
|
||||
|
||||
@ -379,21 +291,16 @@ export default function Dashboard() {
|
||||
*/
|
||||
const savePush = async () => {
|
||||
// Format the new object with environment variables
|
||||
let obj = Object.assign(
|
||||
const obj = Object.assign(
|
||||
{},
|
||||
...data.map((row) => ({ [row.key]: [row.value, row.type] }))
|
||||
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.type] }))
|
||||
);
|
||||
|
||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||
const nameErrors = !Object.keys(obj)
|
||||
.map((key) => !isNaN(key.charAt(0)))
|
||||
.map((key) => !isNaN(Number(key[0].charAt(0))))
|
||||
.every((v) => v === false);
|
||||
const duplicatesExist =
|
||||
data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) => index !== data?.map((item) => item.key).indexOf(item)
|
||||
).length > 0;
|
||||
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
|
||||
|
||||
if (nameErrors) {
|
||||
return createNotification({
|
||||
@ -409,9 +316,11 @@ export default function Dashboard() {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('pushing', obj)
|
||||
|
||||
// Once "Save changed is clicked", disable that button
|
||||
setButtonReady(false);
|
||||
pushKeys({ obj, workspaceId: router.query.id, env });
|
||||
pushKeys({ obj, workspaceId: String(router.query.id), env });
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
@ -420,8 +329,8 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const addData = (newData) => {
|
||||
setData(data.concat(newData));
|
||||
const addData = (newData: SecretDataProps[]) => {
|
||||
setData(data!.concat(newData));
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
@ -429,37 +338,39 @@ export default function Dashboard() {
|
||||
setBlurred(!blurred);
|
||||
};
|
||||
|
||||
const sortValuesHandler = (dataToSort) => {
|
||||
const sortedData = (dataToSort != 1 ? dataToSort : data)
|
||||
.sort((a, b) =>
|
||||
sortMethod == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index
|
||||
};
|
||||
});
|
||||
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
||||
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||
.sort((a, b) =>
|
||||
howToSort == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item: SecretDataProps, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index
|
||||
};
|
||||
});
|
||||
console.log('override', sortedData)
|
||||
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
// This function downloads the secrets as a .env file
|
||||
const download = () => {
|
||||
const file = data
|
||||
.map((item) => [item.key, item.value].join('='))
|
||||
const file = data!
|
||||
.map((item: SecretDataProps) => [item.key, item.value].join('='))
|
||||
.join('\n');
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
let alink = document.createElement('a');
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.env';
|
||||
alink.click();
|
||||
};
|
||||
|
||||
const deleteCertainRow = (id) => {
|
||||
const deleteCertainRow = (id: string) => {
|
||||
deleteRow(id);
|
||||
};
|
||||
|
||||
@ -467,15 +378,17 @@ export default function Dashboard() {
|
||||
* This function copies the project id to the clipboard
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
var copyText = document.getElementById('myInput');
|
||||
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||
|
||||
if (copyText) {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return data ? (
|
||||
@ -491,6 +404,18 @@ export default function Dashboard() {
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
{sidebarSecretId != "None" && <SideBar
|
||||
toggleSidebar={toggleSidebar}
|
||||
data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyValue={listenChangeValue}
|
||||
addOverride={addOverride}
|
||||
deleteOverride={deleteOverride}
|
||||
buttonReady={buttonReady}
|
||||
savePush={savePush}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>}
|
||||
<div className="w-full max-h-96 pb-2">
|
||||
<NavHeader pageName="Secrets" isProjectRelated={true} />
|
||||
{checkDocsPopUpVisible && (
|
||||
@ -513,7 +438,6 @@ export default function Dashboard() {
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -568,7 +492,6 @@ export default function Dashboard() {
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>
|
||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
@ -630,144 +553,55 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
{data?.length !== 0 ? (
|
||||
<div
|
||||
id="dataall"
|
||||
className="flex flex-col max-h-40 grow max-h-[calc(100vh-240px)] w-full overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
|
||||
>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-1 rounded-md pb-2 max-w-5xl overflow-visible`}
|
||||
>
|
||||
<div className="rounded-t-md sticky top-0 z-20 bg-bunker flex flex-row pl-4 pr-6 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Personal</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 mt-1 text-lg"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Personal keys are only visible to you
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data1" className="">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair.key
|
||||
.toLowerCase()
|
||||
.includes(searchKeys.toLowerCase()) &&
|
||||
keyPair.type == 'personal'
|
||||
)
|
||||
?.map((keyPair) => (
|
||||
<KeyPair
|
||||
<div className="flex flex-col w-full mt-1 mb-2">
|
||||
<div className='bg-mineshaft-800 rounded-md px-2 py-2 max-w-5xl'>
|
||||
<div
|
||||
className={`mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
<div className="px-1 pt-2">
|
||||
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
duplicates={data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
data?.map((item) => item.key).indexOf(item)
|
||||
)}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-2 rounded-md p-1 pb-2 max-w-5xl ${
|
||||
data?.length > 8 ? 'h-3/4' : 'h-min'
|
||||
}`}
|
||||
>
|
||||
<div className="sticky top-0 z-40 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Shared</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 text-lg mt-1"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Shared keys are visible to your whole team
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-5xl px-2 pt-2">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data2" className="data2">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair.key
|
||||
.toLowerCase()
|
||||
.includes(searchKeys.toLowerCase()) &&
|
||||
keyPair.type == 'shared'
|
||||
)
|
||||
?.map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
duplicates={data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
data?.map((item) => item.key).indexOf(item)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-5xl">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
||||
{fileState.message != "There's nothing to pull" &&
|
||||
fileState.message != undefined && (
|
||||
<FontAwesomeIcon
|
||||
className="text-7xl mb-8"
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
)}
|
||||
{(fileState.message == "There's nothing to pull" ||
|
||||
fileState.message == undefined) &&
|
||||
isKeyAvailable && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
)}
|
||||
{fileState.message ==
|
||||
'Failed membership validation for workspace' && (
|
||||
<p>You are not authorized to view this project.</p>
|
||||
{isKeyAvailable && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
numCurrentRows={data.length}
|
||||
keysExist={false}
|
||||
/>
|
||||
)}
|
||||
{fileState.message == 'Access needed to pull the latest file' ||
|
||||
{
|
||||
// fileState.message == 'Access needed to pull the latest file' ||
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Home() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@ -38,6 +38,10 @@ export default function Login() {
|
||||
* This function check if the user entered the correct credentials and should be allowed to log in.
|
||||
*/
|
||||
const loginCheck = async () => {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
await attemptLogin(
|
||||
email,
|
||||
@ -45,7 +49,7 @@ export default function Login() {
|
||||
setErrorLogin,
|
||||
router,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
).then(() => {
|
||||
setTimeout(function () {
|
||||
setIsLoading(false);
|
||||
@ -75,68 +79,73 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
Log in to your account
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">Forgot password?</Link>
|
||||
</div>
|
||||
</div>
|
||||
{errorLogin && <Error text="Your email and/or password are wrong." />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
text="Log In"
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
<form
|
||||
onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
Log in to your account
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">Forgot password?</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && errorLogin && <Error text="Your email and/or password are wrong." />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Log In"
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
|
||||
</div> */}
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
We are experiencing minor technical difficulties. We are working on
|
||||
solving it right now. Please come back in a few minutes.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
Need an Infisical account?
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
|
||||
Sign up here.
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
We are experiencing minor technical difficulties. We are working on
|
||||
solving it right now. Please come back in a few minutes.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
Need an Infisical account?
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
|
||||
Sign up here.
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
|
||||
import Plan from "~/components/billing/Plan";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
@ -260,46 +260,49 @@ export default function SignUp() {
|
||||
|
||||
// Step 1 of the sign up process (enter the email or choose google authentication)
|
||||
const step1 = (
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 mb-48 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
{'Let\''}s get started
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-h-24 max-w-md mx-auto pt-2">
|
||||
<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">
|
||||
By creating an account, you agree to our Terms and have read and
|
||||
acknowledged the Privacy Policy.
|
||||
</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 className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-md text-sky-500">
|
||||
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-sm text-primary-500">
|
||||
Have an account? Log in
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<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">
|
||||
By creating an account, you agree to our Terms and have read and
|
||||
acknowledged the Privacy Policy.
|
||||
</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<Button loading={isLoading} text="Get Started" onButtonPressed={emailCheck} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
// Step 2 of the signup process (enter the email verification code)
|
||||
@ -340,11 +343,11 @@ export default function SignUp() {
|
||||
<Button text="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">
|
||||
<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 text-sm ${isResendingVerificationEmail ? 'text-gray-400' : 'text-sky-500 hover:opacity-90 duration-200'}`}>
|
||||
<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>
|
||||
@ -512,7 +515,7 @@ export default function SignUp() {
|
||||
It contains your Secret Key which we cannot access or recover for you if
|
||||
you lose it.
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-max mx-auto mt-6 py-1 md:mt-4 text-lg text-center md:text-left">
|
||||
<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 () => {
|
||||
@ -521,11 +524,9 @@ export default function SignUp() {
|
||||
password,
|
||||
personalName: firstName + ' ' + lastName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push('/home/' + userWorkspace);
|
||||
router.push('/dashboard/');
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
@ -571,7 +572,9 @@ export default function SignUp() {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
Reference in New Issue
Block a user