Fix merge conflicts for index

This commit is contained in:
Tuan Dang
2022-12-15 15:31:14 -05:00
35 changed files with 1689 additions and 214 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
built
healthcheck.js

View File

@ -0,0 +1,30 @@
version: '3'
services:
backend:
container_name: infisical-backend-test
restart: unless-stopped
depends_on:
- mongo
image: infisical/backend:test
command: npm run start
environment:
- NODE_ENV=production
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
- MONGO_USERNAME=test
- MONGO_PASSWORD=example
networks:
- infisical-test
mongo:
container_name: infisical-mongo-test
image: mongo
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=test
- MONGO_INITDB_ROOT_PASSWORD=example
networks:
- infisical-test
networks:
infisical-test:

26
.github/resources/healthcheck.sh vendored Executable file
View File

@ -0,0 +1,26 @@
# Name of the target container to check
container_name="$1"
# Timeout in seconds. Default: 60
timeout=$((${2:-60}));
if [ -z $container_name ]; then
echo "No container name specified";
exit 1;
fi
echo "Container: $container_name";
echo "Timeout: $timeout sec";
try=0;
is_healthy="false";
while [ $is_healthy != "true" ];
do
try=$(($try + 1));
printf "■";
is_healthy=$(docker inspect --format='{{json .State.Health}}' $container_name | jq '.Status == "healthy"');
sleep 1;
if [[ $try -eq $timeout ]]; then
echo " Container was not ready within timeout";
exit 1;
fi
done

View File

@ -3,40 +3,38 @@ name: Push to Docker Hub
on: [workflow_dispatch]
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
-
name: ☁️ Checkout source
- name: ☁️ Checkout source
uses: actions/checkout@v3
-
name: 🔧 Set up QEMU
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: 🔧 Set up Docker Buildx
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: 🐋 Login to Docker Hub
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# -
# name: 📦 Build backend and export to Docker
# uses: docker/build-push-action@v3
# with:
# load: true
# context: backend
# tags: infisical/backend:test
# -
# name: 🧪 Test backend image
# run: |
# docker run --rm infisical/backend:test
-
name: 🏗️ Build backend and push
- name: 📦 Build backend and export to Docker
uses: docker/build-push-action@v3
with:
load: true
context: backend
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: docker/build-push-action@v3
with:
push: true
@ -44,42 +42,40 @@ jobs:
tags: infisical/backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
-
name: ☁️ Checkout source
- name: ☁️ Checkout source
uses: actions/checkout@v3
-
name: 🔧 Set up QEMU
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: 🔧 Set up Docker Buildx
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: 🐋 Login to Docker Hub
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# -
# name: 📦 Build frontend and export to Docker
# uses: docker/build-push-action@v3
# with:
# load: true
# context: frontend
# tags: infisical/frontend:test
# build-args: |
# POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
# -
# name: 🧪 Test frontend image
# run: |
# docker run --rm infisical/frontend:test
-
name: 🏗️ Build frontend and push
- name: 📦 Build frontend and export to Docker
uses: docker/build-push-action@v3
with:
load: true
context: frontend
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: docker/build-push-action@v3
with:
push: true

View File

@ -2,11 +2,14 @@ FROM node:16-bullseye-slim
WORKDIR /app
COPY package*.json .
COPY package.json package-lock.json ./
RUN npm install
RUN npm ci --only-production
COPY . .
CMD ["npm", "run", "start"]
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
CMD ["npm", "run", "start"]

24
backend/healthcheck.js Normal file
View File

@ -0,0 +1,24 @@
const http = require('http');
const PORT = process.env.PORT || 4000;
const options = {
host: 'localhost',
port: PORT,
timeout: 2000,
path: '/healthcheck'
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error(`HEALTH CHECK ERROR: ${err}`);
process.exit(1);
});
healthCheck.end();

