Compare commits

..

49 Commits

Author SHA1 Message Date
2473ad03fa Update cli to 0.1.16 to trigger arch linux release 2022-12-26 19:28:18 -05:00
d284040c45 Merge pull request #176 from jon4hz/aur
ci: add aur release
2022-12-26 19:25:51 -05:00
09a04d456e chore: set license to MIT 2022-12-27 00:24:19 +01:00
a9c73ab04c ci: add aur release 2022-12-27 00:19:49 +01:00
a07bfbe4f8 Merge pull request #171 from Infisical/secret-versioning 2022-12-26 17:55:12 -05:00
e3b051226a Merge remote-tracking branch 'origin' into secret-versioning 2022-12-26 17:53:10 -05:00
0c6dfbe4b4 Fix commonjs import/export for patchRouterParam and make secret versioning/snapshots compatible with prev unversioned secrets 2022-12-26 17:52:13 -05:00
0e78336a6e Merge pull request #169 from Infisical/dashboard-sidebar
Added dashboard sidebar
2022-12-26 17:16:12 -05:00
740100a606 Fixed merge conflicts 2022-12-26 17:13:16 -05:00
0a108cbf07 Removed version history for now 2022-12-26 16:48:27 -05:00
dcbf8525bd update secret override desc 2022-12-26 16:01:19 -05:00
79ddada537 Add secret override to docs 2022-12-26 16:01:19 -05:00
44da5da300 update CLI version 2022-12-26 16:01:19 -05:00
9e28ba9b84 Add secret override flag to CLI 2022-12-26 16:01:19 -05:00
454d1d304a Update README.md 2022-12-26 15:10:27 -05:00
8f8c501a64 Merge pull request #172 from akhilmhdh/chore/cleanup-remove-react-import
chore(frontend): removed unneccessary react import in project
2022-12-26 15:08:51 -05:00
5294fe9302 Style updates 2022-12-26 14:58:07 -05:00
665c6b1a6d chore(frontend): removed unneccessary react import in project 2022-12-27 00:42:23 +05:30
676f340928 Fixed the TS error 2022-12-26 11:10:32 -05:00
c556072b5d Fixed the duplicate error 2022-12-26 10:59:47 -05:00
a4285df0ff Added welcome email to new users 2022-12-26 09:39:32 -05:00
20050bcba1 solved the bug with duplicate management 2022-12-25 21:37:53 -05:00
9f724b5ede Refactor EE secret versioning/snapshot access 2022-12-25 20:04:27 -05:00
8f765cba57 Merge remote-tracking branch 'origin' into secret-versioning 2022-12-25 20:03:55 -05:00
f642a46924 Merge pull request #165 from naorpeled/feat/frontend/adjust-registration-styles
feat(frontend/signup+login): improve accessibility and UX
2022-12-25 17:32:38 -05:00
f020b553b3 Solving merge conflicts 2022-12-25 17:28:55 -05:00
26fe1dd821 Move secret versioning and snapshot functionality into ee and begin license scoping 2022-12-25 17:08:21 -05:00
ff3370819d Merge remote-tracking branch 'origin/main' into secret-versioning 2022-12-25 14:31:35 -05:00
f37fc9c59d Small modifications to secret versioning/snapshot 2022-12-25 14:30:02 -05:00
bec139315f Merge remote-tracking branch 'origin/main' into feat/frontend/adjust-registration-styles 2022-12-25 20:32:33 +02:00
890aff813b Merge remote-tracking branch 'origin/main' into secret-versioning 2022-12-25 12:46:57 -05:00
142ed1541c Merge branch 'main' into feat/frontend/adjust-registration-styles 2022-12-25 12:34:10 -05:00
f575ae84e0 removed console.log 2022-12-25 11:47:13 -05:00
0cb26a9495 Added memoization and did performance optimizations 2022-12-25 10:11:13 -05:00
d89af29070 Refactored dashboard to TS - still some bugs and inefficiencies 2022-12-25 00:33:37 -05:00
9c769853b4 Patch secret-override mechanism with versioning/snapshots 2022-12-24 20:01:33 -05:00
9bbf380741 Merge remote-tracking branch 'origin' into secret-versioning 2022-12-24 14:53:23 -05:00
f7e3e48038 Merge branch 'secret-versioning' of https://github.com/Infisical/infisical into secret-versioning 2022-12-24 14:52:14 -05:00
c4ebea7422 Finish get secret versions route 2022-12-24 14:51:09 -05:00
dca3bd4fbb Complete v1 secret versioning and project secret snapshots 2022-12-24 14:51:09 -05:00
205bf70861 Added overrides for secrets 2022-12-23 23:00:26 -05:00
417eddaeff cleanup 2022-12-24 03:43:03 +02:00
6405e9f43f cleanup 2022-12-24 03:37:10 +02:00
13e7883373 wip 2022-12-24 03:34:27 +02:00
d25f4ccc89 wip 2022-12-24 03:33:43 +02:00
71a7497ea7 initial commit 2022-12-24 03:28:58 +02:00
e4e0370dad Complete v1 secret versioning and project secret snapshots 2022-12-23 10:06:37 -05:00
87a9a587b9 Added state management to the dashboard sidebar 2022-12-22 21:42:24 -05:00
092436b6a3 Finished the first ddraft of the dashboard sidebar 2022-12-22 14:15:12 -05:00
68 changed files with 1607 additions and 593 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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);

View File

@ -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
};

View File

@ -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
});
};
};

View File

@ -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);

View File

@ -7,7 +7,7 @@ import {
Integration,
IntegrationAuth,
IUser,
ServiceToken
ServiceToken,
} from '../models';
import {
createWorkspace as create,

View File

@ -1,5 +1,9 @@
import * as stripeController from './stripeController';
import * as secretController from './secretController';
import * as workspaceController from './workspaceController';
export {
stripeController
stripeController,
secretController,
workspaceController
}

View 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
});
}

View 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
});
}

View File

@ -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
}

View 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
}

View File

@ -0,0 +1,9 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion
}

View 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;

View 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;

View File

@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}

View 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;

View 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;

View 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);

View 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;

View File

@ -0,0 +1,7 @@
import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService";
export {
EELicenseService,
EESecretService
}

View File

@ -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,

View File

@ -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',

View File

@ -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',

View File

@ -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,

View File

@ -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
};

View File

@ -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.

View File

@ -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 ...\")")
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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` |

View File

@ -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:

View File

@ -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` |

View File

@ -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);

View 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>
)
}

View File

@ -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}
>

View File

@ -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"

View File

@ -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';

View File

@ -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';

View File

@ -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);

View 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;

View 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);

View 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;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faArrowRight,

View File

@ -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';

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faAngleRight,

View File

@ -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;

View File

@ -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,

View 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;

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import { useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";

View File

@ -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

View File

@ -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");

View File

@ -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';

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useRouter } from "next/router";
export default function Home() {

View File

@ -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";

View File

@ -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>
);
}

View File

@ -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");

View File

@ -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';

View File

@ -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";

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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>
);

View File

@ -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';

View File

@ -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';

View File

@ -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");

View File

@ -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';