mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge remote-tracking branch 'origin' into service-token-v2
This commit is contained in:
11
.github/workflows/release_build.yml
vendored
11
.github/workflows/release_build.yml
vendored
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -24,6 +24,15 @@ jobs:
|
||||
go-version: '>=1.19.3'
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
run: |
|
||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update && apt-cache policy libssl1.0-dev
|
||||
sudo apt-get install libssl1.0-dev
|
||||
- name: OSXCross for CGO Support
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser
|
||||
|
||||
@@ -7,12 +7,23 @@
|
||||
# # you may remove this if you don't need go generate
|
||||
# - cd cli && go generate ./...
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
- id: darwin-build
|
||||
binary: infisical
|
||||
id: infisical
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=/home/runner/work/osxcross/target/bin/o64-clang
|
||||
- CXX=/home/runner/work/osxcross/target/bin/o64-clang++
|
||||
goos:
|
||||
- darwin
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
dir: ./cli
|
||||
- id: all-other-builds
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: infisical
|
||||
goos:
|
||||
- freebsd
|
||||
- linux
|
||||
- netbsd
|
||||
@@ -27,8 +38,6 @@ builds:
|
||||
- 6
|
||||
- 7
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
- goos: freebsd
|
||||
@@ -71,7 +80,7 @@ nfpms:
|
||||
- id: infisical
|
||||
package_name: infisical
|
||||
builds:
|
||||
- infisical
|
||||
- all-other-builds
|
||||
vendor: Infisical, Inc
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
|
||||
@@ -321,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||
|
||||
import { patchRouterParam } from './utils/patchAsyncRoutes';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
@@ -10,6 +11,11 @@ dotenv.config();
|
||||
import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||
import { apiLimiter } from './helpers/rateLimiter';
|
||||
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
} from './ee/routes';
|
||||
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
@@ -30,12 +36,13 @@ import {
|
||||
integrationAuth as integrationAuthRouter,
|
||||
apiKey as apiKeyRouter
|
||||
} 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();
|
||||
|
||||
@@ -57,6 +64,10 @@ if (NODE_ENV === 'production') {
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
// /ee routers
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
|
||||
// routers
|
||||
app.use('/api/v1/signup', signupRouter);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
|
||||
@@ -41,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||
const LICENSE_KEY = process.env.LICENSE_KEY!;
|
||||
|
||||
export {
|
||||
PORT,
|
||||
@@ -83,5 +84,6 @@ export {
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
TELEMETRY_ENABLED
|
||||
TELEMETRY_ENABLED,
|
||||
LICENSE_KEY
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../models';
|
||||
import { Key, Secret } from '../models';
|
||||
import {
|
||||
pushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
@@ -169,9 +169,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
// get (encrypted) secrets from workspace with id [workspaceId]
|
||||
// service token route
|
||||
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
@@ -225,4 +222,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../helpers/signup';
|
||||
import { issueTokens, createToken } from '../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../variables';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
@@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
|
||||
token = tokens.token;
|
||||
refreshToken = tokens.refreshToken;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (process.env.LOOPS_API_KEY) {
|
||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
IUser,
|
||||
ServiceToken
|
||||
ServiceToken,
|
||||
} from '../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as stripeController from './stripeController';
|
||||
import * as secretController from './secretController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
stripeController
|
||||
stripeController,
|
||||
secretController,
|
||||
workspaceController
|
||||
}
|
||||
35
backend/src/ee/controllers/secretController.ts
Normal file
35
backend/src/ee/controllers/secretController.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretVersion } from '../models';
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
let secretVersions;
|
||||
try {
|
||||
const { secretId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret versions'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
35
backend/src/ee/controllers/workspaceController.ts
Normal file
35
backend/src/ee/controllers/workspaceController.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
||||
let secretSnapshots;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret snapshots'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshots
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.licenseKey - Infisical license key
|
||||
*/
|
||||
const checkLicenseKey = ({
|
||||
licenseKey
|
||||
}: {
|
||||
licenseKey: string
|
||||
}) => {
|
||||
try {
|
||||
// TODO
|
||||
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
checkLicenseKey
|
||||
}
|
||||
74
backend/src/ee/helpers/secret.ts
Normal file
74
backend/src/ee/helpers/secret.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
try {
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
if (!latestSecretSnapshot) {
|
||||
// case: no snapshots exist for workspace -> create first snapshot
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// case: snapshots exist for workspace
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot.version + 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
}
|
||||
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
}) => {
|
||||
try {
|
||||
await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add secret versions');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
}
|
||||
9
backend/src/ee/models/index.ts
Normal file
9
backend/src/ee/models/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
ISecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
}
|
||||
109
backend/src/ee/models/secretSnapshot.ts
Normal file
109
backend/src/ee/models/secretSnapshot.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
version: number;
|
||||
secrets: {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
environment: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
secrets: [{
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
||||
|
||||
export default SecretSnapshot;
|
||||
75
backend/src/ee/models/secretVersion.ts
Normal file
75
backend/src/ee/models/secretVersion.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
{
|
||||
secret: { // could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Secret',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||
|
||||
export default SecretVersion;
|
||||
7
backend/src/ee/routes/index.ts
Normal file
7
backend/src/ee/routes/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import secret from './secret';
|
||||
import workspace from './workspace';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
}
|
||||
26
backend/src/ee/routes/secret.ts
Normal file
26
backend/src/ee/routes/secret.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
secretController.getSecretVersions
|
||||
);
|
||||
|
||||
export default router;
|
||||
27
backend/src/ee/routes/workspace.ts
Normal file
27
backend/src/ee/routes/workspace.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { workspaceController } from '../controllers';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceSecretSnapshots
|
||||
);
|
||||
|
||||
|
||||
export default router;
|
||||
19
backend/src/ee/services/EELicenseService.ts
Normal file
19
backend/src/ee/services/EELicenseService.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { LICENSE_KEY } from '../../config';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition license actions
|
||||
*/
|
||||
class EELicenseService {
|
||||
|
||||
private readonly _isLicenseValid: boolean;
|
||||
|
||||
constructor(licenseKey: string) {
|
||||
this._isLicenseValid = true;
|
||||
}
|
||||
|
||||
public get isLicenseValid(): boolean {
|
||||
return this._isLicenseValid;
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService(LICENSE_KEY);
|
||||
47
backend/src/ee/services/EESecretService.ts
Normal file
47
backend/src/ee/services/EESecretService.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ISecretVersion } from '../models';
|
||||
import {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
} from '../helpers/secret';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition secret actions
|
||||
*/
|
||||
class EESecretService {
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* SecretSnapshot collection.
|
||||
* Requires a valid license key [licenseKey]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
static async takeSecretSnapshot({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await takeSecretSnapshotHelper({ workspaceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {SecretVersion} obj.secretVersions
|
||||
*/
|
||||
static async addSecretVersions({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EESecretService;
|
||||
7
backend/src/ee/services/index.ts
Normal file
7
backend/src/ee/services/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import EELicenseService from "./EELicenseService";
|
||||
import EESecretService from "./EESecretService";
|
||||
|
||||
export {
|
||||
EELicenseService,
|
||||
EESecretService
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
ISecret
|
||||
ISecret,
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
SecretVersion
|
||||
} from '../ee/models';
|
||||
import {
|
||||
takeSecretSnapshotHelper
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
import { LICENSE_KEY } from '../config';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@@ -19,7 +29,7 @@ interface PushSecret {
|
||||
}
|
||||
|
||||
interface Update {
|
||||
[index: string]: string;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
@@ -46,6 +56,7 @@ const pushSecrets = async ({
|
||||
environment: string;
|
||||
secrets: PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
@@ -53,74 +64,124 @@ const pushSecrets = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
|
||||
return { ...accumulator, [s.secretKeyHash]: s };
|
||||
}, {});
|
||||
const newSecretsObj = secrets.reduce((accumulator, s) => {
|
||||
return { ...accumulator, [s.hashKey]: s };
|
||||
}, {});
|
||||
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||
({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
|
||||
, {});
|
||||
|
||||
// handle deleting secrets
|
||||
const toDelete = oldSecrets.filter(
|
||||
(s: ISecret) => !(s.secretKeyHash in newSecretsObj)
|
||||
);
|
||||
const toDelete = oldSecrets
|
||||
.filter(
|
||||
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||
)
|
||||
.map((s) => s._id);
|
||||
if (toDelete.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete.map((s) => s._id) }
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
// handle modifying secrets where type or value changed
|
||||
const operations = secrets
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (s.hashKey in oldSecretsObj) {
|
||||
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) {
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) {
|
||||
// case: filter secrets where value changed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s.type !== oldSecretsObj[s.hashKey].type) {
|
||||
// case: filter secrets where type changed
|
||||
if (!s.version) {
|
||||
// case: filter (legacy) secrets that were not versioned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
})
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
type: s.type,
|
||||
secretValueCiphertext: s.ciphertextValue,
|
||||
secretValueIV: s.ivValue,
|
||||
secretValueTag: s.tagValue,
|
||||
secretValueHash: s.hashValue
|
||||
};
|
||||
secretValueCiphertext: ciphertextValue,
|
||||
secretValueIV: ivValue,
|
||||
secretValueTag: tagValue,
|
||||
secretValueHash: hashValue
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user assocaited with the personal secret
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: workspaceId,
|
||||
_id: oldSecretsObj[s.hashKey]._id
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
const a = await Secret.bulkWrite(operations as any);
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map(({
|
||||
_id,
|
||||
version,
|
||||
type,
|
||||
secretKeyHash,
|
||||
}) => {
|
||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||
return ({
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||
secretKeyIV: newSecret.ivKey,
|
||||
secretKeyTag: newSecret.tagKey,
|
||||
secretKeyHash: newSecret.hashKey,
|
||||
secretValueCiphertext: newSecret.ciphertextValue,
|
||||
secretValueIV: newSecret.ivValue,
|
||||
secretValueTag: newSecret.tagValue,
|
||||
secretValueHash: newSecret.hashValue
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj));
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
await Secret.insertMany(
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map((s, idx) => {
|
||||
let obj: any = {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
@@ -141,7 +202,39 @@ const pushSecrets = async ({
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -295,6 +388,8 @@ const decryptSecrets = ({
|
||||
return content;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export {
|
||||
pushSecrets,
|
||||
pullSecrets,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
export interface ISecret {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
@@ -26,6 +27,10 @@ export interface ISecret {
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
|
||||
@@ -45,7 +45,7 @@ function wrap(fn) {
|
||||
return copyFnProps(fn, newFn);
|
||||
}
|
||||
|
||||
export function patchRouterParam() {
|
||||
function patchRouterParam() {
|
||||
const originalParam = Router.prototype.constructor.param;
|
||||
Router.prototype.constructor.param = function param(name, fn) {
|
||||
fn = wrap(fn);
|
||||
@@ -62,4 +62,8 @@ Object.defineProperty(Layer.prototype, 'handle', {
|
||||
fn = wrap(fn);
|
||||
this.__handle = fn;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
patchRouterParam
|
||||
};
|
||||
|
||||
@@ -91,7 +91,9 @@ var loginCmd = &cobra.Command{
|
||||
|
||||
err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to store your credentials in system key ring")
|
||||
currentVault, _ := util.GetCurrentVaultBackend()
|
||||
log.Errorf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault)
|
||||
log.Errorln("To trouble shoot further, read https://infisical.com/docs/cli/faq")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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.12",
|
||||
Version: "0.1.15",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
@@ -31,8 +31,6 @@ func init() {
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging")
|
||||
rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend")
|
||||
|
||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
util.InitKeyRingInstance()
|
||||
}
|
||||
// rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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 ...\")")
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ var vaultSetCmd = &cobra.Command{
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]")
|
||||
log.Errorf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,6 +19,7 @@ func GetHomeDir() (string, error) {
|
||||
return directory, err
|
||||
}
|
||||
|
||||
// write file to given path. If path does not exist throw error
|
||||
func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error {
|
||||
err := os.WriteFile(fileName, dataToWrite, filePerm)
|
||||
if err != nil {
|
||||
|
||||
@@ -187,7 +187,7 @@ func GetConfigFile() (models.ConfigFile, error) {
|
||||
|
||||
// Write a ConfigFile to disk. Raise error if unable to save the model to ask
|
||||
func WriteConfigFile(configFile *models.ConfigFile) error {
|
||||
fullConfigFilePath, _, err := GetFullConfigFilePath()
|
||||
fullConfigFilePath, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
@@ -197,8 +197,20 @@ func WriteConfigFile(configFile *models.ConfigFile) error {
|
||||
return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when marshalling the config file [err=%s]", err)
|
||||
}
|
||||
|
||||
// check if config folder exists and if not create it
|
||||
if _, err := os.Stat(fullConfigFileDirPath); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(fullConfigFileDirPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create file in directory
|
||||
err = WriteToFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
|
||||
err = os.WriteFile(fullConfigFilePath, configFileMarshalled, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when write the config to file [err=%s]", err)
|
||||
|
||||
|
||||
@@ -19,7 +19,13 @@ func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error {
|
||||
return fmt.Errorf("StoreUserCredsInKeyRing: something went wrong when marshalling user creds [err=%s]", err)
|
||||
}
|
||||
|
||||
err = keyringInstance.Set(keyring.Item{
|
||||
// Get keyring
|
||||
configuredKeyring, err := GetKeyRing()
|
||||
if err != nil {
|
||||
return fmt.Errorf("StoreUserCredsInKeyRing: unable to get keyring instance with [err=%s]", err)
|
||||
}
|
||||
|
||||
err = configuredKeyring.Set(keyring.Item{
|
||||
Key: userCred.Email,
|
||||
Data: []byte(string(userCredMarshalled)),
|
||||
})
|
||||
@@ -32,20 +38,26 @@ func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentials, err error) {
|
||||
credentialsValue, err := keyringInstance.Get(userEmail)
|
||||
// Get keyring
|
||||
configuredKeyring, err := GetKeyRing()
|
||||
if err != nil {
|
||||
return models.UserCredentials{}, fmt.Errorf("Unable to get key from Keyring. could not find login credentials in your Keyring. This is common if you have switched vault backend recently. If so, please login in again and retry:", err)
|
||||
return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get keyring instance with [err=%s]", err)
|
||||
}
|
||||
|
||||
credentialsValue, err := configuredKeyring.Get(userEmail)
|
||||
if err != nil {
|
||||
return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: unable to get key from Keyring. could not find login credentials in your Keyring. This is common if you have switched vault backend recently. If so, please login in again and retry [err=%s]", err)
|
||||
}
|
||||
|
||||
var userCredentials models.UserCredentials
|
||||
|
||||
err = json.Unmarshal([]byte(credentialsValue.Data), &userCredentials)
|
||||
if err != nil {
|
||||
return models.UserCredentials{}, fmt.Errorf("Something went wrong when unmarshalling user creds:", err)
|
||||
return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return models.UserCredentials{}, fmt.Errorf("Unable to store user credentials", err)
|
||||
return models.UserCredentials{}, fmt.Errorf("GetUserCredsFromKeyRing: Unable to store user credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
return userCredentials, err
|
||||
@@ -82,7 +94,7 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) {
|
||||
|
||||
if response.StatusCode() > 299 {
|
||||
log.Infoln("Login expired, please login again.")
|
||||
return false, "", fmt.Errorf("Login expired, please login again.")
|
||||
return false, "", fmt.Errorf("GetUserCredsFromKeyRing: Login expired, please login again.")
|
||||
}
|
||||
|
||||
return true, configFile.LoggedInUserEmail, nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,14 +5,9 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/99designs/keyring"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// Keyring instance
|
||||
var keyringInstance keyring.Keyring
|
||||
var keyringInstanceConfig keyring.Config
|
||||
|
||||
func GetCurrentVaultBackend() (keyring.BackendType, error) {
|
||||
configFile, err := GetConfigFile()
|
||||
if err != nil {
|
||||
@@ -20,21 +15,19 @@ func GetCurrentVaultBackend() (keyring.BackendType, error) {
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType == "" {
|
||||
if keyring.AvailableBackends()[0] == keyring.FileBackend {
|
||||
}
|
||||
return keyring.AvailableBackends()[0], nil
|
||||
}
|
||||
|
||||
return configFile.VaultBackendType, nil
|
||||
}
|
||||
|
||||
func InitKeyRingInstance() {
|
||||
func GetKeyRing() (keyring.Keyring, error) {
|
||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||
if err != nil {
|
||||
log.Infof("InitKeyRingInstance: unable to get the current vault backend, [err=%s]", err)
|
||||
return nil, fmt.Errorf("GetKeyRing: unable to get the current vault backend, [err=%s]", err)
|
||||
}
|
||||
|
||||
keyringInstanceConfig = keyring.Config{
|
||||
keyringInstanceConfig := keyring.Config{
|
||||
FilePasswordFunc: fileKeyringPassphrasePrompt,
|
||||
ServiceName: SERVICE_NAME,
|
||||
LibSecretCollectionName: SERVICE_NAME,
|
||||
@@ -51,10 +44,12 @@ func InitKeyRingInstance() {
|
||||
keyringInstanceConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(currentVaultBackend)}
|
||||
}
|
||||
|
||||
keyringInstance, err = keyring.Open(keyringInstanceConfig)
|
||||
keyringInstance, err := keyring.Open(keyringInstanceConfig)
|
||||
if err != nil {
|
||||
log.Errorf("InitKeyRingInstance: Unable to create instance of Keyring because of [err=%s]", err)
|
||||
return nil, fmt.Errorf("GetKeyRing: Unable to create instance of Keyring because of [err=%s]", err)
|
||||
}
|
||||
|
||||
return keyringInstance, nil
|
||||
}
|
||||
|
||||
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||
|
||||
@@ -9,7 +9,7 @@ title: "Commands"
|
||||
| `login` | Used to authenticate and set the logged in user. |
|
||||
| `init` | Used to link a local project to the platform. |
|
||||
| `run` | Used to inject envars from the platform into an application process. |
|
||||
|
||||
| `vault` | Used to manage where your login credentials are stored at rest |
|
||||
## Global options
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -7,7 +7,5 @@ infisical login
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
Verify a user and save credentials to the system keyring.
|
||||
|
||||
To change the logged in user, run the command again to overwrite the previous login.
|
||||
The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI.
|
||||
If you want to change where the login credentials are stored, visit the [vaults command](./vault)
|
||||
@@ -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` |
|
||||
|
||||
50
docs/cli/commands/vault.mdx
Normal file
50
docs/cli/commands/vault.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "infisical vault"
|
||||
---
|
||||
|
||||
<Tabs>
|
||||
<Tab title="View current Vault">
|
||||
```bash
|
||||
infisical vault
|
||||
|
||||
# Example output
|
||||
The following vaults are available on your system:
|
||||
- keychain
|
||||
- pass
|
||||
- file
|
||||
|
||||
You are currently using [keychain] vault to store your login credentials
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Switch vault">
|
||||
```bash
|
||||
infisical vault set <name-of-vault>
|
||||
|
||||
# Example
|
||||
infisical vault set keychain
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
To ensure secure storage of your login credentials when using the CLI, Infisical stores login credentials securely in a system vault or encrypted text file with a passphrase known only by the user.
|
||||
|
||||
<Accordion title="Supported vaults">
|
||||
By default, the most appropriate vault is chosen to store your login credentials.
|
||||
For example, if you are on macOS, KeyChain will be automatically selected.
|
||||
|
||||
- [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac)
|
||||
- [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager)
|
||||
- Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5))
|
||||
- [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)
|
||||
- [Pass](https://www.passwordstore.org/)
|
||||
- [KeyCtl]()
|
||||
- Encrypted file (JWT)
|
||||
</Accordion>
|
||||
|
||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
|
||||
|
||||
|
||||
15
docs/cli/faq.mdx
Normal file
15
docs/cli/faq.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "FAQ"
|
||||
---
|
||||
|
||||
Frequently asked questions about the CLI can be found on this page.
|
||||
If you can't find the answer you're looking for, please create an issue on our GitHub repository or join our Slack channel for additional support.
|
||||
|
||||
<Accordion title="I'm getting a Keyring related error message when trying to login" defaultOpen="true">
|
||||
By default, the CLI will choose the most suitable store available on your system.
|
||||
If you experience issues with the default store, you can switch to a different one.
|
||||
If none of the available stores work for you, you can try using the `file` store type by running `infisical vault set file`, which should work in most cases.
|
||||
If you are still experiencing trouble, please seek support.
|
||||
|
||||
[Learn more about vault command](./commands/vault)
|
||||
</Accordion>
|
||||
@@ -4,10 +4,27 @@ title: "Usage"
|
||||
|
||||
Prerequisite: [Install the CLI](/cli/overview)
|
||||
|
||||
## Authenticate
|
||||
<Tabs>
|
||||
<Tab title="Local development">
|
||||
To use the Infisical CLI in your development environment, you can run the command below.
|
||||
This will allow you to access the features and functionality provided by the CLI.
|
||||
|
||||
```bash
|
||||
infisical login
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Infisical Token">
|
||||
To use Infisical CLI in environments where you cannot run the `infisical login` command, you can authenticate via a
|
||||
Infisical Token instead. Learn more about [Infisical Token](../getting-started/dashboard/token).
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Initialize Infisical for your project
|
||||
|
||||
```bash
|
||||
# move to your project
|
||||
# navigate to your project
|
||||
cd /path/to/project
|
||||
|
||||
# initialize infisical
|
||||
@@ -21,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:
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ To generate the the token, head over to your project settings as shown below.
|
||||
|
||||

|
||||
|
||||
## Feeding Infisical Token to the CLI
|
||||
|
||||
The Infisical CLI checks for the presence of an environment variable called `INFISICAL_TOKEN`.
|
||||
If it detects this variable in the terminal where it is being run, it will use it to authenticate and retrieve the environment variables that the token is authorized to access.
|
||||
This allows you to use the CLI in environments where you are unable to run the `infisical login` command.
|
||||
|
||||
<Note>
|
||||
The token grants read-only access to a particular environment and project for
|
||||
a specified amount of time. Once the token is expired, the CLI using it will no longer be able to make
|
||||
|
||||
@@ -94,9 +94,11 @@
|
||||
"cli/commands/login",
|
||||
"cli/commands/init",
|
||||
"cli/commands/run",
|
||||
"cli/commands/export"
|
||||
"cli/commands/export",
|
||||
"cli/commands/vault"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cli/faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ Configuring Infisical requires setting some environment variables. There is a fi
|
||||
| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` |
|
||||
| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` |
|
||||
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
|
||||
| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` |
|
||||
| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` |
|
||||
| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` |
|
||||
| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { memo,useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -96,6 +96,7 @@ const InputField = (
|
||||
/>
|
||||
{props.label?.includes('Password') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
}}
|
||||
@@ -139,4 +140,4 @@ const InputField = (
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(InputField);
|
||||
export default memo(InputField);
|
||||
|
||||
81
frontend/components/basic/Toggle.tsx
Normal file
81
frontend/components/basic/Toggle.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
id: string;
|
||||
deleteOverride: (id: string) => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||
* @param obj
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {string} obj.keyName - key of a certain secret
|
||||
* @param {string} obj.value - value of a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
#TODO: make the secret id persistent?
|
||||
* @param {string} obj.id - id of a certain secret
|
||||
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle ({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
keyName,
|
||||
value,
|
||||
pos,
|
||||
id,
|
||||
deleteOverride,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: ToggleProps): JSX.Element {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride({ id, keyName, value, pos });
|
||||
setSharedToHide([
|
||||
...sharedToHide!,
|
||||
id
|
||||
])
|
||||
} else {
|
||||
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
|
||||
deleteOverride(id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
className={`${
|
||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className="sr-only">Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import Image from "next/image";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
@@ -16,8 +16,9 @@ type ButtonProps = {
|
||||
size: string;
|
||||
icon?: IconProp;
|
||||
active?: boolean;
|
||||
iconDisabled?: string;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>['type'];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,15 +73,16 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
props.color == "mineshaft" && "text-gray-400",
|
||||
props.color != "mineshaft" && props.color != "red" && "text-black",
|
||||
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
||||
props.color == "red" && "text-gray-200",
|
||||
activityStatus && props.color != "red" ? "group-hover:text-black" : "",
|
||||
props.color == "none" && "text-gray-200 text-xl",
|
||||
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
||||
|
||||
props.size == "icon" && "flex items-center justify-center"
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"relative duration-200",
|
||||
"relative duration-200 text-center w-full",
|
||||
|
||||
// Show the loading sign if the loading indicator is on
|
||||
props.loading ? "opacity-0" : "opacity-100",
|
||||
@@ -91,6 +93,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
const button = (
|
||||
<button
|
||||
disabled={!activityStatus}
|
||||
type={props.type}
|
||||
onClick={props.onButtonPressed}
|
||||
className={styleButton}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
|
||||
interface PopupProps {
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
titleText: string;
|
||||
emoji: string;
|
||||
textLine1: string;
|
||||
textLine2: string;
|
||||
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||
* @param {object} org
|
||||
@@ -23,16 +33,16 @@ export default function BottonRightPopup({
|
||||
textLine1,
|
||||
textLine2,
|
||||
setCheckDocsPopUpVisible,
|
||||
}) {
|
||||
}: PopupProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||
<div>{titleText}</div>
|
||||
<div class="ml-2.5">{emoji}</div>
|
||||
<div className="ml-2.5">{emoji}</div>
|
||||
</div>
|
||||
<button
|
||||
className="mt-1"
|
||||
@@ -44,14 +54,14 @@ export default function BottonRightPopup({
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
{textLine1}
|
||||
</div>
|
||||
<div class="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="flex flex-row px-6 w-full">
|
||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
href={buttonLink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { SyntheticEvent, useRef } from 'react';
|
||||
import { memo, SyntheticEvent, useRef } from 'react';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -11,19 +11,21 @@ interface DashboardInputFieldProps {
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
value: string;
|
||||
type: 'varName' | 'value';
|
||||
blurred: boolean;
|
||||
duplicates: string[];
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
override?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the input fields on the dashboard
|
||||
* @param {object} obj - the order number of a keyPair
|
||||
* @param {number} obj.pos - the order number of a keyPair
|
||||
* @param {number} obj.position - the order number of a keyPair
|
||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||
* @param {string} obj.value - value of the InputField
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {string[]} obj.duplicates - list of all the duplicated key names on the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@@ -33,7 +35,8 @@ const DashboardInputField = ({
|
||||
type,
|
||||
value,
|
||||
blurred,
|
||||
duplicates
|
||||
isDuplicate,
|
||||
override
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
@@ -45,8 +48,7 @@ const DashboardInputField = ({
|
||||
|
||||
if (type === 'varName') {
|
||||
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
|
||||
const hasDuplicates = duplicates?.includes(value);
|
||||
const error = startsWithNumber || hasDuplicates;
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
@@ -72,7 +74,7 @@ const DashboardInputField = ({
|
||||
Should not start with a number
|
||||
</p>
|
||||
)}
|
||||
{hasDuplicates && !startsWithNumber && (
|
||||
{isDuplicate && !startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Secret names should be unique
|
||||
</p>
|
||||
@@ -85,6 +87,7 @@ const DashboardInputField = ({
|
||||
<div
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
||||
>
|
||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
@@ -99,10 +102,13 @@ const DashboardInputField = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
blurred
|
||||
blurred && !override
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||
: ''
|
||||
} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
} ${
|
||||
override ? 'text-primary-300' : 'text-gray-400'
|
||||
}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
@@ -153,4 +159,8 @@ const DashboardInputField = ({
|
||||
return <>Something Wrong</>;
|
||||
};
|
||||
|
||||
export default React.memo(DashboardInputField);
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
|
||||
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Fragment,useState } from 'react';
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
|
||||
/**
|
||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||
* @returns the popup-menu for randomly generating secrets
|
||||
*/
|
||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
|
||||
return <Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
||||
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
|
||||
export default GenerateSecretMenu;
|
||||
102
frontend/components/dashboard/KeyPair.tsx
Normal file
102
frontend/components/dashboard/KeyPair.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
deleteRow: (id: string) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId
|
||||
}: KeyPairProps) => {
|
||||
return (
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={keyPair.type == "personal"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2"></div>
|
||||
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="none"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(KeyPair);
|
||||
171
frontend/components/dashboard/SideBar.tsx
Normal file
171
frontend/components/dashboard/SideBar.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useState } from 'react';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SecretVersionList from 'ee/components/SecretVersionList';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
deleteOverride: (id: string) => void;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
|
||||
* @param {function} obj.modifyKey - function that modifies the secret key
|
||||
* @param {function} obj.modifyValue - function that modifies the secret value
|
||||
* @param {function} obj.addOverride - override a certain secret
|
||||
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
|
||||
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
|
||||
* @param {function} obj.savePush - save changes andp ush secrets
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns the sidebar with 'secret's settings'
|
||||
*/
|
||||
const SideBar = ({
|
||||
toggleSidebar,
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
addOverride,
|
||||
deleteOverride,
|
||||
buttonReady,
|
||||
savePush,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: SideBarProps) => {
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">Secret</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>Key</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0].pos}
|
||||
value={data[0].key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>Value</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
|
||||
This secret is personal. It is not shared with any of your teammates.
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0].key}
|
||||
value={data[0].value}
|
||||
pos={data[0].pos}
|
||||
id={data[0].id}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].value : data[0].value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
|
||||
<p className='text-sm text-bunker-200'>Group</p>
|
||||
<ListBox
|
||||
selected={"Database Secrets"}
|
||||
onChange={() => {}}
|
||||
data={["Group1"]}
|
||||
isFull={true}
|
||||
/>
|
||||
</div> */}
|
||||
<div className={`relative mt-4 px-4 pt-4`}>
|
||||
<div className='flex flex-row justify-between'>
|
||||
<p className='text-sm text-bunker-300'>Comments & notes</p>
|
||||
<div className="bg-yellow rounded-md h-min">
|
||||
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
|
||||
Leave your comment here...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faAngleRight,
|
||||
|
||||
@@ -39,7 +39,7 @@ const getSecretsForProject = async ({
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempFileState: { key: string; value: string; type: string }[] = [];
|
||||
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
|
||||
if (file.key) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
@@ -97,7 +97,7 @@ const getSecretsForProject = async ({
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
}
|
||||
return true;
|
||||
return [];
|
||||
};
|
||||
|
||||
export default getSecretsForProject;
|
||||
|
||||
@@ -51,7 +51,7 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
iv: ivKey,
|
||||
tag: tagKey,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
plaintext: key.slice(1),
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
@@ -65,13 +65,13 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = obj[key as keyof typeof obj][1] != null ? obj[key as keyof typeof obj][1] : "personal";
|
||||
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||
|
||||
return {
|
||||
ciphertextKey,
|
||||
ivKey,
|
||||
tagKey,
|
||||
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
|
||||
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
|
||||
44
frontend/ee/components/SecretVersionList.tsx
Normal file
44
frontend/ee/components/SecretVersionList.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface SecretVersionListProps {}
|
||||
|
||||
const versionData = [{
|
||||
value: "Value1",
|
||||
date: "Date1",
|
||||
user: "vlad@infisical.com"
|
||||
}, {
|
||||
value: "Value2",
|
||||
date: "Date2",
|
||||
user: "tony@infisical.com"
|
||||
}]
|
||||
|
||||
/**
|
||||
* @returns a list of the versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = () => {
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>Version History</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||
<div className='h-48 overflow-y-scroll overflow-x-none'>
|
||||
{versionData.map((version, index) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>{version.date}</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -6,208 +6,68 @@ import {
|
||||
faArrowDownAZ,
|
||||
faArrowDownZA,
|
||||
faCheck,
|
||||
faCircleInfo,
|
||||
faCopy,
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFolderOpen,
|
||||
faMagnifyingGlass,
|
||||
faPeopleGroup,
|
||||
faPerson,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
import DashboardInputField from '~/components/dashboard/DashboardInputField';
|
||||
import DropZone from '~/components/dashboard/DropZone';
|
||||
import KeyPair from '~/components/dashboard/KeyPair';
|
||||
import SideBar from '~/components/dashboard/SideBar';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
||||
import pushKeys from '~/components/utilities/secrets/pushKeys';
|
||||
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import { envMapping } from '../../public/data/frequentConstants';
|
||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyVisibility - switch between public/private visibility
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {string[]} obj.duplicates - list of all the duplicates secret names on the dashboard
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyVisibility,
|
||||
isBlurred,
|
||||
duplicates
|
||||
}) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
|
||||
return (
|
||||
<div className="px-1 flex flex-col items-center ml-1">
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-2">
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex items-center md:px-1 rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
duplicates={duplicates}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.value}
|
||||
blurred={isBlurred}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="cursor-pointer w-9 h-9 bg-white/10 rounded-md flex flex-row justify-center items-center opacity-50 hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
onClick={() =>
|
||||
modifyVisibility(
|
||||
keyPair.type == 'personal' ? 'shared' : 'personal',
|
||||
keyPair.pos
|
||||
)
|
||||
}
|
||||
className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={keyPair.type == 'personal' ? faPeopleGroup : faPerson}
|
||||
/>
|
||||
<div className="text-sm">
|
||||
{keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
keyPair.pos
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={keyPair.value == '' ? faPlus : faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<div className="w-2"></div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* this function finds the teh duplicates in an array
|
||||
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
|
||||
* @returns - a list with duplicates
|
||||
*/
|
||||
function findDuplicates(arr: any[]) {
|
||||
const map = new Map();
|
||||
return arr.filter((item) => {
|
||||
if (map.has(item)) {
|
||||
map.set(item, false);
|
||||
return true;
|
||||
} else {
|
||||
map.set(item, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets)
|
||||
* @returns
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
const [data, setData] = useState();
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const [data, setData] = useState<SecretDataProps[] | null>();
|
||||
const [fileState, setFileState] = useState<SecretDataProps[]>([]);
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useState('');
|
||||
@@ -227,6 +87,8 @@ export default function Dashboard() {
|
||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
@@ -249,7 +111,7 @@ export default function Dashboard() {
|
||||
useEffect(() => {
|
||||
const warningText =
|
||||
'Do you want to save your results before leaving this page?';
|
||||
const handleWindowClose = (e) => {
|
||||
const handleWindowClose = (e: any) => {
|
||||
if (!buttonReady) return;
|
||||
e.preventDefault();
|
||||
return (e.returnValue = warningText);
|
||||
@@ -265,18 +127,18 @@ export default function Dashboard() {
|
||||
/**
|
||||
* Reorder rows alphabetically or in the opprosite order
|
||||
*/
|
||||
const reorderRows = (dataToReorder) => {
|
||||
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
|
||||
setSortMethod((prevSort) =>
|
||||
prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical'
|
||||
);
|
||||
|
||||
sortValuesHandler(dataToReorder);
|
||||
sortValuesHandler(dataToReorder, undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
let userWorkspaces = await getWorkspaces();
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
||||
if (
|
||||
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
|
||||
@@ -288,31 +150,41 @@ export default function Dashboard() {
|
||||
router.push(router.asPath.split('?')[0] + '?' + env);
|
||||
}
|
||||
setBlurred(true);
|
||||
setWorkspaceId(router.query.id);
|
||||
setWorkspaceId(String(router.query.id));
|
||||
|
||||
const dataToSort = await getSecretsForProject({
|
||||
env,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: router.query.id
|
||||
workspaceId: String(router.query.id)
|
||||
});
|
||||
reorderRows(dataToSort);
|
||||
|
||||
setSharedToHide(
|
||||
dataToSort?.filter(row => (dataToSort
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
dataToSort?.map((item) => item.key).indexOf(item)
|
||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
||||
)
|
||||
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(new Date()) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
let userAction = await checkUserAction({
|
||||
const userAction = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
setHasUserEverPushed(userAction ? true : false);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
setData([]);
|
||||
setData(undefined);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -321,10 +193,10 @@ export default function Dashboard() {
|
||||
const addRow = () => {
|
||||
setIsNew(false);
|
||||
setData([
|
||||
...data,
|
||||
...data!,
|
||||
{
|
||||
id: guidGenerator(),
|
||||
pos: data.length,
|
||||
pos: data!.length,
|
||||
key: '',
|
||||
value: '',
|
||||
type: 'shared'
|
||||
@@ -332,45 +204,85 @@ export default function Dashboard() {
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteRow = (id) => {
|
||||
setButtonReady(true);
|
||||
setData(data.filter((row) => row.id !== id));
|
||||
interface overrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function add an ovverrided version of a certain secret to the current user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.id - if of this secret that is about to be overriden
|
||||
* @param {string} obj.keyName - key name of this secret
|
||||
* @param {string} obj.value - value of this secret
|
||||
* @param {string} obj.pos - position of this secret on the dashboard
|
||||
*/
|
||||
const addOverride = ({ id, keyName, value, pos }: overrideProps) => {
|
||||
setIsNew(false);
|
||||
const tempdata: SecretDataProps[] | 1 = [
|
||||
...data!,
|
||||
{
|
||||
id: id,
|
||||
pos: pos,
|
||||
key: keyName,
|
||||
value: value,
|
||||
type: 'personal'
|
||||
}
|
||||
];
|
||||
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
||||
};
|
||||
|
||||
const modifyValue = (value, pos) => {
|
||||
const deleteRow = (id: string) => {
|
||||
setButtonReady(true);
|
||||
setData(data!.filter((row: SecretDataProps) => row.id !== id));
|
||||
};
|
||||
|
||||
/**
|
||||
* This function deleted the override of a certain secrer
|
||||
* @param {string} id - id of a secret to be deleted
|
||||
*/
|
||||
const deleteOverride = (id: string) => {
|
||||
setButtonReady(true);
|
||||
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
|
||||
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
|
||||
};
|
||||
|
||||
const modifyValue = (value: string, pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].value = value;
|
||||
return [...oldData];
|
||||
oldData![pos].value = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyKey = (value, pos) => {
|
||||
const modifyKey = (value: string, pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].key = value;
|
||||
return [...oldData];
|
||||
oldData![pos].key = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
const modifyVisibility = (value, pos) => {
|
||||
const modifyVisibility = (value: "shared" | "personal", pos: number) => {
|
||||
setData((oldData) => {
|
||||
oldData[pos].type = value;
|
||||
return [...oldData];
|
||||
oldData![pos].type = value;
|
||||
return [...oldData!];
|
||||
});
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
// For speed purposes and better perforamance, we are using useCallback
|
||||
const listenChangeValue = useCallback((value, pos) => {
|
||||
const listenChangeValue = useCallback((value: string, pos: number) => {
|
||||
modifyValue(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeKey = useCallback((value, pos) => {
|
||||
const listenChangeKey = useCallback((value: string, pos: number) => {
|
||||
modifyKey(value, pos);
|
||||
}, []);
|
||||
|
||||
const listenChangeVisibility = useCallback((value, pos) => {
|
||||
const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
|
||||
modifyVisibility(value, pos);
|
||||
}, []);
|
||||
|
||||
@@ -379,21 +291,16 @@ export default function Dashboard() {
|
||||
*/
|
||||
const savePush = async () => {
|
||||
// Format the new object with environment variables
|
||||
let obj = Object.assign(
|
||||
const obj = Object.assign(
|
||||
{},
|
||||
...data.map((row) => ({ [row.key]: [row.value, row.type] }))
|
||||
...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.type] }))
|
||||
);
|
||||
|
||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||
const nameErrors = !Object.keys(obj)
|
||||
.map((key) => !isNaN(key.charAt(0)))
|
||||
.map((key) => !isNaN(Number(key[0].charAt(0))))
|
||||
.every((v) => v === false);
|
||||
const duplicatesExist =
|
||||
data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) => index !== data?.map((item) => item.key).indexOf(item)
|
||||
).length > 0;
|
||||
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
|
||||
|
||||
if (nameErrors) {
|
||||
return createNotification({
|
||||
@@ -409,9 +316,11 @@ export default function Dashboard() {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('pushing', obj)
|
||||
|
||||
// Once "Save changed is clicked", disable that button
|
||||
setButtonReady(false);
|
||||
pushKeys({ obj, workspaceId: router.query.id, env });
|
||||
pushKeys({ obj, workspaceId: String(router.query.id), env });
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
@@ -420,8 +329,8 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const addData = (newData) => {
|
||||
setData(data.concat(newData));
|
||||
const addData = (newData: SecretDataProps[]) => {
|
||||
setData(data!.concat(newData));
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
@@ -429,37 +338,39 @@ export default function Dashboard() {
|
||||
setBlurred(!blurred);
|
||||
};
|
||||
|
||||
const sortValuesHandler = (dataToSort) => {
|
||||
const sortedData = (dataToSort != 1 ? dataToSort : data)
|
||||
.sort((a, b) =>
|
||||
sortMethod == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index
|
||||
};
|
||||
});
|
||||
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
||||
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||
.sort((a, b) =>
|
||||
howToSort == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item: SecretDataProps, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index
|
||||
};
|
||||
});
|
||||
console.log('override', sortedData)
|
||||
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
// This function downloads the secrets as a .env file
|
||||
const download = () => {
|
||||
const file = data
|
||||
.map((item) => [item.key, item.value].join('='))
|
||||
const file = data!
|
||||
.map((item: SecretDataProps) => [item.key, item.value].join('='))
|
||||
.join('\n');
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
let alink = document.createElement('a');
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.env';
|
||||
alink.click();
|
||||
};
|
||||
|
||||
const deleteCertainRow = (id) => {
|
||||
const deleteCertainRow = (id: string) => {
|
||||
deleteRow(id);
|
||||
};
|
||||
|
||||
@@ -467,15 +378,17 @@ export default function Dashboard() {
|
||||
* This function copies the project id to the clipboard
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
var copyText = document.getElementById('myInput');
|
||||
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||
|
||||
if (copyText) {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return data ? (
|
||||
@@ -491,6 +404,18 @@ export default function Dashboard() {
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
{sidebarSecretId != "None" && <SideBar
|
||||
toggleSidebar={toggleSidebar}
|
||||
data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyValue={listenChangeValue}
|
||||
addOverride={addOverride}
|
||||
deleteOverride={deleteOverride}
|
||||
buttonReady={buttonReady}
|
||||
savePush={savePush}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>}
|
||||
<div className="w-full max-h-96 pb-2">
|
||||
<NavHeader pageName="Secrets" isProjectRelated={true} />
|
||||
{checkDocsPopUpVisible && (
|
||||
@@ -513,7 +438,6 @@ export default function Dashboard() {
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -568,7 +492,6 @@ export default function Dashboard() {
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
// ref={useRef(123)}
|
||||
onChange={setEnv}
|
||||
className="z-40"
|
||||
/>
|
||||
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
@@ -630,144 +553,55 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
{data?.length !== 0 ? (
|
||||
<div
|
||||
id="dataall"
|
||||
className="flex flex-col max-h-40 grow max-h-[calc(100vh-240px)] w-full overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
|
||||
>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-1 rounded-md pb-2 max-w-5xl overflow-visible`}
|
||||
>
|
||||
<div className="rounded-t-md sticky top-0 z-20 bg-bunker flex flex-row pl-4 pr-6 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Personal</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 mt-1 text-lg"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Personal keys are only visible to you
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data1" className="">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair.key
|
||||
.toLowerCase()
|
||||
.includes(searchKeys.toLowerCase()) &&
|
||||
keyPair.type == 'personal'
|
||||
)
|
||||
?.map((keyPair) => (
|
||||
<KeyPair
|
||||
<div className="flex flex-col w-full mt-1 mb-2">
|
||||
<div className='bg-mineshaft-800 rounded-md px-2 py-2 max-w-5xl'>
|
||||
<div
|
||||
className={`mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
<div className="px-1 pt-2">
|
||||
{data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
duplicates={data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
data?.map((item) => item.key).indexOf(item)
|
||||
)}
|
||||
isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
|
||||
toggleSidebar={toggleSidebar}
|
||||
sidebarSecretId={sidebarSecretId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`bg-white/5 mt-1 mb-2 rounded-md p-1 pb-2 max-w-5xl ${
|
||||
data?.length > 8 ? 'h-3/4' : 'h-min'
|
||||
}`}
|
||||
>
|
||||
<div className="sticky top-0 z-40 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
|
||||
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
|
||||
<div className="flex flex-row items-center">
|
||||
<p className="pl-2 text-lg">Shared</p>
|
||||
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="ml-3 text-lg mt-1"
|
||||
icon={faCircleInfo}
|
||||
/>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
|
||||
Shared keys are visible to your whole team
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-5xl px-2 pt-2">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="data2" className="data2">
|
||||
{data
|
||||
.filter(
|
||||
(keyPair) =>
|
||||
keyPair.key
|
||||
.toLowerCase()
|
||||
.includes(searchKeys.toLowerCase()) &&
|
||||
keyPair.type == 'shared'
|
||||
)
|
||||
?.map((keyPair) => (
|
||||
<KeyPair
|
||||
key={keyPair.id}
|
||||
keyPair={keyPair}
|
||||
deleteRow={deleteCertainRow}
|
||||
modifyValue={listenChangeValue}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyVisibility={listenChangeVisibility}
|
||||
isBlurred={blurred}
|
||||
duplicates={data
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
data?.map((item) => item.key).indexOf(item)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-5xl">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
||||
{fileState.message != "There's nothing to pull" &&
|
||||
fileState.message != undefined && (
|
||||
<FontAwesomeIcon
|
||||
className="text-7xl mb-8"
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
)}
|
||||
{(fileState.message == "There's nothing to pull" ||
|
||||
fileState.message == undefined) &&
|
||||
isKeyAvailable && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
)}
|
||||
{fileState.message ==
|
||||
'Failed membership validation for workspace' && (
|
||||
<p>You are not authorized to view this project.</p>
|
||||
{isKeyAvailable && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
numCurrentRows={data.length}
|
||||
keysExist={false}
|
||||
/>
|
||||
)}
|
||||
{fileState.message == 'Access needed to pull the latest file' ||
|
||||
{
|
||||
// fileState.message == 'Access needed to pull the latest file' ||
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Home() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@@ -38,6 +38,10 @@ export default function Login() {
|
||||
* This function check if the user entered the correct credentials and should be allowed to log in.
|
||||
*/
|
||||
const loginCheck = async () => {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
await attemptLogin(
|
||||
email,
|
||||
@@ -45,7 +49,7 @@ export default function Login() {
|
||||
setErrorLogin,
|
||||
router,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
).then(() => {
|
||||
setTimeout(function () {
|
||||
setIsLoading(false);
|
||||
@@ -75,68 +79,73 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
Log in to your account
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">Forgot password?</Link>
|
||||
</div>
|
||||
</div>
|
||||
{errorLogin && <Error text="Your email and/or password are wrong." />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
text="Log In"
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
<form
|
||||
onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
Log in to your account
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">Forgot password?</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && errorLogin && <Error text="Your email and/or password are wrong." />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Log In"
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
|
||||
</div> */}
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
We are experiencing minor technical difficulties. We are working on
|
||||
solving it right now. Please come back in a few minutes.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
Need an Infisical account?
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
|
||||
Sign up here.
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
We are experiencing minor technical difficulties. We are working on
|
||||
solving it right now. Please come back in a few minutes.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
Need an Infisical account?
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
|
||||
Sign up here.
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
|
||||
import Plan from "~/components/billing/Plan";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
@@ -260,46 +260,49 @@ export default function SignUp() {
|
||||
|
||||
// Step 1 of the sign up process (enter the email or choose google authentication)
|
||||
const step1 = (
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 mb-48 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
{'Let\''}s get started
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-h-24 max-w-md mx-auto pt-2">
|
||||
<div>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{'Let\''}s get started
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={emailError}
|
||||
errorText={emailErrorMessage}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
|
||||
<Checkbox className="mr-4"/>
|
||||
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
|
||||
</div> */}
|
||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">
|
||||
By creating an account, you agree to our Terms and have read and
|
||||
acknowledged the Privacy Policy.
|
||||
</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
|
||||
<Link href="/login">
|
||||
<button className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-md text-sky-500">
|
||||
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-sm text-primary-500">
|
||||
Have an account? Log in
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={emailError}
|
||||
errorText={emailErrorMessage}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
|
||||
<Checkbox className="mr-4"/>
|
||||
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
|
||||
</div> */}
|
||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">
|
||||
By creating an account, you agree to our Terms and have read and
|
||||
acknowledged the Privacy Policy.
|
||||
</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<Button loading={isLoading} text="Get Started" onButtonPressed={emailCheck} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
// Step 2 of the signup process (enter the email verification code)
|
||||
@@ -340,11 +343,11 @@ export default function SignUp() {
|
||||
<Button text="Verify" onButtonPressed={incrementStep} size="lg" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-gray-400">
|
||||
Not seeing an email?
|
||||
</span>
|
||||
<u className={`font-normal text-sm ${isResendingVerificationEmail ? 'text-gray-400' : 'text-sky-500 hover:opacity-90 duration-200'}`}>
|
||||
<u className={`font-normal ${isResendingVerificationEmail ? 'text-gray-400' : 'text-primary-500 hover:opacity-90 duration-200'}`}>
|
||||
<button disabled={isLoading} onClick={resendVerificationEmail}>
|
||||
{isResendingVerificationEmail ? 'Resending...' : 'Resend'}
|
||||
</button>
|
||||
@@ -512,7 +515,7 @@ export default function SignUp() {
|
||||
It contains your Secret Key which we cannot access or recover for you if
|
||||
you lose it.
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-max mx-auto mt-6 py-1 md:mt-4 text-lg text-center md:text-left">
|
||||
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
|
||||
<Button
|
||||
text="Download PDF"
|
||||
onButtonPressed={async () => {
|
||||
@@ -521,11 +524,9 @@ export default function SignUp() {
|
||||
password,
|
||||
personalName: firstName + ' ' + lastName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push('/home/' + userWorkspace);
|
||||
router.push('/dashboard/');
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
@@ -571,7 +572,9 @@ export default function SignUp() {
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
{step == 1 ? step1 : step == 2 ? step2 : step == 3 ? step3 : step4}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
Reference in New Issue
Block a user