View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
@ -2028,6 +2029,14 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@godaddy/terminus": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz",
"integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==",
"dependencies": {
"stoppable": "^1.1.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@ -6731,7 +6740,129 @@
"treeverse",
"validate-npm-package-name",
"which",
"write-file-atomic"
"write-file-atomic",
"@colors/colors",
"@gar/promisify",
"@npmcli/disparity-colors",
"@npmcli/git",
"@npmcli/installed-package-contents",
"@npmcli/metavuln-calculator",
"@npmcli/move-file",
"@npmcli/name-from-folder",
"@npmcli/node-gyp",
"@npmcli/promise-spawn",
"@npmcli/query",
"@tootallnate/once",
"agent-base",
"agentkeepalive",
"aggregate-error",
"ansi-regex",
"ansi-styles",
"aproba",
"are-we-there-yet",
"asap",
"balanced-match",
"bin-links",
"binary-extensions",
"brace-expansion",
"builtins",
"cidr-regex",
"clean-stack",
"clone",
"cmd-shim",
"color-convert",
"color-name",
"color-support",
"common-ancestor-path",
"concat-map",
"console-control-strings",
"cssesc",
"debug",
"debuglog",
"defaults",
"delegates",
"depd",
"dezalgo",
"diff",
"emoji-regex",
"encoding",
"env-paths",
"err-code",
"fs.realpath",
"function-bind",
"gauge",
"has",
"has-flag",
"has-unicode",
"http-cache-semantics",
"http-proxy-agent",
"https-proxy-agent",
"humanize-ms",
"iconv-lite",
"ignore-walk",
"imurmurhash",
"indent-string",
"infer-owner",
"inflight",
"inherits",
"ip",
"ip-regex",
"is-core-module",
"is-fullwidth-code-point",
"is-lambda",
"isexe",
"json-stringify-nice",
"jsonparse",
"just-diff",
"just-diff-apply",
"lru-cache",
"minipass-collect",
"minipass-fetch",
"minipass-flush",
"minipass-json-stream",
"minipass-sized",
"minizlib",
"mute-stream",
"negotiator",
"normalize-package-data",
"npm-bundled",
"npm-normalize-package-bin",
"npm-packlist",
"once",
"path-is-absolute",
"postcss-selector-parser",
"promise-all-reject-late",
"promise-call-limit",
"promise-inflight",
"promise-retry",
"promzard",
"read-cmd-shim",
"readable-stream",
"retry",
"safe-buffer",
"safer-buffer",
"set-blocking",
"signal-exit",
"smart-buffer",
"socks",
"socks-proxy-agent",
"spdx-correct",
"spdx-exceptions",
"spdx-expression-parse",
"spdx-license-ids",
"string_decoder",
"string-width",
"strip-ansi",
"supports-color",
"unique-filename",
"unique-slug",
"util-deprecate",
"validate-npm-package-license",
"walk-up-path",
"wcwidth",
"wide-align",
"wrappy",
"yallist"
],
"dev": true,
"dependencies": {
@ -10192,6 +10323,15 @@
"node": ">= 0.8"
}
},
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
"engines": {
"node": ">=4",
"npm": ">=6"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@ -12607,6 +12747,14 @@
"strip-json-comments": "^3.1.1"
}
},
"@godaddy/terminus": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@godaddy/terminus/-/terminus-4.11.2.tgz",
"integrity": "sha512-e/kbOWpGKME42eltM/wXM3RxSUOrfureZxEd6Dt6NXyFoJ7E8lnmm7znXydJsL3B7ky4HRFZI+eHrep54NZbeQ==",
"requires": {
"stoppable": "^1.1.0"
}
},
"@humanwhocodes/config-array": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
@ -18631,6 +18779,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",

View File

@ -103,7 +103,7 @@ export const login2 = async (req: Request, res: Response) => {
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/token',
path: '/',
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});
@ -147,7 +147,7 @@ export const logout = async (req: Request, res: Response) => {
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/token',
path: '/',
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});

View File

