Continue developing service token v2

This commit is contained in:
Tuan Dang
2023-01-01 07:10:47 +07:00
156 changed files with 4451 additions and 1470 deletions

View File

@@ -41,6 +41,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith

2
.gitignore vendored
View File

@@ -12,6 +12,8 @@ node_modules
.DS_Store
/dist
/completions/
/manpages/
# frontend

View File

@@ -6,6 +6,11 @@
# - cd cli && go mod tidy
# # you may remove this if you don't need go generate
# - cd cli && go generate ./...
before:
hooks:
- ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh
builds:
- id: darwin-build
binary: infisical
@@ -30,13 +35,13 @@ builds:
- openbsd
- windows
goarch:
- 386
- "386"
- amd64
- arm
- arm64
goarm:
- 6
- 7
- "6"
- "7"
ignore:
- goos: windows
goarch: "386"
@@ -44,6 +49,16 @@ builds:
goarch: "386"
dir: ./cli
archives:
- format_overrides:
- goos: windows
format: zip
files:
- README*
- LICENSE*
- manpages/*
- completions/*
release:
replace_existing_draft: true
mode: 'replace'
@@ -85,13 +100,22 @@ nfpms:
homepage: https://infisical.com/
maintainer: Infisical, Inc
description: The offical Infisical CLI
license: Apache 2.0
license: MIT
formats:
- rpm
- deb
- apk
- archlinux
bindir: /usr/bin
contents:
- src: ./completions/infisical.bash
dst: /etc/bash_completion.d/infisical
- src: ./completions/infisical.fish
dst: /usr/share/fish/vendor_completions.d/infisical.fish
- src: ./completions/infisical.zsh
dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz
scoop:
bucket:
owner: Infisical
@@ -101,7 +125,31 @@ scoop:
email: ai@infisical.com
homepage: "https://infisical.com"
description: "The official Infisical CLI"
license: Apache-2.0
license: MIT
aurs:
-
name: infisical-bin
homepage: "https://infisical.com"
description: "The official Infisical CLI"
maintainers:
- Infisical, Inc <support@infisical.com>
license: MIT
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
package: |-
# bin
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
# license
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
# completions
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
# dockers:
# - dockerfile: goreleaser.dockerfile
# goos: linux

View File

@@ -41,7 +41,7 @@
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon)
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation**
@@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis
</tr>
<tr>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
✔️ Ruby on Rails
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
✔️ Vue
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
✔️ Vue
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
✔️ Ruby on Rails
</a>
</td>
</tr>
@@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis
</a>
</td>
</tr>
<tr>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/dotnet?ref=github.com">
✔️ .NET
</a>
</td>
<td align="left" valign="middle">
And more...
</td>
</tr>
</tbody>
</table>
@@ -321,4 +331,10 @@ 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/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).

View File

@@ -6677,9 +6677,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@@ -17175,9 +17175,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
"dev": true
},
"jsonwebtoken": {

View File

@@ -14,28 +14,31 @@ import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter
} from './ee/routes';
} from './ee/routes/v1';
import {
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter,
serviceTokenData as serviceTokenDataRouter
} from './routes';
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
serviceTokenData as v1ServiceTokenDataRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
secret as v2SecretRouter,
workspace as v2WorkspaceRouter
} from './routes/v2';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
@@ -64,29 +67,33 @@ if (NODE_ENV === 'production') {
app.use(helmet());
}
// /ee routers
// (EE) routes
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);
app.use('/api/v1/bot', botRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
app.use('/api/v1/service-token-data', serviceTokenDataRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/service-token-data', v1ServiceTokenDataRouter);
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{

View File

@@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User } from '../models';
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
import { User } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../config';
} from '../../config';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../models';
import { createBot } from '../helpers/bot';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
interface BotKey {
encryptedKey: string;

View File

@@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
import * as userActionController from './userActionController';
import * as userController from './userController';
import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
export {
authController,
@@ -31,5 +32,6 @@ export {
stripeController,
userActionController,
userController,
workspaceController
workspaceController,
serviceTokenDataController
};

View File

@@ -2,10 +2,10 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import axios from 'axios';
import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
import { IntegrationService } from '../services';
import { getApps, revokeAccess } from '../integrations';
import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
export const getIntegrationOptions = async (
req: Request,

View File

@@ -1,9 +1,9 @@
import { Request, Response } from 'express';
import { readFileSync } from 'fs';
import * as Sentry from '@sentry/node';
import { Integration, Bot, BotKey } from '../models';
import { EventService } from '../services';
import { eventPushSecrets } from '../events';
import { Integration, Bot, BotKey } from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface Key {
encryptedKey: string;

View File

@@ -1,8 +1,8 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../models';
import { findMembership } from '../helpers/membership';
import { GRANTED } from '../variables';
import { Key } from '../../models';
import { findMembership } from '../../helpers/membership';
import { GRANTED } from '../../variables';
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with

View File

@@ -1,13 +1,13 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../models';
import { Membership, MembershipOrg, User, Key } from '../../models';
import {
findMembership,
deleteMembership as deleteMember
} from '../helpers/membership';
import { sendMail } from '../helpers/nodemailer';
import { SITE_URL } from '../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
} from '../../helpers/membership';
import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables';
/**
* Check that user is a member of workspace with id [workspaceId]

View File

@@ -1,14 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
import { MembershipOrg, Organization, User, Token } from '../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
import { checkEmailVerification } from '../helpers/signup';
import { createToken } from '../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../helpers/organization';
import { sendMail } from '../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
/**
* Delete organization membership with id [membershipOrgId] from organization
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// TODO
let membershipToChangeRole;
try {
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization membership role'
});
}
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({
membershipOrg: membershipToChangeRole
@@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: 'Failed email magic link verification for complete account'
});
}
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
@@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
code
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
return res.status(200).send({
message: 'Successfully verified email',
user,
});
}
if (!user) {
// initialize user account
user = await new User({

View File

@@ -6,7 +6,7 @@ import {
STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_CARD_AUTH
} from '../config';
} from '../../config';
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
@@ -18,10 +18,10 @@ import {
Organization,
Workspace,
IncidentContactOrg
} from '../models';
import { createOrganization as create } from '../helpers/organization';
import { addMembershipsOrg } from '../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../variables';
} from '../../models';
import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../../variables';
const productToPriceMap = {
starter: STRIPE_PRODUCT_STARTER,

View File

@@ -1,13 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../models';
import { checkEmailVerification } from '../helpers/signup';
import { createToken } from '../helpers/auth';
import { sendMail } from '../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
import { User, Token, BackupPrivateKey } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
const clientPublicKeys: any = {};

View File

@@ -1,16 +1,16 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key, Secret } from '../models';
import { Key, Secret } from '../../models';
import {
pushSecrets as push,
v1PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../helpers/secret';
import { pushKeys } from '../helpers/key';
import { eventPushSecrets } from '../events';
import { EventService } from '../services';
import { ENV_SET } from '../variables';
import { postHogClient } from '../services';
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { ENV_SET } from '../../variables';
import { postHogClient } from '../../services';
interface PushSecret {
ciphertextKey: string;
@@ -21,6 +21,10 @@ interface PushSecret {
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal';
}

View File

@@ -1,8 +1,8 @@
import { Request, Response } from 'express';
import { ServiceToken } from '../models';
import { createToken } from '../helpers/auth';
import { ENV_SET } from '../variables';
import { JWT_SERVICE_SECRET } from '../config';
import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../../config';
/**
* Return service token on request
@@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../config';
* @returns
*/
export const getServiceToken = async (req: Request, res: Response) => {
// get service token
return res.status(200).send({
serviceToken: req.serviceToken
});
@@ -73,13 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({
token
});
};
/**
* SERVICE_TOKEN: <JWT_SERVICE_TOKEN>,<PRIVATE_KEY_SERVICE_TOKEN>
* - <JWT_SERVICE_TOKEN> authorizes the service token for "service token"-only endpoints.
* - <PRIVATE_KEY_SERVICE_TOKEN> authorizes the service token to pull secrets via that endpoint.
*
*
*
*/
};

View File