@ -2,34 +2,35 @@ import rateLimit from 'express-rate-limit';
// 300 requests per 15 minutes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 400,
standardHeaders: true,
legacyHeaders: false
windowMs: 15 * 60 * 1000,
max: 400,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => request.path === '/healthcheck'
});
// 5 requests per hour
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
});
// 10 requests per hour
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 20,
standardHeaders: true,
legacyHeaders: false
windowMs: 60 * 60 * 1000,
max: 20,
standardHeaders: true,
legacyHeaders: false
});
// 5 requests per hour
const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
});
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };

View File

@ -1,3 +1,5 @@
/* eslint-disable no-console */
import http from 'http';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
@ -7,16 +9,17 @@ import dotenv from 'dotenv';
dotenv.config();
import * as Sentry from '@sentry/node';
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST, TELEMETRY_ENABLED } from './config';
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import { createTerminus } from '@godaddy/terminus';
const app = express();
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
import {
@ -40,31 +43,35 @@ import {
} from './routes';
const connectWithRetry = () => {
mongoose.connect(MONGO_URL)
.then(() => console.log('Successfully connected to DB'))
.catch((e) => {
console.log('Failed to connect to DB ', e);
setTimeout(() => {
console.log(e);
}, 5000);
});
}
mongoose
.connect(MONGO_URL)
.then(() => console.log('Successfully connected to DB'))
.catch((e) => {
console.log('Failed to connect to DB ', e);
setTimeout(() => {
console.log(e);
}, 5000);
});
return mongoose.connection;
};
connectWithRetry();
const dbConnection = connectWithRetry();
app.enable('trust proxy');
app.use(cookieParser());
app.use(cors({
credentials: true,
origin: SITE_URL
}));
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
app.use(express.json());
@ -88,6 +95,35 @@ app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
app.listen(PORT, () => {
console.log('Listening on PORT ' + PORT);
const server = http.createServer(app);
const onSignal = () => {
console.log('Server is starting clean-up');
return Promise.all([
() => {
dbConnection.close(() => {
console.info('Database connection closed');
});
}
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
return Promise
.resolve
// optionally include a resolve value to be included as
// info in the health check response
();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
server.listen(PORT, () => {
console.log('Listening on PORT ' + PORT);
});

View File

@ -2,25 +2,30 @@ import { Schema, model } from 'mongoose';
import { EMAIL_TOKEN_LIFETIME } from '../config';
export interface IToken {
email: String;
token: String;
createdAt: Date;
email: string;
token: string;
createdAt: Date;
}
const tokenSchema = new Schema<IToken>({
email: {
type: String,
required: true
},
token: {
type: String,
required: true
},
createdAt: {
type: Date,
expires: parseInt(EMAIL_TOKEN_LIFETIME),
default: Date.now
}
email: {
type: String,
required: true
},
token: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
tokenSchema.index({
createdAt: 1
}, {
expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME)
});
const Token = model<IToken>('Token', tokenSchema);

View File

@ -1,15 +1,19 @@
import { PostHog } from 'posthog-node';
import { NODE_ENV, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, TELEMETRY_ENABLED } from '../config';
import {
NODE_ENV,
POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY,
TELEMETRY_ENABLED
} from '../config';
console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED);
let postHogClient: any;
if (
NODE_ENV === 'production'
&& TELEMETRY_ENABLED
) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
host: POSTHOG_HOST
});
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
// case: enable opt-out telemetry in production
postHogClient = new PostHog(POSTHOG_PROJECT_API_KEY, {
host: POSTHOG_HOST
});
}
export default postHogClient;
export default postHogClient;

View File

@ -19,6 +19,7 @@ const (
FormatDotenv string = "dotenv"
FormatJson string = "json"
FormatCSV string = "csv"
FormatYaml string = "yaml"
)
// exportCmd represents the export command
@ -101,8 +102,10 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
return formatAsJson(envs), nil
case FormatCSV:
return formatAsCSV(envs), nil
case FormatYaml:
return formatAsYaml(envs), nil
default:
return "", fmt.Errorf("invalid format flag: %s", format)
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml})
}
}
@ -127,6 +130,14 @@ func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string {
return dotenv
}
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
var dotenv string
for _, env := range envs {
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value)
}
return dotenv
}
// Format environment variables as a JSON file
func formatAsJson(envs []models.SingleEnvironmentVariable) string {
// Dump as a json array

View File

@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process",
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
Version: "0.1.10",
Version: "0.1.11",
}
// Execute adds all child commands to the root command and sets flags appropriately.