@@ -0,0 +1,102 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceTokenData
} from '../../models';
import {
SALT_ROUNDS
} from '../../config';
/**
* Return service token data associated with service token on request
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => ({
serviceTokenData: req.serviceTokenData
});
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
try {
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn
} = req.body;
// create 41-char service token with first 9-char being the prefix
serviceToken = `st.${crypto.randomBytes(19).toString('hex')}`;
const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS);
// compute access token expiration date
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
prefix: serviceToken.substring(0, 9),
serviceTokenHash,
encryptedKey,
iv,
tag
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
}
return res.status(200).send({
serviceToken,
serviceTokenData
});
}
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});
}

View File

@@ -1,15 +1,15 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
import { User, MembershipOrg } from '../models';
import { completeAccount } from '../helpers/user';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
sendEmailVerification,
checkEmailVerification,
initializeDefaultOrg
} from '../helpers/signup';
import { issueTokens, createToken } from '../helpers/auth';
import { INVITED, ACCEPTED } from '../variables';
} from '../../helpers/signup';
import { issueTokens, createToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios';
/**

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../models';
import { UserAction } from '../../models';
/**
* Add user action [action]

View File

@@ -8,14 +8,14 @@ import {
IntegrationAuth,
IUser,
ServiceToken,
ServiceTokenData,
} from '../models';
ServiceTokenData
} from '../../models';
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../helpers/workspace';
import { addMemberships } from '../helpers/membership';
import { ADMIN, COMPLETED, GRANTED } from '../variables';
} from '../../helpers/workspace';
import { addMemberships } from '../../helpers/membership';
import { ADMIN, COMPLETED, GRANTED } from '../../variables';
/**
* Return public keys of members of workspace with id [workspaceId]

View File

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

View File

@@ -0,0 +1,194 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
Key,
IUser,
ServiceToken,
} from '../../models';
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import {
v2PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { addMemberships } from '../../helpers/membership';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables';
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
/**
* Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets
secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
);
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId;
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
}
secrets = await pull({
userId,
workspaceId,
environment
});
if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets
});
};
export const getWorkspaceKey = async (req: Request, res: Response) => {
let key;
try {
const { workspaceId } = req.params;
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
return res.status(200).send({
key
});
}

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretVersion } from '../models';
import { SecretVersion } from '../../models';
/**
* Return secret versions for secret with id [secretId]

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
});

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../models';
import { SecretSnapshot } from '../../models';
/**
* Return secret snapshots for workspace with id [workspaceId]

View File

@@ -4,14 +4,16 @@ import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
} from '../../../middleware';
import { body, query, param } from 'express-validator';
import { secretController } from '../controllers';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables';
router.get(
'/:secretId/secret-versions',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]

View File

@@ -1,6 +1,6 @@
import express from 'express';
const router = express.Router();
import { stripeController } from '../controllers';
import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook);

View File

@@ -4,14 +4,16 @@ import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
} from '../../../middleware';
import { param, query } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { workspaceController } from '../controllers';
import { ADMIN, MEMBER, GRANTED } from '../../../variables';
import { workspaceController } from '../../controllers/v1';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]

View File

@@ -1,15 +1,69 @@
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
User
User,
ServiceTokenData
} from '../models';
import {
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET
JWT_REFRESH_SECRET,
SALT_ROUNDS
} from '../config';
/**
* Attach auth payload
* @param {Object} obj
* @param {String} obj.authTokenValue
*/
const attachAuthPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
let serviceTokenHash, decodedToken; // intermediate variables
let serviceTokenData, user; // payloads
try {
switch (authTokenValue.split('.', 1)[0]) {
case 'st':
// case: service token auth mode
serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS);
serviceTokenData = await ServiceTokenData
.findOne({
serviceTokenHash
})
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) {
throw new Error('Account not found error');
}
return serviceTokenData;
default:
// case: JWT token auth mode
decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);
user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user)
throw new Error('Account not found error');
if (!user?.publicKey)
throw new Error('Unable to authenticate due to partially set up account');
return user;
}
} catch (err) {
throw new Error('Failed to attach auth payload');
}
}
/**
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
* @param {Object} obj
@@ -99,4 +153,9 @@ const createToken = ({
}
};
export { createToken, issueTokens, clearTokens };
export {
attachAuthPayload,
createToken,
issueTokens,
clearTokens
};

View File

@@ -12,7 +12,6 @@ import {
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { decryptSecrets } from '../helpers/secret';
import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables';

View File

@@ -26,7 +26,7 @@ const validateMembership = async ({
membership = await Membership.findOne({
user: userId,
workspace: workspaceId
});
}).populate("workspace");
if (!membership) throw new Error('Failed to find membership');

View File

@@ -14,9 +14,8 @@ import {
} from '../ee/helpers/secret';
import { decryptSymmetric } from '../utils/crypto';
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
import { LICENSE_KEY } from '../config';
interface PushSecret {
interface V1PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
@@ -25,9 +24,29 @@ interface PushSecret {
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal';
}
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
interface Update {
[index: string]: any;
}
@@ -45,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const pushSecrets = async ({
const v1PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -54,7 +73,7 @@ const pushSecrets = async ({
userId: string;
workspaceId: string;
environment: string;
secrets: PushSecret[];
secrets: V1PushSecret[];
}): Promise<void> => {
// TODO: clean up function and fix up types
try {
@@ -93,8 +112,9 @@ const pushSecrets = async ({
const toUpdate = oldSecrets
.filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) {
// case: filter secrets where value changed
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed
return true;
}
@@ -113,14 +133,22 @@ const pushSecrets = async ({
ciphertextValue,
ivValue,
tagValue,
hashValue
hashValue,
ciphertextComment,
ivComment,
tagComment,
hashComment
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext: ciphertextValue,
secretValueIV: ivValue,
secretValueTag: tagValue,
secretValueHash: hashValue
secretValueHash: hashValue,
secretCommentCiphertext: ciphertextComment,
secretCommentIV: ivComment,
secretCommentTag: tagComment,
secretCommentHash: hashComment,
}
if (!s.version) {
@@ -192,7 +220,254 @@ const pushSecrets = async ({
secretValueCiphertext: s.ciphertextValue,
secretValueIV: s.ivValue,
secretValueTag: s.tagValue,
secretValueHash: s.hashValue
secretValueHash: s.hashValue,
secretCommentCiphertext: s.ciphertextComment,
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment
};
if (toAdd[idx].type === 'personal') {
obj['user' as keyof typeof obj] = userId;
}
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);
throw new Error('Failed to push shared and personal secrets');
}
};
/**
* Push secrets for user with id [userId] to workspace
* with id [workspaceId] with environment [environment]. Follow steps:
* 1. Handle shared secrets (insert, delete)
* 2. handle personal secrets (insert, delete)
* @param {Object} obj
* @param {String} obj.userId - id of user to push secrets for
* @param {String} obj.workspaceId - id of workspace to push to
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const v2PushSecrets = async ({
userId,
workspaceId,
environment,
secrets
}: {
userId: string;
workspaceId: string;
environment: string;
secrets: V2PushSecret[];
}): Promise<void> => {
// TODO: clean up function and fix up types
try {
// construct useful data structures
const oldSecrets = await pullSecrets({
userId,
workspaceId,
environment
});
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.secretKeyHash}`]: s })
, {});
// handle deleting secrets
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 }
});
await SecretVersion.updateMany({
secret: { $in: toDelete }
}, {
isDeleted: true
});
}
const toUpdate = oldSecrets
.filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
// case: filter secrets where value or comment changed
return true;
}
if (!s.version) {
// case: filter (legacy) secrets that were not versioned
return true;
}
}
return false;
});
const operations = toUpdate
.map((s) => {
const {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
return ({
secret: s._id,
version: s.version ? s.version + 1 : 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
})
})
});
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map(({
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}, idx) => {
const obj: any = {
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash
};
if (toAdd[idx].type === 'personal') {
@@ -315,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
iv: s.secretValueIV,
tag: s.secretValueTag,
hash: s.secretValueHash
},
secretComment: {
workspace: s.workspace,
ciphertext: s.secretCommentCiphertext,
iv: s.secretCommentIV,
tag: s.secretCommentTag,
hash: s.secretCommentHash
}
}));
} catch (err) {
@@ -326,73 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets;
};
/**
* Return decrypted secrets in format [format]
* @param {Object} obj
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
* @return {String|Object} (decrypted) secrets also called the content
*/
const decryptSecrets = ({
secrets,
key,
format
}: {
secrets: PushSecret[];
key: string;
format: DecryptSecretType;
}) => {
// init content
let content: any = format === 'text' ? '' : {};
// decrypt secrets
secrets.forEach((s, idx) => {
const secretKey = decryptSymmetric({
ciphertext: s.ciphertextKey,
iv: s.ivKey,
tag: s.tagKey,
key
});
const secretValue = decryptSymmetric({
ciphertext: s.ciphertextValue,
iv: s.ivValue,
tag: s.tagValue,
key
});
switch (format) {
case 'text':
content += secretKey;
content += '=';
content += secretValue;
if (idx < secrets.length) {
content += '\n';
}
break;
case 'object':
content[secretKey] = secretValue;
break;
case 'expanded':
content[secretKey] = {
...s,
plaintextKey: secretKey,
plaintextValue: secretValue
};
break;
}
});
return content;
};
export {
pushSecrets,
v1PushSecrets,
v2PushSecrets,
pullSecrets,
reformatPullSecrets,
decryptSecrets
reformatPullSecrets
};

View File