View File

@ -15,7 +15,7 @@ services:
- backend
networks:
- infisical
backend:
container_name: infisical-backend
restart: unless-stopped
@ -28,7 +28,7 @@ services:
- NODE_ENV=production
networks:
- infisical
frontend:
container_name: infisical-frontend
restart: unless-stopped

View File

@ -60,5 +60,8 @@ EXPOSE 3000
ENV PORT 3000
ENV NEXT_TELEMETRY_DISABLED 1
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node scripts/healthcheck.js
CMD ["/app/scripts/start.sh"]

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -26,7 +25,6 @@ const InputField = (
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
) => {
const [passwordVisible, setPasswordVisible] = useState(false);
const router = useRouter();
if (props.static === true) {
return (

View File

@ -41,7 +41,7 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) {
const router = useRouter();
const [workspaceList, setWorkspaceList] = useState([]);
const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]);
const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]);
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
const [newWorkspaceName, setNewWorkspaceName] = useState('');
const [isOpen, setIsOpen] = useState(false);
@ -221,20 +221,20 @@ export default function Layout({ children }: LayoutProps) {
useEffect(() => {
try {
if (
workspaceMapping[Number(workspaceSelected)] &&
`${workspaceMapping[Number(workspaceSelected)]}` !==
workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !==
router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0]
) {
router.push(
'/dashboard/' +
workspaceMapping[Number(workspaceSelected)] +
workspaceMapping[workspaceSelected as any] +
'?Development'
);
localStorage.setItem(
'projectData.id',
`${workspaceMapping[Number(workspaceSelected)]}`
`${workspaceMapping[workspaceSelected as any]}`
);
}
} catch (error) {

View File

@ -63,13 +63,15 @@ const DropZone = ({
if (event.target === null || event.target.result === null) return;
// parse function's argument looks like to be ArrayBuffer
const keyPairs = parse(event.target.result as Buffer);
const newData = Object.keys(keyPairs).map((key, index) => [
guidGenerator(),
numCurrentRows + index,
key,
keyPairs[key as keyof typeof keyPairs],
'shared'
]);
const newData = Object.keys(keyPairs).map((key, index) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs],
type: 'shared'
};
});
setData(newData);
setButtonReady(true);
};
@ -97,13 +99,15 @@ const DropZone = ({
if (typeof result === 'string') {
const newData = result
.split('\n')
.map((line: string, index: number) => [
guidGenerator(),
numCurrentRows + index,
line.split('=')[0],
line.split('=').slice(1, line.split('=').length).join('='),
'shared'
]);
.map((line: string, index: number) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: line.split('=')[0],
value: line.split('=').slice(1, line.split('=').length).join('='),
type: 'shared'
};
});
setData(newData);
setButtonReady(true);
}

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.31.2",
"styled-components": "^5.3.5",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",

View File

@ -0,0 +1,34 @@
interface Props {
email: string;
code: string;
}
/**
* This is the second part of the account recovery step (a user needs to verify their email).
* A user need to click on a button in a magic link page
* @param {object} obj
* @param {object} obj.email - email of a user that is trying to recover access to their account
* @param {object} obj.code - token that a use received via the magic link
* @returns
*/
const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
const response = await fetch('/api/v1/password/email/password-reset-verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
code: code
})
});
if (response?.status === 200) {
return response;
}
throw new Error(
'Something went wrong during email verification on password reset.'
);
};
export default EmailVerifyOnPasswordReset;

View File

@ -0,0 +1,33 @@
interface Props {
email: string;
}
/**
* This is the first of the account recovery step (a user needs to verify their email).
* It will send an email containing a magic link to start the account recovery flow.
* @param {object} obj
* @param {object} obj.email - email of a user that is trying to recover access to their account
* @returns
*/
const SendEmailOnPasswordReset = async ({ email }: Props) => {
const response = await fetch('/api/v1/password/email/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email
})
});
// need precise error handling about the status code
if (response?.status === 200) {
const data = await response.json();
return data;
}
throw new Error(
'Something went wrong while sending the email verification for password reset.'
);
};
export default SendEmailOnPasswordReset;

View File

@ -0,0 +1,26 @@
/**
* This is the route that get an encrypted private key (will be decrypted with a backup key)
* @param {object} obj
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
* @returns
*/
const getBackupEncryptedPrivateKey = ({
verificationToken
}: {
verificationToken: string;
}) => {
return fetch('/api/v1/password/backup-private-key', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + verificationToken
}
}).then(async (res) => {
if (res?.status !== 200) {
console.log('Failed to get the backup key');
}
return (await res?.json())?.backupPrivateKey;
});
};
export default getBackupEncryptedPrivateKey;

View File

@ -0,0 +1,50 @@
interface Props {
verificationToken: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
salt: string;
verifier: string;
}
/**
* This is the route that resets the account password if all the previus steps were passed
* @param {object} obj
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
* @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password)
* @param {object} obj.iv
* @param {object} obj.tag
* @param {object} obj.salt
* @param {object} obj.verifier
* @returns
*/
const resetPasswordOnAccountRecovery = ({
verificationToken,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
}: Props) => {
return fetch('/api/v1/password/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + verificationToken
},
body: JSON.stringify({
encryptedPrivateKey: encryptedPrivateKey,
iv: iv,
tag: tag,
salt: salt,
verifier: verifier
})
}).then(async (res) => {
if (res?.status !== 200) {
console.log('Failed to get the backup key');
}
return res;
});
};
export default resetPasswordOnAccountRecovery;

View File

@ -0,0 +1,22 @@
import React from 'react';
import Head from 'next/head';
export default function Activity() {
return (
<div className="bg-bunker-800 md:h-screen flex flex-col justify-between">
<Head>
<title>Request a New Invite</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-col items-center justify-center text-gray-200 h-screen w-screen">
<p className="text-6xl">Oops.</p>
<p className="mt-2 mb-1 text-xl">Your email was not verified. </p>
<p className="text-xl">Please try again.</p>
<p className="text-md mt-8 text-gray-600 max-w-sm text-center">
Note: If it still {"doesn't work"}, please reach out to us at
support@infisical.com
</p>
</div>
</div>
);
}

View File