@@ -21,7 +21,6 @@ import {
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
import { user } from '../routes';
interface ExchangeCodeHerokuResponse {
token_type: string;

View File

@@ -1,6 +1,9 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User } from '../models';
import { User, ServiceTokenData } from '../models';
import {
attachAuthPayload
} from '../helpers/auth';
import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
@@ -11,34 +14,58 @@ declare module 'jsonwebtoken' {
}
/**
* Validate if JWT (auth) token on request is valid (e.g. not expired),
* if there is an associated user, and if that user is fully setup.
* @param req - express request object
* @param res - express response object
* @param next - express next function
* Validate if token on request is valid (e.g. not expired) for various auth modes:
* - If token is a JWT token, then check if there is an associated user
* and if user is fully setup.
* - If token is a service token (st), then check if there is associated
* service token data.
* @param {Object} obj
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
* @returns
*/
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const requireAuth = ({
acceptedAuthModes = ['jwt']
}: {
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null)
return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null)
return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
// validate auth mode
let authMode;
switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) {
case 'st':
authMode = 'st';
break;
default:
authMode = 'jwt';
break;
}
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
);
// attach auth request payload
const payload = await attachAuthPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
switch (authMode) {
case 'st':
req.serviceTokenData = payload;
break;
default:
req.user = payload;
break;
}
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey)
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
req.user = user;
return next();
};
return next();
}
}
export default requireAuth;

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { ServiceToken, ServiceTokenData } from '../models';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors';
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
@@ -15,21 +15,25 @@ const requireServiceTokenDataAuth = ({
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId)
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) {
return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
if (req.user) {
// case: jwt auth
await validateMembership({
userId: req.user._id.toString(),
workspaceId: serviceTokenData.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
}
req.serviceTokenData = serviceTokenData;

View File

@@ -8,8 +8,8 @@ type req = 'params' | 'body' | 'query';
* Validate if user on request is a member with proper roles for workspace
* on request params.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses for JWT auth
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireWorkspaceAuth = ({
@@ -22,17 +22,28 @@ const requireWorkspaceAuth = ({
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// workspace authorization middleware
try {
const membership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: req[location].workspaceId,
acceptedRoles,
acceptedStatuses
});
const { workspaceId } = req[location];
req.membership = membership;
if (req.user) {
// case: jwt auth
const membership = await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles,
acceptedStatuses
});
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace !== workspaceId
&& req.serviceTokenData.environment !== req.body.environment
)
// case: st auth
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
return next();
} catch (err) {

View File

@@ -23,6 +23,10 @@ export interface ISecret {
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
const secretSchema = new Schema<ISecret>(
@@ -82,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
secretValueHash: {
type: String,
required: true
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
}
},
{

View File

@@ -5,6 +5,7 @@ export interface IServiceTokenData {
name: string;
workspace: Types.ObjectId;
environment: string; // TODO: adapt to upcoming environment id
user: Types.ObjectId;
expiresAt: Date;
prefix: string;
serviceTokenHash: string;
@@ -28,6 +29,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: {
type: Date
},
@@ -38,7 +44,8 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
serviceTokenHash: {
type: String,
unique: true,
required: true
required: true,
select: true
},
encryptedKey: {
type: String,

View File

@@ -1,144 +0,0 @@
import express from 'express';
const router = express.Router();
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import * as Sentry from '@sentry/node';
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenDataAuth,
validateRequest
} from '../middleware';
import {
ServiceTokenData
} from '../models';
import { param, body, query } from 'express-validator';
import {
SALT_ROUNDS
} from '../config';
import {
ADMIN,
MEMBER,
COMPLETED,
GRANTED
} from '../variables';
// TODO: move logic into separate controller (probably after pull with latest routing)
/**
* 2 different concepts that we should distinguish between:
* - API key (user) - allows user to perform queries and mutations on whatever
* their account could access (better than JWT because it has ACL and scoping).
* - Service token (bound to a workspace and environment).
*/
/**
* Service token flow?
* 1. Post service token data details including project key encrypted under <symmetric_key> on cient-side.
* 2. Construct <infisical_token> on client-side as <infisical_token>=<service_token>.<symmetric_key>
* 3. Need for CLI to be able to get back service token details
*/
router.post(
'/',
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
location: 'body'
}),
requireAuth,
body('name').exists().trim(),
body('workspace'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresAt'),
validateRequest,
async (req, res) => {
let serviceToken, serviceTokenData;
try {
const {
name,
workspace,
environment,
encryptedKey,
iv,
tag,
expiresAt
} = req.body;
// create 38-char service token with first 6-char being the prefix
serviceToken = crypto.randomBytes(19).toString('hex');
const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS);
serviceTokenData = await new ServiceTokenData({
name,
workspace,
environment,
expiresAt,
prefix: serviceToken.substring(0, 6),
serviceTokenHash,
encryptedKey,
iv,
tag
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
}
return res.status(200).send({
serviceToken,
serviceTokenData
});
}
);
// TODO: CLI has to get service token details without needing a JWT
router.get(
'/:serviceTokenDataId',
requireAuth,
requireServiceTokenDataAuth,
param('serviceTokenDataId').exists().trim(),
validateRequest,
async (req, res) => {
return ({
serviceTokenData: req.serviceTokenData
});
}
);
router.delete(
'/:serviceTokenDataId',
requireAuth,
requireServiceTokenDataAuth,
param('serviceTokenDataId').exists().trim(),
validateRequest,
async (req, res) => {
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});
}
);
export default router;

View File

@@ -1,8 +0,0 @@
import express from 'express';
const router = express.Router();
import { requireAuth } from '../middleware';
import { userController } from '../controllers';
router.get('/', requireAuth, userController.getUser);
export default router;

View File

@@ -1,9 +1,9 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { authController } from '../controllers';
import { loginLimiter } from '../helpers/rateLimiter';
import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1';
import { loginLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken);
@@ -25,7 +25,19 @@ router.post(
authController.login2
);
router.post('/logout', requireAuth, authController.logout);
router.post('/checkAuth', requireAuth, authController.checkAuth);
router.post(
'/logout',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.logout
);
router.post(
'/checkAuth',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.checkAuth
);
export default router;

View File

@@ -6,13 +6,15 @@ import {
requireBotAuth,
requireWorkspaceAuth,
validateRequest
} from '../middleware';
import { botController } from '../controllers';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
} from '../../middleware';
import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.get(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -24,7 +26,9 @@ router.get(
router.patch(
'/:botId/active',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireBotAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]

View File

@@ -4,14 +4,16 @@ import {
requireAuth,
requireIntegrationAuth,
validateRequest
} from '../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables';
} from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../controllers';
import { integrationController } from '../../controllers/v1';
router.patch(
'/:integrationId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -29,7 +31,9 @@ router.patch(
router.delete(
'/:integrationId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]

View File

@@ -6,19 +6,23 @@ import {
requireWorkspaceAuth,
requireIntegrationAuthorizationAuth,
validateRequest
} from '../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables';
import { integrationAuthController } from '../controllers';
} from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { integrationAuthController } from '../../controllers/v1';
router.get(
'/integration-options',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
integrationAuthController.getIntegrationOptions
);
router.post(
'/oauth-token',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],
@@ -33,7 +37,9 @@ router.post(
router.get(
'/:integrationAuthId/apps',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -45,7 +51,9 @@ router.get(
router.delete(
'/:integrationAuthId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],

View File

@@ -1,12 +1,14 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { membershipOrgController } from '../controllers';
import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../../controllers/v1';
router.post(
'/signup',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(),
validateRequest,

View File

@@ -4,14 +4,16 @@ import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../middleware';
} from '../../middleware';
import { body, param } from 'express-validator';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
import { keyController } from '../controllers';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { keyController } from '../../controllers/v1';
router.post(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -24,7 +26,9 @@ router.post(
router.get(
'/:workspaceId/latest',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]

View File

@@ -1,12 +1,14 @@
import express from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { membershipController } from '../controllers';
import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1';
router.get( // used for CLI (deprecate)
'/:workspaceId/connect',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('workspaceId').exists().trim(),
validateRequest,
membershipController.validateMembership
@@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate)
router.delete(
'/:membershipId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipId').exists().trim(),
validateRequest,
membershipController.deleteMembership
@@ -22,7 +26,9 @@ router.delete(
router.post(
'/:membershipId/change-role',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('role').exists().trim(),
validateRequest,
membershipController.changeMembershipRole

View File

@@ -1,13 +1,15 @@
import express from 'express';
const router = express.Router();
import { param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware';
import { membershipOrgController } from '../controllers';
import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../../controllers/v1';
router.post(
// TODO
'/membershipOrg/:membershipOrgId/change-role',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId'),
validateRequest,
membershipOrgController.changeMembershipOrgRole
@@ -15,7 +17,9 @@ router.post(
router.delete(
'/:membershipOrgId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId').exists().trim(),
validateRequest,
membershipOrgController.deleteMembershipOrg

View File

@@ -5,19 +5,23 @@ import {
requireAuth,
requireOrganizationAuth,
validateRequest
} from '../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables';
import { organizationController } from '../controllers';
} from '../../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationController } from '../../controllers/v1';
router.get(
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
organizationController.getOrganizations
);
router.post( // not used on frontend
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('organizationName').exists().trim().notEmpty(),
validateRequest,
organizationController.createOrganization
@@ -25,7 +29,9 @@ router.post( // not used on frontend
router.get(
'/:organizationId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -37,7 +43,9 @@ router.get(
router.get(
'/:organizationId/users',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -49,7 +57,9 @@ router.get(
router.get(
'/:organizationId/my-workspaces',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -61,7 +71,9 @@ router.get(
router.patch(
'/:organizationId/name',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -74,7 +86,9 @@ router.patch(
router.get(
'/:organizationId/incidentContactOrg',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -86,7 +100,9 @@ router.get(
router.post(
'/:organizationId/incidentContactOrg',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -99,7 +115,9 @@ router.post(
router.delete(
'/:organizationId/incidentContactOrg',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -112,7 +130,9 @@ router.delete(
router.post(
'/:organizationId/customer-portal-session',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
@@ -124,7 +144,9 @@ router.post(
router.get(
'/:organizationId/subscriptions',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]

View File

@@ -1,13 +1,15 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
import { passwordController } from '../controllers';
import { passwordLimiter } from '../helpers/rateLimiter';
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
import { passwordController } from '../../controllers/v1';
import { passwordLimiter } from '../../helpers/rateLimiter';
router.post(
'/srp1',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientPublicKey').exists().trim().notEmpty(),
validateRequest,
passwordController.srp1
@@ -16,7 +18,9 @@ router.post(
router.post(
'/change-password',
passwordLimiter,
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
body('iv').exists().trim().notEmpty(), // new iv for private key
@@ -54,7 +58,9 @@ router.get(
router.post(
'/backup-private-key',
passwordLimiter,
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key

View File

@@ -5,14 +5,16 @@ import {
requireWorkspaceAuth,
requireServiceTokenAuth,
validateRequest
} from '../middleware';
} from '../../middleware';
import { body, query, param } from 'express-validator';
import { secretController } from '../controllers';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.post(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -28,7 +30,9 @@ router.post(
router.get(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]

View File

@@ -5,12 +5,12 @@ import {
requireWorkspaceAuth,
requireServiceTokenAuth,
validateRequest
} from '../middleware';
} from '../../middleware';
import { body } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../variables';
import { serviceTokenController } from '../controllers';
import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { serviceTokenController } from '../../controllers/v1';
// Note to devs: service-token to be deprecated in favor of api-key
// note: deprecate service-token routes in favor of service-token data routes/structure
router.get(
'/',
@@ -20,7 +20,9 @@ router.get(
router.post(
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED],

View File

@@ -0,0 +1,63 @@
import express, { Request, Response } from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenDataAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
import {
ADMIN,
MEMBER,
COMPLETED,
GRANTED
} from '../../variables';
import { serviceTokenDataController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['st']
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,
serviceTokenDataController.getServiceTokenData
);
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
location: 'body'
}),
body('name').exists().trim(),
body('workspaceId'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresIn'), // measured in ms
validateRequest,
serviceTokenDataController.createServiceTokenData
);
router.delete(
'/:serviceTokenDataId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,
serviceTokenDataController.deleteServiceTokenData
);
export default router;

View File

@@ -1,9 +1,9 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { requireSignupAuth, validateRequest } from '../middleware';
import { signupController } from '../controllers';
import { signupLimiter } from '../helpers/rateLimiter';
import { requireSignupAuth, validateRequest } from '../../middleware';
import { signupController } from '../../controllers/v1';
import { signupLimiter } from '../../helpers/rateLimiter';
router.post(
'/email/signup',

View File

@@ -1,6 +1,6 @@
import express from 'express';
const router = express.Router();
import { stripeController } from '../controllers';
import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook);

View File

@@ -0,0 +1,14 @@
import express from 'express';
const router = express.Router();
import { requireAuth } from '../../middleware';
import { userController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
userController.getUser
);
export default router;

View File

@@ -1,12 +1,14 @@
import express from 'express';
const router = express.Router();
import { requireAuth, validateRequest } from '../middleware';
import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator';
import { userActionController } from '../controllers';
import { userActionController } from '../../controllers/v1';
router.post(
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('action'),
validateRequest,
userActionController.addUserAction
@@ -14,7 +16,9 @@ router.post(
router.get(
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
query('action'),
validateRequest,
userActionController.getUserAction

View File

@@ -5,13 +5,15 @@ import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
import { workspaceController, membershipController } from '../controllers';
} from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { workspaceController, membershipController } from '../../controllers/v1';
router.get(
'/:workspaceId/keys',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -23,7 +25,9 @@ router.get(
router.get(
'/:workspaceId/users',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -33,11 +37,19 @@ router.get(
workspaceController.getWorkspaceMemberships
);
router.get('/', requireAuth, workspaceController.getWorkspaces);
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
workspaceController.getWorkspaces
);
router.get(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -49,7 +61,9 @@ router.get(
router.post(
'/',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(),
validateRequest,
@@ -58,7 +72,9 @@ router.post(
router.delete(
'/:workspaceId',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED]
@@ -70,7 +86,9 @@ router.delete(
router.post(
'/:workspaceId/name',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
@@ -83,7 +101,9 @@ router.post(
router.post(
'/:workspaceId/invite-signup',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -96,7 +116,9 @@ router.post(
router.get(
'/:workspaceId/integrations',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -108,7 +130,9 @@ router.get(
router.get(
'/:workspaceId/authorizations',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -120,7 +144,9 @@ router.get(
router.get(
'/:workspaceId/service-tokens', // deprecate
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
@@ -132,7 +158,9 @@ router.get(
router.get(
'/:workspaceId/service-token-data',
requireAuth,
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]

View File

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

View File

@@ -0,0 +1,4 @@
import express from 'express';
const router = express.Router();
export default router;

View File

@@ -0,0 +1,62 @@
import express from 'express';
const router = express.Router();
import { body, param, query } from 'express-validator';
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { membershipController } from '../../controllers/v1';
import { workspaceController } from '../../controllers/v2';
router.post( // unfinished
'/:workspaceId/secrets',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
body('secrets').exists(),
body('keys').exists(),
body('environment').exists().trim().notEmpty(),
body('channel'),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.pushWorkspaceSecrets
);
router.get( // unfinished, check that it works with st
'/:workspaceId/secrets',
requireAuth({
acceptedAuthModes: ['jwt', 'st']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
query('environment').exists().trim(),
query('channel'),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.pullSecrets
);
router.get(
'/:workspaceId/key',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceKey
);
export default router;

View File

@@ -4,6 +4,8 @@ go 1.19
require (
github.com/99designs/keyring v1.2.2
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/roff v0.1.0
github.com/spf13/cobra v1.6.1
golang.org/x/crypto v0.3.0
golang.org/x/term v0.3.0
@@ -22,6 +24,8 @@ require (
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect

View File

@@ -56,6 +56,14 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=

35
cli/packages/cmd/man.go Normal file
View File

@@ -0,0 +1,35 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
"github.com/spf13/cobra"
)
var manCmd = &cobra.Command{
Use: "man",
Short: "generates the manpages",
SilenceUsage: true,
DisableFlagsInUseLine: true,
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
manPage, err := mcobra.NewManPage(1, rootCmd)
if err != nil {
return err
}
_, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument()))
return err
},
}
func init() {
rootCmd.AddCommand(manCmd)
}

View File

@@ -14,8 +14,8 @@ var rootCmd = &cobra.Command{
Use: "infisical",
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.15",
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
Version: "0.1.16",
}
// Execute adds all child commands to the root command and sets flags appropriately.

8
cli/scripts/completions.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
rm -rf completions
mkdir completions
cd cli
for sh in bash zsh fish; do
go run . completion "$sh" > "../completions/infisical.$sh"
done

6
cli/scripts/manpages.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
rm -rf manpages
mkdir manpages
cd cli
go run . man | gzip -c > "../manpages/infisical.1.gz"

View File

@@ -48,6 +48,8 @@ services:
- ./frontend/public:/app/public
- ./frontend/styles:/app/styles
- ./frontend/components:/app/components
- ./frontend/locales:/app/locales
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
env_file: .env
environment:
- NEXT_PUBLIC_ENV=development

View File

@@ -88,6 +88,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo
sudo apt-get update && sudo apt-get install -y infisical
```
</Tab>
<Tab title="Arch Linux">
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
```bash
yay -S infisical-bin
```
</Tab>
</Tabs>

View File

@@ -0,0 +1,27 @@
---
title: ".NET"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- [Install the CLI](/cli/overview)
## Initialize Infisical for your [.NET](https://dotnet.microsoft.com) app
```bash
# navigate to the root of your of your project
cd /path/to/project
# then initialize infisical
infisical init
```
## Start your application as usual but with Infisical
```bash
infisical run -- <your application start command>
# Example
infisical run -- dotnet run
```

View File

@@ -166,7 +166,8 @@
"integrations/frameworks/django",
"integrations/frameworks/flask",
"integrations/frameworks/laravel",
"integrations/frameworks/rails"
"integrations/frameworks/rails",
"integrations/frameworks/dotnet"
]
},
{

2
frontend/.gitignore vendored
View File

@@ -32,3 +32,5 @@ yarn-error.log*
.env.production.local
.vercel
.env.infisical
.vscode

View File

@@ -48,6 +48,7 @@ export default function RouteGuard({ children }) {
// Check if the user is authenticated
const response = await checkAuth();
// #TODO: figure our why sometimes it doesn't output a response
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
if (!publicPaths.includes(path)) {
try {
if (response.status !== 200) {

View File

@@ -1,38 +1,39 @@
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import {
faBookOpen,
faGear,
faKey,
faMobile,
faPlug,
faUser
} from '@fortawesome/free-solid-svg-icons';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from '~/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
import checkUserAction from '~/pages/api/userActions/checkUserAction';
import addUserToWorkspace from '~/pages/api/workspace/addUserToWorkspace';
import createWorkspace from '~/pages/api/workspace/createWorkspace';
import getWorkspaces from '~/pages/api/workspace/getWorkspaces';
import uploadKeys from '~/pages/api/workspace/uploadKeys';
import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
import checkUserAction from "~/pages/api/userActions/checkUserAction";
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import uploadKeys from "~/pages/api/workspace/uploadKeys";
import NavBarDashboard from '../navigation/NavBarDashboard';
import onboardingCheck from '../utilities/checks/OnboardingCheck';
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
import NavBarDashboard from "../navigation/NavBarDashboard";
import onboardingCheck from "../utilities/checks/OnboardingCheck";
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
import {
decryptAssymmetric,
encryptAssymmetric
} from '../utilities/cryptography/crypto';
import Button from './buttons/Button';
import AddWorkspaceDialog from './dialog/AddWorkspaceDialog';
import Listbox from './Listbox';
encryptAssymmetric,
} from "../utilities/cryptography/crypto";
import Button from "./buttons/Button";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import Listbox from "./Listbox";
interface LayoutProps {
children: React.ReactNode;
@@ -41,15 +42,17 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) {
const router = useRouter();
const [workspaceList, setWorkspaceList] = useState([]);
const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]);
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
const [newWorkspaceName, setNewWorkspaceName] = useState('');
const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]);
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
const [newWorkspaceName, setNewWorkspaceName] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] =
useState(0);
const { t } = useTranslation();
function closeModal() {
setIsOpen(false);
}
@@ -75,35 +78,35 @@ export default function Layout({ children }: LayoutProps) {
if (!currentWorkspaces.includes(workspaceName)) {
const newWorkspace = await createWorkspace({
workspaceName,
organizationId: tempLocalStorage('orgData.id')
organizationId: tempLocalStorage("orgData.id"),
});
const newWorkspaceId = newWorkspace._id;
if (addAllUsers) {
const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage('orgData.id')
orgId: tempLocalStorage("orgData.id"),
});
orgUsers.map(async (user: any) => {
if (user.status == 'accepted') {
if (user.status == "accepted") {
const result = await addUserToWorkspace(
user.user.email,
newWorkspaceId
);
if (result?.invitee && result?.latestKey) {
const PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY');
const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
privateKey: PRIVATE_KEY,
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey: result.invitee.publicKey,
privateKey: PRIVATE_KEY
privateKey: PRIVATE_KEY,
}) as { ciphertext: string; nonce: string };
uploadKeys(
@@ -116,11 +119,11 @@ export default function Layout({ children }: LayoutProps) {
}
});
}
router.push('/dashboard/' + newWorkspaceId + '?Development');
router.push("/dashboard/" + newWorkspaceId + "?Development");
setIsOpen(false);
setNewWorkspaceName('');
setNewWorkspaceName("");
} else {
console.error('A project with this name already exists.');
console.error("A project with this name already exists.");
setError(true);
setLoading(false);
}
@@ -131,62 +134,65 @@ export default function Layout({ children }: LayoutProps) {
}
}
const menuItems = [
{
href:
'/dashboard/' +
workspaceMapping[workspaceSelected as any] +
'?Development',
title: 'Secrets',
emoji: <FontAwesomeIcon icon={faKey} />
},
{
href: '/users/' + workspaceMapping[workspaceSelected as any],
title: 'Members',
emoji: <FontAwesomeIcon icon={faUser} />
},
{
href: '/integrations/' + workspaceMapping[workspaceSelected as any],
title: 'Integrations',
emoji: <FontAwesomeIcon icon={faPlug} />
},
{
href: '/settings/project/' + workspaceMapping[workspaceSelected as any],
title: 'Project Settings',
emoji: <FontAwesomeIcon icon={faGear} />
}
];
const menuItems = useMemo(
() => [
{
href:
"/dashboard/" +
workspaceMapping[workspaceSelected as any] +
"?Development",
title: t("nav:menu.secrets"),
emoji: <FontAwesomeIcon icon={faKey} />,
},
{
href: "/users/" + workspaceMapping[workspaceSelected as any],
title: t("nav:menu.members"),
emoji: <FontAwesomeIcon icon={faUser} />,
},
{
href: "/integrations/" + workspaceMapping[workspaceSelected as any],
title: t("nav:menu.integrations"),
emoji: <FontAwesomeIcon icon={faPlug} />,
},
{
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
title: t("nav:menu.project-settings"),
emoji: <FontAwesomeIcon icon={faGear} />,
},
],
[t, workspaceMapping, workspaceSelected]
);
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
if (tempLocalStorage("orgData.id") === "") {
const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id);
localStorage.setItem("orgData.id", userOrgs[0]._id);
}
const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage('orgData.id')
orgId: tempLocalStorage("orgData.id"),
});
const userWorkspaces = orgUserProjects;
if (
userWorkspaces.length == 0 &&
router.asPath != '/noprojects' &&
!router.asPath.includes('settings')
router.asPath != "/noprojects" &&
!router.asPath.includes("settings")
) {
router.push('/noprojects');
} else if (router.asPath != '/noprojects') {
router.push("/noprojects");
} else if (router.asPath != "/noprojects") {
const intendedWorkspaceId = router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
.split("/")
[router.asPath.split("/").length - 1].split("?")[0];
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
intendedWorkspaceId != 'heroku' &&
intendedWorkspaceId != "heroku" &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
) {
router.push('/dashboard/' + userWorkspaces[0]._id + '?Development');
router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
} else {
setWorkspaceList(
userWorkspaces.map((workspace: any) => workspace.name)
@@ -195,7 +201,7 @@ export default function Layout({ children }: LayoutProps) {
Object.fromEntries(
userWorkspaces.map((workspace: any) => [
workspace.name,
workspace._id
workspace._id,
])
) as any
);
@@ -203,12 +209,12 @@ export default function Layout({ children }: LayoutProps) {
Object.fromEntries(
userWorkspaces.map((workspace: any) => [
workspace._id,
workspace.name
workspace.name,
])
)[
router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0]
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
]
);
}
@@ -224,16 +230,16 @@ export default function Layout({ children }: LayoutProps) {
workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !==
router.asPath
.split('/')
[router.asPath.split('/').length - 1].split('?')[0]
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
) {
router.push(
'/dashboard/' +
"/dashboard/" +
workspaceMapping[workspaceSelected as any] +
'?Development'
"?Development"
);
localStorage.setItem(
'projectData.id',
"projectData.id",
`${workspaceMapping[workspaceSelected as any]}`
);
}
@@ -257,7 +263,7 @@ export default function Layout({ children }: LayoutProps) {
<div>
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
PROJECT
{t("nav:menu.project")}
</div>
{workspaceList.length > 0 ? (
<Listbox
@@ -282,11 +288,11 @@ export default function Layout({ children }: LayoutProps) {
{workspaceList.length > 0 &&
menuItems.map(({ href, title, emoji }) => (
<li className="mt-0.5 mx-2" key={title}>
{router.asPath.split('/')[1] === href.split('/')[1] &&
(['project', 'billing', 'org', 'personal'].includes(
router.asPath.split('/')[2]
{router.asPath.split("/")[1] === href.split("/")[1] &&
(["project", "billing", "org", "personal"].includes(
router.asPath.split("/")[2]
)
? router.asPath.split('/')[2] === href.split('/')[2]
? router.asPath.split("/")[2] === href.split("/")[2]
: true) ? (
<div
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
@@ -297,7 +303,7 @@ export default function Layout({ children }: LayoutProps) {
</p>
{title}
</div>
) : router.asPath == '/noprojects' ? (
) : router.asPath == "/noprojects" ? (
<div
className={`flex p-2.5 text-white text-sm rounded`}
>
@@ -323,7 +329,7 @@ export default function Layout({ children }: LayoutProps) {
</ul>
</div>
<div className="w-full mt-40 mb-4 px-2">
{router.asPath.split('/')[1] === 'home' ? (
{router.asPath.split("/")[1] === "home" ? (
<div
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
>
@@ -334,12 +340,12 @@ export default function Layout({ children }: LayoutProps) {
Infisical Guide
<img
src={`/images/progress-${
totalOnboardingActionsDone == 0 ? '0' : ''
}${totalOnboardingActionsDone == 1 ? '14' : ''}${
totalOnboardingActionsDone == 2 ? '28' : ''
}${totalOnboardingActionsDone == 3 ? '43' : ''}${
totalOnboardingActionsDone == 4 ? '57' : ''
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`}
totalOnboardingActionsDone == 0 ? "0" : ""
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
totalOnboardingActionsDone == 2 ? "28" : ""
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
totalOnboardingActionsDone == 4 ? "57" : ""
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
height={58}
width={58}
alt="progress bar"
@@ -359,12 +365,12 @@ export default function Layout({ children }: LayoutProps) {
Infisical Guide
<img
src={`/images/progress-${
totalOnboardingActionsDone == 0 ? '0' : ''
}${totalOnboardingActionsDone == 1 ? '14' : ''}${
totalOnboardingActionsDone == 2 ? '28' : ''
}${totalOnboardingActionsDone == 3 ? '43' : ''}${
totalOnboardingActionsDone == 4 ? '57' : ''
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`}
totalOnboardingActionsDone == 0 ? "0" : ""
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
totalOnboardingActionsDone == 2 ? "28" : ""
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
totalOnboardingActionsDone == 4 ? "57" : ""
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
height={58}
width={58}
alt="progress bar"
@@ -394,9 +400,7 @@ export default function Layout({ children }: LayoutProps) {
className="text-gray-300 text-7xl mb-8"
/>
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
{' '}
To use Infisical, please log in through a device with larger
dimensions.{' '}
{` ${t("common:no-mobile")} `}
</p>
</div>
</>

View File

@@ -7,6 +7,7 @@ interface OverrideProps {
keyName: string;
value: string;
pos: number;
comment: string;
}
interface ToggleProps {
@@ -17,6 +18,7 @@ interface ToggleProps {
value: string;
pos: number;
id: string;
comment: string;
deleteOverride: (id: string) => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
@@ -46,6 +48,7 @@ export default function Toggle ({
value,
pos,
id,
comment,
deleteOverride,
sharedToHide,
setSharedToHide
@@ -55,13 +58,12 @@ export default function Toggle ({
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos });
addOverride({ id, keyName, value, pos, comment });
setSharedToHide([
...sharedToHide!,
id
])
} else {
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
deleteOverride(id);
}
setEnabled(!enabled);

View File

@@ -1,12 +1,7 @@
import { Fragment } from "react";
import { useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react";
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button";
const ActivateBotDialog = ({
@@ -16,6 +11,7 @@ const ActivateBotDialog = ({
handleBotActivate,
handleIntegrationOption
}) => {
const { t } = useTranslation();
const submit = async () => {
try {
@@ -64,18 +60,18 @@ const ActivateBotDialog = ({
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
Grant Infisical access to your secrets
{t("integrations:grant-access-to-secrets")}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="text-sm text-gray-500">
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
{t("integrations:why-infisical-needs-access")}
</p>
</div>
<div className="mt-6 max-w-max">
<Button
onButtonPressed={submit}
color="mineshaft"
text="Grant access"
text={t("integrations:grant-access-button")}
size="md"
/>
</div>

View File

@@ -1,4 +1,5 @@
import { Fragment, useState } from "react";
import { useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react";
import addIncidentContact from "~/pages/api/organization/addIncidentContact";
@@ -14,6 +15,7 @@ const AddIncidentContactDialog = ({
setIncidentContacts,
}) => {
let [incidentContactEmail, setIncidentContactEmail] = useState("");
const { t } = useTranslation();
const submit = () => {
setIncidentContacts(
@@ -59,17 +61,16 @@ const AddIncidentContactDialog = ({
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
Add an Incident Contact
{t("section-incident:add-dialog.title")}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="text-sm text-gray-500">
This contact will be notified in the unlikely event of a
severe incident.
{t("section-incident:add-dialog.description")}
</p>
</div>
<div className="max-h-28">
<InputField
label="Email"
label={t("common:email")}
onChangeHandler={setIncidentContactEmail}
type="varName"
value={incidentContactEmail}
@@ -81,7 +82,7 @@ const AddIncidentContactDialog = ({
<Button
onButtonPressed={submit}
color="mineshaft"
text="Add Incident Contact"
text={t("section-incident:add-dialog.add-incident")}
size="md"
/>
</div>

View File

@@ -1,5 +1,6 @@
import { Fragment, useState } from "react";
import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
@@ -15,6 +16,7 @@ const AddProjectMemberDialog = ({
setEmail,
}) => {
const router = useRouter();
const { t } = useTranslation();
return (
<div className="z-50">
@@ -49,48 +51,55 @@ const AddProjectMemberDialog = ({
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
Add a member to your project
{t("section-members:add-dialog.add-member-to-project")}
</Dialog.Title>
) : (
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
All the users in your organization are already invited.
{t("section-members:add-dialog.already-all-invited")}
</Dialog.Title>
)}
<div className="mt-2 mb-4">
{data?.length > 0 ? (
<div className="flex flex-col">
<p className="text-sm text-gray-500">
The user will receive an email with the instructions.
{t("section-members:add-dialog.user-will-email")}
</p>
<div className="">
<button
type="button"
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() =>
router.push("/settings/org/" + router.query.id)
}
>
If you are looking to add users to your org,
</button>
<button
type="button"
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() =>
router.push(
"/settings/org/" + router.query.id + "?invite"
)
}
>
click here.
</button>
<Trans
i18nKey="section-members:add-dialog.looking-add"
components={[
// eslint-disable-next-line react/jsx-key
<button
type="button"
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() =>
router.push(
"/settings/org/" + router.query.id
)
}
/>,
// eslint-disable-next-line react/jsx-key
<button
type="button"
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() =>
router.push(
"/settings/org/" +
router.query.id +
"?invite"
)
}
/>,
]}
/>
</div>
</div>
) : (
<p className="text-sm text-gray-500">
Add more users to the organization first.
{t("section-members:add-dialog.add-user-org-first")}
</p>
)}
</div>
@@ -110,7 +119,7 @@ const AddProjectMemberDialog = ({
<Button
onButtonPressed={submitModal}
color="mineshaft"
text="Add Member"
text={t("section-members:add-member")}
size="md"
/>
</div>
@@ -120,7 +129,7 @@ const AddProjectMemberDialog = ({
router.push("/settings/org/" + router.query.id)
}
color="mineshaft"
text="Add Users to Organization"
text={t("section-members:add-dialog.add-user-to-org")}
size="md"
/>
)}

View File

@@ -1,40 +1,42 @@
import { Fragment, useState } from 'react';
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react';
import nacl from 'tweetnacl';
import { Fragment, useState } from "react";
import { useTranslation } from "next-i18next";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Dialog, Transition } from "@headlessui/react";
import nacl from "tweetnacl";
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from '../../../public/data/frequentConstants';
import { envMapping } from "../../../public/data/frequentConstants";
import {
decryptAssymmetric,
encryptAssymmetric
} from '../../utilities/cryptography/crypto';
import Button from '../buttons/Button';
import InputField from '../InputField';
import ListBox from '../Listbox';
encryptAssymmetric,
} from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button";
import InputField from "../InputField";
import ListBox from "../Listbox";
const expiryMapping = {
'1 day': 86400,
'7 days': 604800,
'1 month': 2592000,
'6 months': 15552000,
'12 months': 31104000
"1 day": 86400,
"7 days": 604800,
"1 month": 2592000,
"6 months": 15552000,
"12 months": 31104000,
};
const AddServiceTokenDialog = ({
isOpen,
closeModal,
workspaceId,
workspaceName
workspaceName,
}) => {
const [serviceToken, setServiceToken] = useState('');
const [serviceTokenName, setServiceTokenName] = useState('');
const [serviceTokenEnv, setServiceTokenEnv] = useState('Development');
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation();
const generateServiceToken = async () => {
const latestFileKey = await getLatestFileKey({ workspaceId });
@@ -43,7 +45,7 @@ const AddServiceTokenDialog = ({
ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem('PRIVATE_KEY')
privateKey: localStorage.getItem("PRIVATE_KEY"),
});
// generate new public/private key pair
@@ -55,7 +57,7 @@ const AddServiceTokenDialog = ({
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey
privateKey,
});
let newServiceToken = await addServiceToken({
@@ -65,16 +67,16 @@ const AddServiceTokenDialog = ({
expiresIn: expiryMapping[serviceTokenExpiresIn],
publicKey,
encryptedKey,
nonce
nonce,
});
const serviceToken = newServiceToken + ',' + privateKey;
const serviceToken = newServiceToken + "," + privateKey;
setServiceToken(serviceToken);
};
function copyToClipboard() {
// Get the text field
var copyText = document.getElementById('serviceToken');
var copyText = document.getElementById("serviceToken");
// Select the text field
copyText.select();
@@ -91,8 +93,8 @@ const AddServiceTokenDialog = ({
const closeAddServiceTokenModal = () => {
closeModal();
setServiceTokenName('');
setServiceToken('');
setServiceTokenName("");
setServiceToken("");
};
return (
@@ -122,27 +124,26 @@ const AddServiceTokenDialog = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
{serviceToken == '' ? (
{serviceToken == "" ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
Add a service token for {workspaceName}
{t("section-token:add-dialog.title", {
target: workspaceName,
})}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
Specify the name, environment, and expiry period. When
a token is generated, you will only be able to see it
once before it disappears. Make sure to save it
somewhere.
{t("section-token:add-dialog.description")}
</p>
</div>
</div>
<div className="max-h-28 mb-2">
<InputField
label="Service Token Name"
label={t("section-token:add-dialog.name")}
onChangeHandler={setServiceTokenName}
type="varName"
value={serviceTokenName}
@@ -155,13 +156,13 @@ const AddServiceTokenDialog = ({
selected={serviceTokenEnv}
onChange={setServiceTokenEnv}
data={[
'Development',
'Staging',
'Production',
'Testing'
"Development",
"Staging",
"Production",
"Testing",
]}
isFull={true}
text="Environment: "
width="full"
text={`${t("common:environment")}: `}
/>
</div>
<div className="max-h-28">
@@ -169,14 +170,14 @@ const AddServiceTokenDialog = ({
selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn}
data={[
'1 day',
'7 days',
'1 month',
'6 months',
'12 months'
"1 day",
"7 days",
"1 month",
"6 months",
"12 months",
]}
isFull={true}
text="Expires in: "
width="full"
text={`${t("common:expired-in")}: `}
/>
</div>
<div className="max-w-max">
@@ -184,10 +185,10 @@ const AddServiceTokenDialog = ({
<Button
onButtonPressed={() => generateServiceToken()}
color="mineshaft"
text="Add Service Token"
textDisabled="Add Service Token"
text={t("section-token:add-dialog.add")}
textDisabled={t("section-token:add-dialog.add")}
size="md"
active={serviceTokenName == '' ? false : true}
active={serviceTokenName == "" ? false : true}
/>
</div>
</div>
@@ -198,13 +199,14 @@ const AddServiceTokenDialog = ({
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
Copy your service token
{t("section-token:add-dialog.copy-service-token")}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
Once you close this popup, you will never see your
service token again
{t(
"section-token:add-dialog.copy-service-token-description"
)}
</p>
</div>
</div>
@@ -234,7 +236,7 @@ const AddServiceTokenDialog = ({
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
Click to Copy
{t("common.click-to-copy")}
</span>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { useTranslation } from "next-i18next";
/**
* This is the text field where people can add comments to particular secrets.
*/
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
const { t } = useTranslation();
return <div className={`relative mt-4 px-4 pt-4`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
<textarea
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."
/>
</div>
}
export default CommentField;

View File

@@ -1,12 +1,13 @@
import { type ChangeEvent, type DragEvent, useState } from 'react';
import Image from 'next/image';
import { faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { type ChangeEvent, type DragEvent, useState } from "react";
import Image from "next/image";
import { useTranslation } from "next-i18next";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
import parse from '../utilities/file';
import guidGenerator from '../utilities/randomId';
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import parse from "../utilities/file";
import guidGenerator from "../utilities/randomId";
interface DropZoneProps {
// TODO: change Data type from any
@@ -26,8 +27,10 @@ const DropZone = ({
errorDragAndDrop,
setButtonReady,
keysExist,
numCurrentRows
numCurrentRows,
}: DropZoneProps) => {
const { t } = useTranslation();
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -43,7 +46,7 @@ const DropZone = ({
e.stopPropagation();
// set dropEffect to copy i.e copy of the source item
e.dataTransfer.dropEffect = 'copy';
e.dataTransfer.dropEffect = "copy";
};
const [loading, setLoading] = useState(false);
@@ -54,7 +57,7 @@ const DropZone = ({
setTimeout(() => setLoading(false), 5000);
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
e.dataTransfer.dropEffect = "copy";
const file = e.dataTransfer.files[0];
const reader = new FileReader();
@@ -69,7 +72,7 @@ const DropZone = ({
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs],
type: 'shared'
type: "shared",
};
});
setData(newData);
@@ -96,16 +99,16 @@ const DropZone = ({
reader.onload = (event) => {
if (event.target === null || event.target.result === null) return;
const { result } = event.target;
if (typeof result === 'string') {
if (typeof result === "string") {
const newData = result
.split('\n')
.split("\n")
.map((line: string, index: number) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: line.split('=')[0],
value: line.split('=').slice(1, line.split('=').length).join('='),
type: 'shared'
key: line.split("=")[0],
value: line.split("=").slice(1, line.split("=").length).join("="),
type: "shared",
};
});
setData(newData);
@@ -126,7 +129,7 @@ const DropZone = ({
</div>
) : keysExist ? (
<div
className="opacity-60 hover:opacity-100 duration-200 relative bg-bunker outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-gray-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
@@ -147,11 +150,9 @@ const DropZone = ({
<div className="flex flex-row">
<FontAwesomeIcon
icon={faUpload}
className="text-gray-300 text-3xl mr-6"
className="text-bunker-300 text-3xl mr-6"
/>
<p className="text-gray-300 mt-1">
Drag and drop your .env file here to add more keys.
</p>
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
</div>
{errorDragAndDrop ? (
<div className="mt-8 max-w-xl opacity-80">
@@ -170,7 +171,7 @@ const DropZone = ({
onDrop={handleDrop}
>
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
<p className="">Drag and drop your .env file here.</p>
<p className="">{t("common:drop-zone")}</p>
<input
id="fileSelect"
type="file"

View File

@@ -1,4 +1,5 @@
import { Fragment,useState } from 'react';
import { useTranslation } from "next-i18next";
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
@@ -10,6 +11,7 @@ import { Menu, Transition } from '@headlessui/react';
*/
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation();
return <Menu as="div" className="relative inline-block text-left">
<div>
@@ -51,8 +53,8 @@ const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: st
icon={faShuffle}
/>
<div className="text-sm justify-between flex flex-row w-full">
<p>Generate Random Hex</p>
<p>digits</p>
<p>{t("dashboard:sidebar.generate-random-hex")}</p>
<p>{t("dashboard:sidebar.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 ">

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { useTranslation } from "next-i18next";
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 CommentField from './CommentField';
import DashboardInputField from './DashboardInputField';
import GenerateSecretMenu from './GenerateSecretMenu';
@@ -15,6 +17,7 @@ interface SecretProps {
pos: number;
type: string;
id: string;
comment: string;
}
interface OverrideProps {
@@ -22,6 +25,7 @@ interface OverrideProps {
keyName: string;
value: string;
pos: number;
comment: string;
}
interface SideBarProps {
@@ -29,6 +33,7 @@ interface SideBarProps {
data: SecretProps[];
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyComment: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void;
buttonReady: boolean;
@@ -56,6 +61,7 @@ const SideBar = ({
data,
modifyKey,
modifyValue,
modifyComment,
addOverride,
deleteOverride,
buttonReady,
@@ -64,29 +70,30 @@ const SideBar = ({
setSharedToHide
}: SideBarProps) => {
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
<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>
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.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>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0].pos}
value={data[0].key}
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>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
@@ -100,21 +107,22 @@ const SideBar = ({
</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.
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
{t("dashboard:sidebar.personal-explanation")}
</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>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0].key}
value={data[0].value}
pos={data[0].pos}
id={data[0].id}
keyName={data[0]?.key}
value={data[0]?.value}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
@@ -124,13 +132,13 @@ const SideBar = ({
<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}
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} />
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
</div>
</div>
</div>
@@ -143,21 +151,12 @@ const SideBar = ({
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>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div>
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button
text="Save Changes"
text={String(t("common:save-changes"))}
onButtonPressed={savePush}
color="primary"
size="md"

View File

@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "next-i18next";
import CloudIntegration from "./CloudIntegration";
@@ -24,12 +25,14 @@ const CloudIntegrationSection = ({
integrationOptionPress,
integrationAuths
}: Props) => {
const { t } = useTranslation();
return (
<>
<div className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}>
<h1 className="font-semibold text-3xl">Cloud Integrations</h1>
<h1 className="font-semibold text-3xl">{t("integrations:cloud-integrations")}</h1>
<p className="text-base text-gray-400">
Click on an integration to begin syncing secrets to it.
{t("integrations:click-to-start")}
</p>
</div>
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl">

View File

@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "next-i18next";
import FrameworkIntegration from "./FrameworkIntegration";
@@ -15,24 +16,26 @@ interface Props {
}
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
return (
<>
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">Framework Integrations</h1>
<p className="text-base text-gray-400">
Click on a framework to get the setup instructions.
</p>
</div>
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
{frameworks.map((framework) => (
<FrameworkIntegration
framework={framework}
key={`framework-integration-${framework.slug}`}
/>
))}
</div>
</>
);
const { t } = useTranslation();
return (
<>
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">{t("integrations:framework-integrations")}</h1>
<p className="text-base text-gray-400">
{t("integrations:click-to-setup")}
</p>
</div>
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
{frameworks.map((framework) => (
<FrameworkIntegration
framework={framework}
key={`framework-integration-${framework.slug}`}
/>
))}
</div>
</>
);
}
export default FrameworkIntegrationSection;

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react/jsx-key */
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';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import { Fragment, useEffect, useMemo, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { TFunction, useTranslation } from "next-i18next";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faBook,
@@ -12,39 +12,39 @@ import {
faEnvelope,
faGear,
faPlus,
faRightFromBracket
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
faRightFromBracket,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, Transition } from "@headlessui/react";
import logout from '~/pages/api/auth/Logout';
import getOrganization from '~/pages/api/organization/GetOrg';
import getOrganizations from '~/pages/api/organization/getOrgs';
import getUser from '~/pages/api/user/getUser';
import logout from "~/pages/api/auth/Logout";
import guidGenerator from '../utilities/randomId';
import getOrganization from "../../pages/api/organization/GetOrg";
import getOrganizations from "../../pages/api/organization/getOrgs";
import getUser from "../../pages/api/user/getUser";
import guidGenerator from "../utilities/randomId";
const supportOptions = [
const supportOptions = (t: TFunction) => [
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
'Join Slack Forum',
'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g'
t("nav:support.slack"),
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
],
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
'Read Docs',
'https://infisical.com/docs/getting-started/introduction'
t("nav:support.docs"),
"https://infisical.com/docs/getting-started/introduction",
],
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
'Open a GitHub Issue',
'https://github.com/Infisical/infisical-cli/issues'
t("nav:support.issue"),
"https://github.com/Infisical/infisical-cli/issues",
],
[
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
'Send us an Email',
'mailto:support@infisical.com'
]
t("nav:support.email"),
"mailto:support@infisical.com",
],
];
export interface ICurrentOrg {
@@ -68,6 +68,10 @@ export default function Navbar() {
const [orgs, setOrgs] = useState([]);
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
const { t } = useTranslation();
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
useEffect(() => {
(async () => {
const userData = await getUser();
@@ -75,16 +79,16 @@ export default function Navbar() {
const orgsData = await getOrganizations();
setOrgs(orgsData);
const currentOrg = await getOrganization({
orgId: String(localStorage.getItem('orgData.id'))
orgId: String(localStorage.getItem("orgData.id")),
});
setCurrentOrg(currentOrg);
})();
}, []);
const closeApp = async () => {
console.log('Logging out...');
console.log("Logging out...");
await logout();
router.push('/login');
router.push("/login");
};
return (
@@ -119,18 +123,17 @@ export default function Navbar() {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
{supportOptions.map((option) => (
// eslint-disable-next-line react/jsx-no-target-blank
{supportOptionsList.map(([icon, text, url]) => (
<a
key={guidGenerator()}
target="_blank"
rel="noopener"
href={String(option[2])}
rel="noopener noreferrer"
href={String(url)}
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
>
<div 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">
{option[0]}
<div className="text-sm">{option[1]}</div>
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
))}
@@ -159,11 +162,11 @@ export default function Navbar() {
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 ">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
SIGNED IN AS
{t("nav:user.signed-in-as")}
</div>
<div
onClick={() =>
router.push('/settings/personal/' + router.query.id)
router.push("/settings/personal/" + router.query.id)
}
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
>
@@ -173,11 +176,11 @@ export default function Navbar() {
<div className="flex items-center justify-between w-full">
<div>
<p className="text-gray-300 px-2 pt-1 text-sm">
{' '}
{" "}
{user?.firstName} {user?.lastName}
</p>
<p className="text-gray-400 px-2 pb-1 text-xs">
{' '}
{" "}
{user?.email}
</p>
</div>
@@ -190,11 +193,11 @@ export default function Navbar() {
</div>
<div className="px-2 pt-2">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
CURRENT ORGANIZATION
{t("nav:user.current-organization")}
</div>
<div
onClick={() =>
router.push('/settings/org/' + router.query.id)
router.push("/settings/org/" + router.query.id)
}
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
>
@@ -217,7 +220,7 @@ export default function Navbar() {
>
<div
onClick={() =>
router.push('/settings/billing/' + router.query.id)
router.push("/settings/billing/" + router.query.id)
}
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
>
@@ -225,7 +228,7 @@ export default function Navbar() {
className="text-lg pl-1.5 pr-3"
icon={faCoins}
/>
<div className="text-sm">Usage & Billing</div>
<div className="text-sm">{t("nav:user.usage-billing")}</div>
</div>
</button>
<button
@@ -235,7 +238,7 @@ export default function Navbar() {
<div
onClick={() =>
router.push(
'/settings/org/' + router.query.id + '?invite'
"/settings/org/" + router.query.id + "?invite"
)
}
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
@@ -243,26 +246,26 @@ export default function Navbar() {
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="ml-1" />
</span>
<div className="text-sm ml-1">Invite Members</div>
<div className="text-sm ml-1">{t("nav:user.invite")}</div>
</div>
</button>
</div>
{orgs?.length > 1 && (
<div className="px-1 pt-1">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
OTHER ORGANIZATIONS
{t("nav:user.other-organizations")}
</div>
<div className="flex flex-col items-start px-1 mt-3 mb-2">
{orgs
.filter(
(org: { _id: string }) =>
org._id != localStorage.getItem('orgData.id')
org._id != localStorage.getItem("orgData.id")
)
.map((org: { _id: string; name: string }) => (
<div
key={guidGenerator()}
onClick={() => {
localStorage.setItem('orgData.id', org._id);
localStorage.setItem("orgData.id", org._id);
router.reload();
}}
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
@@ -287,8 +290,8 @@ export default function Navbar() {
onClick={closeApp}
className={`${
active
? 'bg-red font-semibold text-white'
: 'text-gray-400'
? "bg-red font-semibold text-white"
: "text-gray-400"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<div className="relative flex justify-start items-center cursor-pointer select-none">
@@ -296,7 +299,7 @@ export default function Navbar() {
className="text-lg ml-1.5 mr-3"
icon={faRightFromBracket}
/>
Log Out
{t("common:logout")}
</div>
</button>
)}

View File

@@ -16,12 +16,19 @@ export default class SecurityClient {
const req = new Request(resource, options);
if (this.#token == '') {
this.setToken(await token());
try {
// TODO: This should be moved to a context to do it only once when app loads
// this try catch saves route guard from a stuck state
this.setToken(await token());
} catch (error) {
console.error("Unauthorized access");
}
}
if (this.#token) {
req.headers.set('Authorization', 'Bearer ' + this.#token);
return fetch(req);
}
return fetch(req);
}
}

View File

@@ -52,6 +52,7 @@ const attemptLogin = async (
// if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } =
await login2(email, clientProof);
SecurityClient.setToken(token);
const privateKey = Aes256Gcm.decrypt({
@@ -108,15 +109,16 @@ const attemptLogin = async (
if (isSignUp) {
await pushKeys({
obj: {
DATABASE_URL: [
sDATABASE_URL: [
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
'personal'
'This is an example of secret referencing.'
],
DB_USERNAME: ['user1234', 'personal'],
DB_PASSWORD: ['example_password', 'personal'],
TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'],
WEBSITE_URL: ['http://localhost:3000', 'shared'],
STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared']
sDB_USERNAME: ['OVERRIDE_THIS', ''],
sDB_PASSWORD: ['OVERRIDE_THIS', ''],
pDB_USERNAME: ['user1234', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
pDB_PASSWORD: ['example_password', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
sWEBSITE_URL: ['http://localhost:3000', ''],
},
workspaceId: projectToLogin,
env: 'Development'

View File

@@ -10,6 +10,14 @@ const {
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface SecretProps {
key: string;
value: string;
type: 'personal' | 'shared';
comment: string;
id: string;
}
interface Props {
env: keyof typeof envMapping;
setFileState: any;
@@ -34,12 +42,12 @@ const getSecretsForProject = async ({
} catch (error) {
console.log('ERROR: Not able to access the latest file');
}
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
const tempFileState: SecretProps[] = [];
if (file.key) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
@@ -64,10 +72,25 @@ const getSecretsForProject = async ({
tag: secretPair.secretValue.tag,
key
});
let plainTextComment;
if (secretPair.secretComment.ciphertext) {
plainTextComment = decryptSymmetric({
ciphertext: secretPair.secretComment.ciphertext,
iv: secretPair.secretComment.iv,
tag: secretPair.secretComment.tag,
key
});
} else {
plainTextComment = "";
}
tempFileState.push({
id: secretPair._id,
key: plainTextKey,
value: plainTextValue,
type: secretPair.type
type: secretPair.type,
comment: plainTextComment
});
});
}
@@ -76,22 +99,24 @@ const getSecretsForProject = async ({
setData(
tempFileState.map((line, index) => {
return {
id: guidGenerator(),
id: line['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type']
type: line['type'],
comment: line['comment']
};
})
);
return tempFileState.map((line, index) => {
return {
id: guidGenerator(),
id: line['id'],
pos: index,
key: line['key'],
value: line['value'],
type: line['type']
type: line['type'],
comment: line['comment']
};
});
} catch (error) {

View File

@@ -47,9 +47,9 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
const secrets = Object.keys(obj).map((key) => {
// encrypt key
const {
ciphertext: ciphertextKey,
iv: ivKey,
tag: tagKey,
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
} = encryptSymmetric({
plaintext: key.slice(1),
key: randomBytes,
@@ -57,25 +57,39 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
// encrypt value
const {
ciphertext: ciphertextValue,
iv: ivValue,
tag: tagValue,
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][0],
key: randomBytes,
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
} = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][1],
key: randomBytes,
});
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return {
ciphertextKey,
ivKey,
tagKey,
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
ciphertextValue,
ivValue,
tagValue,
hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
type: visibility,
};
});

View File

@@ -0,0 +1,52 @@
import { GetServerSideProps, GetStaticProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
const DefaultNamespaces = ["common", "nav"];
type GetTranslatedStaticProps = (
namespaces: string[],
getStaticProps?: GetStaticProps<any>
) => GetStaticProps;
type GetTranslatedServerSideProps = (
namespaces: string[],
getStaticProps?: GetServerSideProps<any>
) => GetServerSideProps;
export const getTranslatedStaticProps: GetTranslatedStaticProps =
(namespaces, getStaticProps) =>
async ({ locale, ...context }) => {
let staticProps = { props: {} };
if (typeof getStaticProps === "function") {
staticProps = (await getStaticProps(context)) as any;
}
return {
...staticProps,
props: {
...(await serverSideTranslations(locale ?? "en", [
...DefaultNamespaces,
...namespaces,
])),
...staticProps.props,
},
};
};
export const getTranslatedServerSideProps: GetTranslatedServerSideProps =
(namespaces, getServerSideProps) =>
async ({ locale, ...context }) => {
let staticProps = { props: {} };
if (typeof getServerSideProps === "function") {
staticProps = (await getServerSideProps(context)) as any;
}
return {
...staticProps,
props: {
...(await serverSideTranslations(locale ?? "en", [
...DefaultNamespaces,
...namespaces,
])),
...staticProps.props,
},
};
};

View File

@@ -16,3 +16,8 @@ export const publicPaths = [
`/terms`,
`/subprocessors`,
];
export const languageMap = {
en: "English",
ko: "한국어",
};

Some files were not shown because too many files have changed in this diff Show More