@ -1,34 +1,37 @@
import React, { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useEffect, useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button";
import Error from "~/components/basic/Error";
import InputField from "~/components/basic/InputField";
import attemptLogin from "~/utilities/attemptLogin";
import Button from '~/components/basic/buttons/Button';
import Error from '~/components/basic/Error';
import InputField from '~/components/basic/InputField';
import attemptLogin from '~/utilities/attemptLogin';
import getWorkspaces from "./api/workspace/getWorkspaces";
import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
useEffect(async () => {
let userWorkspace;
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0]._id;
router.push("/dashboard/" + userWorkspace);
} catch (error) {
console.log("Error - Not logged in yet");
}
useEffect(() => {
const redirectToDashboard = async () => {
let userWorkspace;
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0]._id;
router.push('/dashboard/' + userWorkspace);
} catch (error) {
console.log('Error - Not logged in yet');
}
};
redirectToDashboard();
}, []);
/**
@ -73,23 +76,9 @@ 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-4xl flex justify-center font-semibold text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
Log In
<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 flex-row items-center justify-center">
<p className="text-md flex justify-center mt-2 text-gray-400">
Need an Infisical account?
</p>
</div>
<div className="flex flex-col items-center justify-center w-full md:pb-4 max-h-24 max-w-md mx-auto">
<Link href="/signup">
<button className="w-full pb-3 hover:opacity-90 duration-200">
<u className="font-normal text-md text-sky-500">
Create an account
</u>
</button>
</Link>
</div>
<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"
@ -98,10 +87,10 @@ export default function Login() {
value={email}
placeholder=""
isRequired
autocomplete="username"
autoComplete="username"
/>
</div>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 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}
@ -112,6 +101,9 @@ export default function Login() {
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">
@ -135,6 +127,16 @@ export default function Login() {
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>
</div>
);
}

View File

@ -0,0 +1,290 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '~/components/basic/buttons/Button';
import InputField from '~/components/basic/InputField';
import passwordCheck from '~/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
const queryString = require('query-string');
const nacl = require('tweetnacl');
const jsrp = require('jsrp');
nacl.util = require('tweetnacl-util');
const client = new jsrp.client();
export default function PasswordReset() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const token = parsedUrl.token;
const email = parsedUrl.to?.replace(' ', '+').trim();
const [verificationToken, setVerificationToken] = useState('');
const [step, setStep] = useState(1);
const [backupKey, setBackupKey] = useState('');
const [privateKey, setPrivateKey] = useState('');
const [newPassword, setNewPassword] = useState('');
const [backupKeyError, setBackupKeyError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
// Unencrypt the private key with a backup key
const getEncryptedKeyHandler = async () => {
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
iv: result.iv,
tag: result.tag,
secret: backupKey
})
);
setStep(3);
} catch {
setBackupKeyError(true);
}
};
// If everything is correct, reset the password
const resetPasswordHandler = async () => {
let errorCheck = false;
errorCheck = passwordCheck({
password: newPassword,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: errorCheck
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: privateKey,
secret: newPassword
.slice(0, 32)
.padStart(
32 +
(newPassword.slice(0, 32).length - new Blob([newPassword]).size),
'0'
)
}) as { ciphertext: string; iv: string; tag: string };
client.init(
{
username: email,
password: newPassword
},
async () => {
client.createVerifier(
async (err: any, result: { salt: string; verifier: string }) => {
const response = await resetPasswordOnAccountRecovery({
verificationToken,
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier
});
// if everything works, go the main dashboard page.
if (response?.status === 200) {
router.push('/login');
}
}
);
}
);
}
};
// Click a button to confirm email
const stepConfirmEmail = (
<div className="bg-bunker flex flex-col items-center w-full py-6 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold mb-8 flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
Confirm your email
</p>
<Image
src="/images/envelope.svg"
height={262}
width={410}
alt="verify email"
></Image>
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
<Button
text="Confirm Email"
onButtonPressed={async () => {
const response = await EmailVerifyOnPasswordReset({
email,
code: token
});
if (response.status == 200) {
setVerificationToken((await response.json()).token);
setStep(2);
} else {
console.log('ERROR', response);
router.push('/email-not-verified');
}
}}
size="lg"
/>
</div>
</div>
);
// Input backup key
const stepInputBackupKey = (
<div className="bg-bunker flex flex-col items-center w-full pt-6 pb-3 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-4">
Enter your backup key
</p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
You can find it in your emrgency kit. You had to download the enrgency
kit during signup.
</p>
</div>
<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="Backup Key"
onChangeHandler={setBackupKey}
type="password"
value={backupKey}
placeholder=""
isRequired
error={backupKeyError}
errorText="Something is wrong with the backup key"
/>
</div>
<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="Submit Backup Key"
onButtonPressed={() => getEncryptedKeyHandler()}
size="lg"
/>
</div>
</div>
</div>
);
// Enter new password
const stepEnterNewPassword = (
<div className="bg-bunker flex flex-col items-center w-full pt-6 pb-3 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100">
Enter new password
</p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-1 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
Make sure you save it somewhere save.
</p>
</div>
<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="New Password"
onChangeHandler={(password) => {
setNewPassword(password);
passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
currentErrorCheck: false
});
}}
type="password"
value={newPassword}
isRequired
error={
passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber
}
autoComplete="new-password"
id="new-password"
/>
</div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="w-full mt-3 bg-white/5 px-2 mx-2 flex flex-col items-start py-2 rounded-md max-w-md mb-2">
<div className={`text-gray-400 text-sm mb-1`}>
Password should contain at least:
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
14 characters
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
1 lowercase character
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className="text-md text-primary mr-2"
/>
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
1 number
</div>
</div>
</div>
) : (
<div className="py-2"></div>
)}
<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="Submit New Password"
onButtonPressed={() => resetPasswordHandler()}
size="lg"
/>
</div>
</div>
</div>
);
return (
<div className="bg-bunker-800 h-screen w-full flex flex-col items-center justify-center">
{step === 1 && stepConfirmEmail}
{step === 2 && stepInputBackupKey}
{step === 3 && stepEnterNewPassword}
</div>
);
}

View File

@ -33,7 +33,7 @@ export default function SignupInvite() {
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const [email, setEmail] = useState(parsedUrl.to);
const [email, setEmail] = useState(parsedUrl.to?.replace(' ', '+').trim());
const token = parsedUrl.token;
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -58,13 +58,13 @@ export default function SignupInvite() {
} else {
setLastNameError(false);
}
errorCheck = passwordCheck(
errorCheck = passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
);
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
@ -150,7 +150,7 @@ export default function SignupInvite() {
width={410}
alt="verify email"
></Image>
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto text-lg py-1 text-center md:text-left">
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
<Button
text="Confirm Email"
onButtonPressed={async () => {
@ -329,7 +329,7 @@ export default function SignupInvite() {
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-xs md:max-w-md mx-auto mt-6 md:mt-8 py-1 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 () => {

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import Button from '~/components/basic/buttons/Button';
import InputField from '~/components/basic/InputField';
import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset';
export default function VerifyEmail() {
const [email, setEmail] = useState('');
const [step, setStep] = useState(1);
/**
* This function sends the verification email and forwards a user to the next step.
*/
const sendVerificationEmail = async () => {
if (email) {
await SendEmailOnPasswordReset({ email });
setStep(2);
}
};
return (
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
<Head>
<title>Login</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Log In to Infisical" />
<meta
name="og:description"
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
/>
</Head>
<Link href="/">
<div className="flex justify-center mb-8 mt-20 cursor-pointer">
<Image
src="/images/biglogo.png"
height={90}
width={120}
alt="long logo"
/>
</div>
</Link>
{step == 1 && (
<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-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
Forgot your password?
</p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max">
You will need your emergency kit. Enter your email to start
account recovery.
</p>
</div>
<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="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="Continue"
onButtonPressed={sendVerificationEmail}
size="lg"
/>
</div>
</div>
</div>
)}
{step == 2 && (
<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-xl md:text-2xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
Look for an email in your inbox.
</p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max text-center">
An email with instructions has been sent to {email}.
</p>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,23 @@
const http = require('http');
const options = {
host: 'localhost',
port: 3000,
timeout: 2000,
path: '/'
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error(`HEALTH CHECK ERROR: ${err}`);
process.exit(1);
});
healthCheck.end();

View File

@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.2
version: 0.1.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View File

@ -22,11 +22,11 @@ spec:
- containerPort: 4000
env:
{{- range $key, $value := .Values.backendEnvironmentVariables }}
{{- if eq $value "MUST_REPLACE" }}
{{- if $value | quote | eq "MUST_REPLACE" }}
{{ fail "Environment variables are not set. Please set all environment variables to continue." }}
{{ end }}
- name: {{ $key }}
value: {{ $value }}
value: {{ quote $value }}
{{- end }}
---

View File

@ -20,6 +20,9 @@ spec:
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
env:
{{- range $key, $value := .Values.frontendEnvironmentVariables }}
{{- if $value | quote | eq "MUST_REPLACE" }}
{{ fail "Environment variables are not set. Please set all environment variables to continue." }}
{{ end }}
- name: {{ $key }}
value: {{ quote $value }}
{{- end }}