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
+1
View File
@@ -41,6 +41,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli - run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith - name: Publish to CloudSmith
+2
View File
@@ -12,6 +12,8 @@ node_modules
.DS_Store .DS_Store
/dist /dist
/completions/
/manpages/
# frontend # frontend
+53 -5
View File
@@ -6,6 +6,11 @@
# - cd cli && go mod tidy # - cd cli && go mod tidy
# # you may remove this if you don't need go generate # # you may remove this if you don't need go generate
# - cd cli && go generate ./... # - cd cli && go generate ./...
before:
hooks:
- ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh
builds: builds:
- id: darwin-build - id: darwin-build
binary: infisical binary: infisical
@@ -30,13 +35,13 @@ builds:
- openbsd - openbsd
- windows - windows
goarch: goarch:
- 386 - "386"
- amd64 - amd64
- arm - arm
- arm64 - arm64
goarm: goarm:
- 6 - "6"
- 7 - "7"
ignore: ignore:
- goos: windows - goos: windows
goarch: "386" goarch: "386"
@@ -44,6 +49,16 @@ builds:
goarch: "386" goarch: "386"
dir: ./cli dir: ./cli
archives:
- format_overrides:
- goos: windows
format: zip
files:
- README*
- LICENSE*
- manpages/*
- completions/*
release: release:
replace_existing_draft: true replace_existing_draft: true
mode: 'replace' mode: 'replace'
@@ -85,13 +100,22 @@ nfpms:
homepage: https://infisical.com/ homepage: https://infisical.com/
maintainer: Infisical, Inc maintainer: Infisical, Inc
description: The offical Infisical CLI description: The offical Infisical CLI
license: Apache 2.0 license: MIT
formats: formats:
- rpm - rpm
- deb - deb
- apk - apk
- archlinux - archlinux
bindir: /usr/bin 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: scoop:
bucket: bucket:
owner: Infisical owner: Infisical
@@ -101,7 +125,31 @@ scoop:
email: [email protected] email: [email protected]
homepage: "https://infisical.com" homepage: "https://infisical.com"
description: "The official Infisical CLI" 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 <[email protected]>
license: MIT
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://[email protected]/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: # dockers:
# - dockerfile: goreleaser.dockerfile # - dockerfile: goreleaser.dockerfile
# goos: linux # goos: linux
+22 -6
View File
@@ -41,7 +41,7 @@
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure - **[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.) - **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
- **Personal/Shared** scoping for environment variables - **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 - 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon) - 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation** - 🔜 **Automatic Secret Rotation**
@@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis
</tr> </tr>
<tr> <tr>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com"> <a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
✔️ Ruby on Rails ✔️ Vue
</a> </a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com"> <a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
✔️ Vue ✔️ Ruby on Rails
</a> </a>
</td> </td>
</tr> </tr>
@@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis
</a> </a>
</td> </td>
</tr> </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> </tbody>
</table> </table>
@@ -321,4 +331,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- 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).
+6 -6
View File
@@ -6677,9 +6677,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
@@ -17175,9 +17175,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
"dev": true "dev": true
}, },
"jsonwebtoken": { "jsonwebtoken": {
+48 -41
View File
@@ -14,28 +14,31 @@ import { apiLimiter } from './helpers/rateLimiter';
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secret as eeSecretRouter secret as eeSecretRouter
} from './ee/routes'; } from './ee/routes/v1';
import { import {
signup as signupRouter, signup as v1SignupRouter,
auth as authRouter, auth as v1AuthRouter,
bot as botRouter, bot as v1BotRouter,
organization as organizationRouter, organization as v1OrganizationRouter,
workspace as workspaceRouter, workspace as v1WorkspaceRouter,
membershipOrg as membershipOrgRouter, membershipOrg as v1MembershipOrgRouter,
membership as membershipRouter, membership as v1MembershipRouter,
key as keyRouter, key as v1KeyRouter,
inviteOrg as inviteOrgRouter, inviteOrg as v1InviteOrgRouter,
user as userRouter, user as v1UserRouter,
userAction as userActionRouter, userAction as v1UserActionRouter,
secret as secretRouter, secret as v1SecretRouter,
serviceToken as serviceTokenRouter, serviceToken as v1ServiceTokenRouter,
password as passwordRouter, serviceTokenData as v1ServiceTokenDataRouter,
stripe as stripeRouter, password as v1PasswordRouter,
integration as integrationRouter, stripe as v1StripeRouter,
integrationAuth as integrationAuthRouter, integration as v1IntegrationRouter,
serviceTokenData as serviceTokenDataRouter integrationAuth as v1IntegrationAuthRouter
} from './routes'; } from './routes/v1';
import {
secret as v2SecretRouter,
workspace as v2WorkspaceRouter
} from './routes/v2';
import { getLogger } from './utils/logger'; import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors'; import { RouteNotFoundError } from './utils/errors';
@@ -64,29 +67,33 @@ if (NODE_ENV === 'production') {
app.use(helmet()); app.use(helmet());
} }
// /ee routers // (EE) routes
app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/workspace', eeWorkspaceRouter);
// routers // v1 routes
app.use('/api/v1/signup', signupRouter); app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', authRouter); app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', botRouter); app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', userRouter); app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', userActionRouter); app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', organizationRouter); app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', workspaceRouter); app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter); app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', membershipRouter); app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', keyRouter); app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter); app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', secretRouter); app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', serviceTokenRouter); app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', passwordRouter); app.use('/api/v1/service-token-data', v1ServiceTokenDataRouter);
app.use('/api/v1/stripe', stripeRouter); app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/integration', integrationRouter); app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter); app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/service-token-data', serviceTokenDataRouter); 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 //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{ app.use((req, res, next)=>{
@@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User } from '../models'; import { User } from '../../models';
import { createToken, issueTokens, clearTokens } from '../helpers/auth'; import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import { import {
NODE_ENV, NODE_ENV,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_SECRET JWT_REFRESH_SECRET
} from '../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../models'; import { Bot, BotKey } from '../../models';
import { createBot } from '../helpers/bot'; import { createBot } from '../../helpers/bot';
interface BotKey { interface BotKey {
encryptedKey: string; encryptedKey: string;
@@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
import * as userActionController from './userActionController'; import * as userActionController from './userActionController';
import * as userController from './userController'; import * as userController from './userController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
export { export {
authController, authController,
@@ -31,5 +32,6 @@ export {
stripeController, stripeController,
userActionController, userActionController,
userController, userController,
workspaceController workspaceController,
serviceTokenDataController
}; };
@@ -2,10 +2,10 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import axios from 'axios'; import axios from 'axios';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../models'; import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables'; import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
import { IntegrationService } from '../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../integrations'; import { getApps, revokeAccess } from '../../integrations';
export const getIntegrationOptions = async ( export const getIntegrationOptions = async (
req: Request, req: Request,
@@ -1,9 +1,9 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Integration, Bot, BotKey } from '../models'; import { Integration, Bot, BotKey } from '../../models';
import { EventService } from '../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../events'; import { eventPushSecrets } from '../../events';
interface Key { interface Key {
encryptedKey: string; encryptedKey: string;
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Key } from '../models'; import { Key } from '../../models';
import { findMembership } from '../helpers/membership'; import { findMembership } from '../../helpers/membership';
import { GRANTED } from '../variables'; import { GRANTED } from '../../variables';
/** /**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@@ -1,13 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../models'; import { Membership, MembershipOrg, User, Key } from '../../models';
import { import {
findMembership, findMembership,
deleteMembership as deleteMember deleteMembership as deleteMember
} from '../helpers/membership'; } from '../../helpers/membership';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../config'; import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables'; import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables';
/** /**
* Check that user is a member of workspace with id [workspaceId] * Check that user is a member of workspace with id [workspaceId]
@@ -1,14 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto'; import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../models'; import { MembershipOrg, Organization, User, Token } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../helpers/signup'; import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// TODO // TODO
let membershipToChangeRole; let membershipToChangeRole;
try { // try {
} catch (err) { // } catch (err) {
Sentry.setUser({ email: req.user.email }); // Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); // Sentry.captureException(err);
return res.status(400).send({ // return res.status(400).send({
message: 'Failed to change organization membership role' // message: 'Failed to change organization membership role'
}); // });
} // }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
@@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
const { email, code } = req.body; const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey'); 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({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
@@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
code 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) { if (!user) {
// initialize user account // initialize user account
user = await new User({ user = await new User({
@@ -6,7 +6,7 @@ import {
STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_CARD_AUTH STRIPE_PRODUCT_CARD_AUTH
} from '../config'; } from '../../config';
import Stripe from 'stripe'; import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, { const stripe = new Stripe(STRIPE_SECRET_KEY, {
@@ -18,10 +18,10 @@ import {
Organization, Organization,
Workspace, Workspace,
IncidentContactOrg IncidentContactOrg
} from '../models'; } from '../../models';
import { createOrganization as create } from '../helpers/organization'; import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../helpers/membershipOrg'; import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../variables'; import { OWNER, ACCEPTED } from '../../variables';
const productToPriceMap = { const productToPriceMap = {
starter: STRIPE_PRODUCT_STARTER, starter: STRIPE_PRODUCT_STARTER,
@@ -1,13 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto'; import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../models'; import { User, Token, BackupPrivateKey } from '../../models';
import { checkEmailVerification } from '../helpers/signup'; import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
const clientPublicKeys: any = {}; const clientPublicKeys: any = {};
@@ -1,16 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Key, Secret } from '../models'; import { Key, Secret } from '../../models';
import { import {
pushSecrets as push, v1PushSecrets as push,
pullSecrets as pull, pullSecrets as pull,
reformatPullSecrets reformatPullSecrets
} from '../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../services'; import { EventService } from '../../services';
import { ENV_SET } from '../variables'; import { ENV_SET } from '../../variables';
import { postHogClient } from '../services'; import { postHogClient } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -21,6 +21,10 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
} }
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ServiceToken } from '../models'; import { ServiceToken } from '../../models';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../variables'; import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../config'; import { JWT_SERVICE_SECRET } from '../../config';
/** /**
* Return service token on request * Return service token on request
@@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../config';
* @returns * @returns
*/ */
export const getServiceToken = async (req: Request, res: Response) => { export const getServiceToken = async (req: Request, res: Response) => {
// get service token
return res.status(200).send({ return res.status(200).send({
serviceToken: req.serviceToken serviceToken: req.serviceToken
}); });
@@ -74,12 +73,3 @@ export const createServiceToken = async (req: Request, res: Response) => {
token 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.
*
*
*
*/
@@ -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
});
}
@@ -1,15 +1,15 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { User, MembershipOrg } from '../models'; import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../helpers/user'; import { completeAccount } from '../../helpers/user';
import { import {
sendEmailVerification, sendEmailVerification,
checkEmailVerification, checkEmailVerification,
initializeDefaultOrg initializeDefaultOrg
} from '../helpers/signup'; } from '../../helpers/signup';
import { issueTokens, createToken } from '../helpers/auth'; import { issueTokens, createToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../variables'; import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios'; import axios from 'axios';
/** /**
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { UserAction } from '../models'; import { UserAction } from '../../models';
/** /**
* Add user action [action] * Add user action [action]
@@ -8,14 +8,14 @@ import {
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken, ServiceToken,
ServiceTokenData, ServiceTokenData
} from '../models'; } from '../../models';
import { import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork deleteWorkspace as deleteWork
} from '../helpers/workspace'; } from '../../helpers/workspace';
import { addMemberships } from '../helpers/membership'; import { addMemberships } from '../../helpers/membership';
import { ADMIN, COMPLETED, GRANTED } from '../variables'; import { ADMIN, COMPLETED, GRANTED } from '../../variables';
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
+5
View File
@@ -0,0 +1,5 @@
import * as workspaceController from './workspaceController';
export {
workspaceController
}
@@ -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
});
}
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SecretVersion } from '../models'; import { SecretVersion } from '../../models';
/** /**
* Return secret versions for secret with id [secretId] * Return secret versions for secret with id [secretId]
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; 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, { const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01' apiVersion: '2022-08-01'
}); });
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../models'; import { SecretSnapshot } from '../../models';
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
@@ -4,14 +4,16 @@ import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../../middleware';
import { body, query, param } from 'express-validator'; import { body, query, param } from 'express-validator';
import { secretController } from '../controllers'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { stripeController } from '../controllers'; import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook); router.post('/webhook', stripeController.handleWebhook);
@@ -4,14 +4,16 @@ import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../../middleware';
import { param, query } from 'express-validator'; import { param, query } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../variables'; import { ADMIN, MEMBER, GRANTED } from '../../../variables';
import { workspaceController } from '../controllers'; import { workspaceController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/secret-snapshots', '/:workspaceId/secret-snapshots',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
+62 -3
View File
@@ -1,15 +1,69 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import { import {
User User,
ServiceTokenData
} from '../models'; } from '../models';
import { import {
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET JWT_REFRESH_SECRET,
SALT_ROUNDS
} from '../config'; } 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] * Return newly issued (JWT) auth and refresh tokens to user with id [userId]
* @param {Object} obj * @param {Object} obj
@@ -99,4 +153,9 @@ const createToken = ({
} }
}; };
export { createToken, issueTokens, clearTokens }; export {
attachAuthPayload,
createToken,
issueTokens,
clearTokens
};
-1
View File
@@ -12,7 +12,6 @@ import {
decryptSymmetric, decryptSymmetric,
decryptAsymmetric decryptAsymmetric
} from '../utils/crypto'; } from '../utils/crypto';
import { decryptSecrets } from '../helpers/secret';
import { ENCRYPTION_KEY } from '../config'; import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables'; import { SECRET_SHARED } from '../variables';
+1 -1
View File
@@ -26,7 +26,7 @@ const validateMembership = async ({
membership = await Membership.findOne({ membership = await Membership.findOne({
user: userId, user: userId,
workspace: workspaceId workspace: workspaceId
}); }).populate("workspace");
if (!membership) throw new Error('Failed to find membership'); if (!membership) throw new Error('Failed to find membership');
+294 -76
View File
@@ -14,9 +14,8 @@ import {
} from '../ee/helpers/secret'; } from '../ee/helpers/secret';
import { decryptSymmetric } from '../utils/crypto'; import { decryptSymmetric } from '../utils/crypto';
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
import { LICENSE_KEY } from '../config';
interface PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
@@ -25,9 +24,29 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; 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 { interface Update {
[index: string]: any; [index: string]: any;
} }
@@ -45,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push * @param {Object[]} obj.secrets - secrets to push
*/ */
const pushSecrets = async ({ const v1PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -54,7 +73,7 @@ const pushSecrets = async ({
userId: string; userId: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
secrets: PushSecret[]; secrets: V1PushSecret[];
}): Promise<void> => { }): Promise<void> => {
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
@@ -93,8 +112,9 @@ const pushSecrets = async ({
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) { if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
// case: filter secrets where value changed || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed
return true; return true;
} }
@@ -113,14 +133,22 @@ const pushSecrets = async ({
ciphertextValue, ciphertextValue,
ivValue, ivValue,
tagValue, tagValue,
hashValue hashValue,
ciphertextComment,
ivComment,
tagComment,
hashComment
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; } = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = { const update: Update = {
secretValueCiphertext: ciphertextValue, secretValueCiphertext: ciphertextValue,
secretValueIV: ivValue, secretValueIV: ivValue,
secretValueTag: tagValue, secretValueTag: tagValue,
secretValueHash: hashValue secretValueHash: hashValue,
secretCommentCiphertext: ciphertextComment,
secretCommentIV: ivComment,
secretCommentTag: tagComment,
secretCommentHash: hashComment,
} }
if (!s.version) { if (!s.version) {
@@ -192,7 +220,254 @@ const pushSecrets = async ({
secretValueCiphertext: s.ciphertextValue, secretValueCiphertext: s.ciphertextValue,
secretValueIV: s.ivValue, secretValueIV: s.ivValue,
secretValueTag: s.tagValue, 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') { if (toAdd[idx].type === 'personal') {
@@ -315,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
iv: s.secretValueIV, iv: s.secretValueIV,
tag: s.secretValueTag, tag: s.secretValueTag,
hash: s.secretValueHash hash: s.secretValueHash
},
secretComment: {
workspace: s.workspace,
ciphertext: s.secretCommentCiphertext,
iv: s.secretCommentIV,
tag: s.secretCommentTag,
hash: s.secretCommentHash
} }
})); }));
} catch (err) { } catch (err) {
@@ -326,73 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets; 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 { export {
pushSecrets, v1PushSecrets,
v2PushSecrets,
pullSecrets, pullSecrets,
reformatPullSecrets, reformatPullSecrets
decryptSecrets
}; };
-1
View File
@@ -21,7 +21,6 @@ import {
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB CLIENT_SECRET_GITHUB
} from '../config'; } from '../config';
import { user } from '../routes';
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
+51 -24
View File
@@ -1,6 +1,9 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; 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 { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; 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), * Validate if token on request is valid (e.g. not expired) for various auth modes:
* if there is an associated user, and if that user is fully setup. * - If token is a JWT token, then check if there is an associated user
* @param req - express request object * and if user is fully setup.
* @param res - express response object * - If token is a service token (st), then check if there is associated
* @param next - express next function * service token data.
* @param {Object} obj
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
* @returns * @returns
*/ */
const requireAuth = async (req: Request, res: Response, next: NextFunction) => { const requireAuth = ({
// JWT authentication middleware acceptedAuthModes = ['jwt']
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.`})) acceptedAuthModes: string[];
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'})) 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'}))
const decodedToken = <jwt.UserIDJwtPayload>( // validate auth mode
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) let authMode;
); switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) {
case 'st':
authMode = 'st';
break;
default:
authMode = 'jwt';
break;
}
const user = await User.findOne({ if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
_id: decodedToken.userId
}).select('+publicKey');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) // attach auth request payload
if (!user?.publicKey) const payload = await attachAuthPayload({
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) authTokenValue: AUTH_TOKEN_VALUE
});
req.user = user; switch (authMode) {
return next(); case 'st':
}; req.serviceTokenData = payload;
break;
default:
req.user = payload;
break;
}
return next();
}
}
export default requireAuth; export default requireAuth;
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ServiceToken, ServiceTokenData } from '../models'; import { ServiceToken, ServiceTokenData } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors'; import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -15,6 +15,7 @@ const requireServiceTokenDataAuth = ({
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const { serviceTokenDataId } = req[location];
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId) .findById(req[location].serviceTokenDataId)
@@ -24,12 +25,15 @@ const requireServiceTokenDataAuth = ({
return next(AccountNotFoundError({message: 'Failed to locate service token data'})); return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
} }
await validateMembership({ if (req.user) {
userId: req.user._id.toString(), // case: jwt auth
workspaceId: serviceTokenData.workspace.toString(), await validateMembership({
acceptedRoles, userId: req.user._id.toString(),
acceptedStatuses workspaceId: serviceTokenData.workspace.toString(),
}); acceptedRoles,
acceptedStatuses
});
}
req.serviceTokenData = serviceTokenData; req.serviceTokenData = serviceTokenData;
+22 -11
View File
@@ -8,8 +8,8 @@ type req = 'params' | 'body' | 'query';
* Validate if user on request is a member with proper roles for workspace * Validate if user on request is a member with proper roles for workspace
* on request params. * on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses * @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 * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/ */
const requireWorkspaceAuth = ({ const requireWorkspaceAuth = ({
@@ -22,17 +22,28 @@ const requireWorkspaceAuth = ({
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// workspace authorization middleware
try { try {
const membership = await validateMembership({ const { workspaceId } = req[location];
userId: req.user._id.toString(),
workspaceId: req[location].workspaceId,
acceptedRoles,
acceptedStatuses
});
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(); return next();
} catch (err) { } catch (err) {
+20
View File
@@ -23,6 +23,10 @@ export interface ISecret {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -82,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
secretValueHash: { secretValueHash: {
type: String, type: String,
required: true required: true
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
} }
}, },
{ {
+8 -1
View File
@@ -5,6 +5,7 @@ export interface IServiceTokenData {
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; // TODO: adapt to upcoming environment id environment: string; // TODO: adapt to upcoming environment id
user: Types.ObjectId;
expiresAt: Date; expiresAt: Date;
prefix: string; prefix: string;
serviceTokenHash: string; serviceTokenHash: string;
@@ -28,6 +29,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
type: String, type: String,
required: true required: true
}, },
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: { expiresAt: {
type: Date type: Date
}, },
@@ -38,7 +44,8 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
serviceTokenHash: { serviceTokenHash: {
type: String, type: String,
unique: true, unique: true,
required: true required: true,
select: true
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
-144
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;
-8
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;
@@ -1,9 +1,9 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../controllers'; import { authController } from '../../controllers/v1';
import { loginLimiter } from '../helpers/rateLimiter'; import { loginLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken); router.post('/token', validateRequest, authController.getNewToken);
@@ -25,7 +25,19 @@ router.post(
authController.login2 authController.login2
); );
router.post('/logout', requireAuth, authController.logout); router.post(
router.post('/checkAuth', requireAuth, authController.checkAuth); '/logout',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.logout
);
router.post(
'/checkAuth',
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.checkAuth
);
export default router; export default router;
@@ -6,13 +6,15 @@ import {
requireBotAuth, requireBotAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { botController } from '../controllers'; import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -24,7 +26,9 @@ router.get(
router.patch( router.patch(
'/:botId/active', '/:botId/active',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireBotAuth({ requireBotAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -4,14 +4,16 @@ import {
requireAuth, requireAuth,
requireIntegrationAuth, requireIntegrationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../controllers'; import { integrationController } from '../../controllers/v1';
router.patch( router.patch(
'/:integrationId', '/:integrationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -29,7 +31,9 @@ router.patch(
router.delete( router.delete(
'/:integrationId', '/:integrationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -6,19 +6,23 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { integrationAuthController } from '../controllers'; import { integrationAuthController } from '../../controllers/v1';
router.get( router.get(
'/integration-options', '/integration-options',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
integrationAuthController.getIntegrationOptions integrationAuthController.getIntegrationOptions
); );
router.post( router.post(
'/oauth-token', '/oauth-token',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED], acceptedStatuses: [GRANTED],
@@ -33,7 +37,9 @@ router.post(
router.get( router.get(
'/:integrationAuthId/apps', '/:integrationAuthId/apps',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -45,7 +51,9 @@ router.get(
router.delete( router.delete(
'/:integrationAuthId', '/:integrationAuthId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED], acceptedStatuses: [GRANTED],
@@ -1,12 +1,14 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../controllers'; import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
'/signup', '/signup',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('inviteeEmail').exists().trim().notEmpty().isEmail(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
validateRequest, validateRequest,
@@ -4,14 +4,16 @@ import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { keyController } from '../controllers'; import { keyController } from '../../controllers/v1';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -24,7 +26,9 @@ router.post(
router.get( router.get(
'/:workspaceId/latest', '/:workspaceId/latest',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -1,12 +1,14 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../controllers'; import { membershipController } from '../../controllers/v1';
router.get( // used for CLI (deprecate) router.get( // used for CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
membershipController.validateMembership membershipController.validateMembership
@@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate)
router.delete( router.delete(
'/:membershipId', '/:membershipId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
membershipController.deleteMembership membershipController.deleteMembership
@@ -22,7 +26,9 @@ router.delete(
router.post( router.post(
'/:membershipId/change-role', '/:membershipId/change-role',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('role').exists().trim(), body('role').exists().trim(),
validateRequest, validateRequest,
membershipController.changeMembershipRole membershipController.changeMembershipRole
@@ -1,13 +1,15 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { param } from 'express-validator'; import { param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../controllers'; import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
// TODO // TODO
'/membershipOrg/:membershipOrgId/change-role', '/membershipOrg/:membershipOrgId/change-role',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId'), param('membershipOrgId'),
validateRequest, validateRequest,
membershipOrgController.changeMembershipOrgRole membershipOrgController.changeMembershipOrgRole
@@ -15,7 +17,9 @@ router.post(
router.delete( router.delete(
'/:membershipOrgId', '/:membershipOrgId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipOrgId').exists().trim(), param('membershipOrgId').exists().trim(),
validateRequest, validateRequest,
membershipOrgController.deleteMembershipOrg membershipOrgController.deleteMembershipOrg
@@ -5,19 +5,23 @@ import {
requireAuth, requireAuth,
requireOrganizationAuth, requireOrganizationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationController } from '../controllers'; import { organizationController } from '../../controllers/v1';
router.get( router.get(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
organizationController.getOrganizations organizationController.getOrganizations
); );
router.post( // not used on frontend router.post( // not used on frontend
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('organizationName').exists().trim().notEmpty(), body('organizationName').exists().trim().notEmpty(),
validateRequest, validateRequest,
organizationController.createOrganization organizationController.createOrganization
@@ -25,7 +29,9 @@ router.post( // not used on frontend
router.get( router.get(
'/:organizationId', '/:organizationId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -37,7 +43,9 @@ router.get(
router.get( router.get(
'/:organizationId/users', '/:organizationId/users',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -49,7 +57,9 @@ router.get(
router.get( router.get(
'/:organizationId/my-workspaces', '/:organizationId/my-workspaces',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -61,7 +71,9 @@ router.get(
router.patch( router.patch(
'/:organizationId/name', '/:organizationId/name',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -74,7 +86,9 @@ router.patch(
router.get( router.get(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -86,7 +100,9 @@ router.get(
router.post( router.post(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -99,7 +115,9 @@ router.post(
router.delete( router.delete(
'/:organizationId/incidentContactOrg', '/:organizationId/incidentContactOrg',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -112,7 +130,9 @@ router.delete(
router.post( router.post(
'/:organizationId/customer-portal-session', '/:organizationId/customer-portal-session',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -124,7 +144,9 @@ router.post(
router.get( router.get(
'/:organizationId/subscriptions', '/:organizationId/subscriptions',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED] acceptedStatuses: [ACCEPTED]
@@ -1,13 +1,15 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
import { passwordController } from '../controllers'; import { passwordController } from '../../controllers/v1';
import { passwordLimiter } from '../helpers/rateLimiter'; import { passwordLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/srp1', '/srp1',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientPublicKey').exists().trim().notEmpty(), body('clientPublicKey').exists().trim().notEmpty(),
validateRequest, validateRequest,
passwordController.srp1 passwordController.srp1
@@ -16,7 +18,9 @@ router.post(
router.post( router.post(
'/change-password', '/change-password',
passwordLimiter, passwordLimiter,
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(), body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
body('iv').exists().trim().notEmpty(), // new iv for private key body('iv').exists().trim().notEmpty(), // new iv for private key
@@ -54,7 +58,9 @@ router.get(
router.post( router.post(
'/backup-private-key', '/backup-private-key',
passwordLimiter, passwordLimiter,
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('clientProof').exists().trim().notEmpty(), body('clientProof').exists().trim().notEmpty(),
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key body('iv').exists().trim().notEmpty(), // new iv for (backup) private key
@@ -5,14 +5,16 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body, query, param } from 'express-validator'; import { body, query, param } from 'express-validator';
import { secretController } from '../controllers'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -28,7 +30,9 @@ router.post(
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -5,12 +5,12 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body } from 'express-validator'; import { body } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { serviceTokenController } from '../controllers'; 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( router.get(
'/', '/',
@@ -20,7 +20,9 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED], acceptedStatuses: [GRANTED],
+63
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;
@@ -1,9 +1,9 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireSignupAuth, validateRequest } from '../middleware'; import { requireSignupAuth, validateRequest } from '../../middleware';
import { signupController } from '../controllers'; import { signupController } from '../../controllers/v1';
import { signupLimiter } from '../helpers/rateLimiter'; import { signupLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/email/signup', '/email/signup',
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { stripeController } from '../controllers'; import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook); router.post('/webhook', stripeController.handleWebhook);
+14
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;
@@ -1,12 +1,14 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { userActionController } from '../controllers'; import { userActionController } from '../../controllers/v1';
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('action'), body('action'),
validateRequest, validateRequest,
userActionController.addUserAction userActionController.addUserAction
@@ -14,7 +16,9 @@ router.post(
router.get( router.get(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
query('action'), query('action'),
validateRequest, validateRequest,
userActionController.getUserAction userActionController.getUserAction
@@ -5,13 +5,15 @@ import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { workspaceController, membershipController } from '../controllers'; import { workspaceController, membershipController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/keys', '/:workspaceId/keys',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -23,7 +25,9 @@ router.get(
router.get( router.get(
'/:workspaceId/users', '/:workspaceId/users',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -33,11 +37,19 @@ router.get(
workspaceController.getWorkspaceMemberships workspaceController.getWorkspaceMemberships
); );
router.get('/', requireAuth, workspaceController.getWorkspaces); router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
workspaceController.getWorkspaces
);
router.get( router.get(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -49,7 +61,9 @@ router.get(
router.post( router.post(
'/', '/',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
body('workspaceName').exists().trim().notEmpty(), body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(),
validateRequest, validateRequest,
@@ -58,7 +72,9 @@ router.post(
router.delete( router.delete(
'/:workspaceId', '/:workspaceId',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -70,7 +86,9 @@ router.delete(
router.post( router.post(
'/:workspaceId/name', '/:workspaceId/name',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED] acceptedStatuses: [COMPLETED, GRANTED]
@@ -83,7 +101,9 @@ router.post(
router.post( router.post(
'/:workspaceId/invite-signup', '/:workspaceId/invite-signup',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -96,7 +116,9 @@ router.post(
router.get( router.get(
'/:workspaceId/integrations', '/:workspaceId/integrations',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -108,7 +130,9 @@ router.get(
router.get( router.get(
'/:workspaceId/authorizations', '/:workspaceId/authorizations',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -120,7 +144,9 @@ router.get(
router.get( router.get(
'/:workspaceId/service-tokens', // deprecate '/:workspaceId/service-tokens', // deprecate
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
@@ -132,7 +158,9 @@ router.get(
router.get( router.get(
'/:workspaceId/service-token-data', '/:workspaceId/service-token-data',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED] acceptedStatuses: [GRANTED]
+7
View File
@@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}
+4
View File
@@ -0,0 +1,4 @@
import express from 'express';
const router = express.Router();
export default router;
+62
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;
+4
View File
@@ -4,6 +4,8 @@ go 1.19
require ( require (
github.com/99designs/keyring v1.2.2 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 github.com/spf13/cobra v1.6.1
golang.org/x/crypto v0.3.0 golang.org/x/crypto v0.3.0
golang.org/x/term 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/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mtibben/percent v0.2.1 // 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/oklog/ulid v1.3.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect
+8
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/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 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+35
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)
}
+2 -2
View File
@@ -14,8 +14,8 @@ var rootCmd = &cobra.Command{
Use: "infisical", Use: "infisical",
Short: "Infisical CLI is used to inject environment variables into any process", 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.`, 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}, CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
Version: "0.1.15", Version: "0.1.16",
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
+8
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
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"
+2
View File
@@ -48,6 +48,8 @@ services:
- ./frontend/public:/app/public - ./frontend/public:/app/public
- ./frontend/styles:/app/styles - ./frontend/styles:/app/styles
- ./frontend/components:/app/components - ./frontend/components:/app/components
- ./frontend/locales:/app/locales
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
env_file: .env env_file: .env
environment: environment:
- NEXT_PUBLIC_ENV=development - NEXT_PUBLIC_ENV=development
+8
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 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> </Tab>
</Tabs> </Tabs>
+27
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
```
+2 -1
View File
@@ -166,7 +166,8 @@
"integrations/frameworks/django", "integrations/frameworks/django",
"integrations/frameworks/flask", "integrations/frameworks/flask",
"integrations/frameworks/laravel", "integrations/frameworks/laravel",
"integrations/frameworks/rails" "integrations/frameworks/rails",
"integrations/frameworks/dotnet"
] ]
}, },
{ {
+2
View File
@@ -32,3 +32,5 @@ yarn-error.log*
.env.production.local .env.production.local
.vercel .vercel
.env.infisical .env.infisical
.vscode
+1
View File
@@ -48,6 +48,7 @@ export default function RouteGuard({ children }) {
// Check if the user is authenticated // Check if the user is authenticated
const response = await checkAuth(); const response = await checkAuth();
// #TODO: figure our why sometimes it doesn't output a response // #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)) { if (!publicPaths.includes(path)) {
try { try {
if (response.status !== 200) { if (response.status !== 200) {
+106 -102
View File
@@ -1,38 +1,39 @@
/* eslint-disable no-unexpected-multiline */ /* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from "react";
import Link from 'next/link'; import Link from "next/link";
import { useRouter } from 'next/router'; import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { import {
faBookOpen, faBookOpen,
faGear, faGear,
faKey, faKey,
faMobile, faMobile,
faPlug, faPlug,
faUser faUser,
} from '@fortawesome/free-solid-svg-icons'; } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from '~/pages/api/organization/getOrgs'; import getOrganizations from "~/pages/api/organization/getOrgs";
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
import checkUserAction from '~/pages/api/userActions/checkUserAction'; import checkUserAction from "~/pages/api/userActions/checkUserAction";
import addUserToWorkspace from '~/pages/api/workspace/addUserToWorkspace'; import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
import createWorkspace from '~/pages/api/workspace/createWorkspace'; import createWorkspace from "~/pages/api/workspace/createWorkspace";
import getWorkspaces from '~/pages/api/workspace/getWorkspaces'; import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import uploadKeys from '~/pages/api/workspace/uploadKeys'; import uploadKeys from "~/pages/api/workspace/uploadKeys";
import NavBarDashboard from '../navigation/NavBarDashboard'; import NavBarDashboard from "../navigation/NavBarDashboard";
import onboardingCheck from '../utilities/checks/OnboardingCheck'; import onboardingCheck from "../utilities/checks/OnboardingCheck";
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage'; import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric encryptAssymmetric,
} from '../utilities/cryptography/crypto'; } from "../utilities/cryptography/crypto";
import Button from './buttons/Button'; import Button from "./buttons/Button";
import AddWorkspaceDialog from './dialog/AddWorkspaceDialog'; import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import Listbox from './Listbox'; import Listbox from "./Listbox";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@@ -41,15 +42,17 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
const router = useRouter(); const router = useRouter();
const [workspaceList, setWorkspaceList] = useState([]); const [workspaceList, setWorkspaceList] = useState([]);
const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]); const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]);
const [workspaceSelected, setWorkspaceSelected] = useState('∞'); const [workspaceSelected, setWorkspaceSelected] = useState("∞");
const [newWorkspaceName, setNewWorkspaceName] = useState(''); const [newWorkspaceName, setNewWorkspaceName] = useState("");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] =
useState(0); useState(0);
const { t } = useTranslation();
function closeModal() { function closeModal() {
setIsOpen(false); setIsOpen(false);
} }
@@ -75,35 +78,35 @@ export default function Layout({ children }: LayoutProps) {
if (!currentWorkspaces.includes(workspaceName)) { if (!currentWorkspaces.includes(workspaceName)) {
const newWorkspace = await createWorkspace({ const newWorkspace = await createWorkspace({
workspaceName, workspaceName,
organizationId: tempLocalStorage('orgData.id') organizationId: tempLocalStorage("orgData.id"),
}); });
const newWorkspaceId = newWorkspace._id; const newWorkspaceId = newWorkspace._id;
if (addAllUsers) { if (addAllUsers) {
const orgUsers = await getOrganizationUsers({ const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage('orgData.id') orgId: tempLocalStorage("orgData.id"),
}); });
orgUsers.map(async (user: any) => { orgUsers.map(async (user: any) => {
if (user.status == 'accepted') { if (user.status == "accepted") {
const result = await addUserToWorkspace( const result = await addUserToWorkspace(
user.user.email, user.user.email,
newWorkspaceId newWorkspaceId
); );
if (result?.invitee && result?.latestKey) { 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 // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey, ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce, nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey, publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY,
}); });
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey: result.invitee.publicKey, publicKey: result.invitee.publicKey,
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY,
}) as { ciphertext: string; nonce: string }; }) as { ciphertext: string; nonce: string };
uploadKeys( uploadKeys(
@@ -116,11 +119,11 @@ export default function Layout({ children }: LayoutProps) {
} }
}); });
} }
router.push('/dashboard/' + newWorkspaceId + '?Development'); router.push("/dashboard/" + newWorkspaceId + "?Development");
setIsOpen(false); setIsOpen(false);
setNewWorkspaceName(''); setNewWorkspaceName("");
} else { } else {
console.error('A project with this name already exists.'); console.error("A project with this name already exists.");
setError(true); setError(true);
setLoading(false); setLoading(false);
} }
@@ -131,62 +134,65 @@ export default function Layout({ children }: LayoutProps) {
} }
} }
const menuItems = [ const menuItems = useMemo(
{ () => [
href: {
'/dashboard/' + href:
workspaceMapping[workspaceSelected as any] + "/dashboard/" +
'?Development', workspaceMapping[workspaceSelected as any] +
title: 'Secrets', "?Development",
emoji: <FontAwesomeIcon icon={faKey} /> title: t("nav:menu.secrets"),
}, emoji: <FontAwesomeIcon icon={faKey} />,
{ },
href: '/users/' + workspaceMapping[workspaceSelected as any], {
title: 'Members', href: "/users/" + workspaceMapping[workspaceSelected as any],
emoji: <FontAwesomeIcon icon={faUser} /> title: t("nav:menu.members"),
}, emoji: <FontAwesomeIcon icon={faUser} />,
{ },
href: '/integrations/' + workspaceMapping[workspaceSelected as any], {
title: 'Integrations', href: "/integrations/" + workspaceMapping[workspaceSelected as any],
emoji: <FontAwesomeIcon icon={faPlug} /> title: t("nav:menu.integrations"),
}, emoji: <FontAwesomeIcon icon={faPlug} />,
{ },
href: '/settings/project/' + workspaceMapping[workspaceSelected as any], {
title: 'Project Settings', href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
emoji: <FontAwesomeIcon icon={faGear} /> title: t("nav:menu.project-settings"),
} emoji: <FontAwesomeIcon icon={faGear} />,
]; },
],
[t, workspaceMapping, workspaceSelected]
);
useEffect(() => { useEffect(() => {
// Put a user in a workspace if they're not in one yet // Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => { const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') { if (tempLocalStorage("orgData.id") === "") {
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id); localStorage.setItem("orgData.id", userOrgs[0]._id);
} }
const orgUserProjects = await getOrganizationUserProjects({ const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage('orgData.id') orgId: tempLocalStorage("orgData.id"),
}); });
const userWorkspaces = orgUserProjects; const userWorkspaces = orgUserProjects;
if ( if (
userWorkspaces.length == 0 && userWorkspaces.length == 0 &&
router.asPath != '/noprojects' && router.asPath != "/noprojects" &&
!router.asPath.includes('settings') !router.asPath.includes("settings")
) { ) {
router.push('/noprojects'); router.push("/noprojects");
} else if (router.asPath != '/noprojects') { } else if (router.asPath != "/noprojects") {
const intendedWorkspaceId = router.asPath const intendedWorkspaceId = router.asPath
.split('/') .split("/")
[router.asPath.split('/').length - 1].split('?')[0]; [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 a user is not a member of a workspace they are trying to access, just push them to one of theirs
if ( if (
intendedWorkspaceId != 'heroku' && intendedWorkspaceId != "heroku" &&
!userWorkspaces !userWorkspaces
.map((workspace: { _id: string }) => workspace._id) .map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
) { ) {
router.push('/dashboard/' + userWorkspaces[0]._id + '?Development'); router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
} else { } else {
setWorkspaceList( setWorkspaceList(
userWorkspaces.map((workspace: any) => workspace.name) userWorkspaces.map((workspace: any) => workspace.name)
@@ -195,7 +201,7 @@ export default function Layout({ children }: LayoutProps) {
Object.fromEntries( Object.fromEntries(
userWorkspaces.map((workspace: any) => [ userWorkspaces.map((workspace: any) => [
workspace.name, workspace.name,
workspace._id workspace._id,
]) ])
) as any ) as any
); );
@@ -203,12 +209,12 @@ export default function Layout({ children }: LayoutProps) {
Object.fromEntries( Object.fromEntries(
userWorkspaces.map((workspace: any) => [ userWorkspaces.map((workspace: any) => [
workspace._id, workspace._id,
workspace.name workspace.name,
]) ])
)[ )[
router.asPath router.asPath
.split('/') .split("/")
[router.asPath.split('/').length - 1].split('?')[0] [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] &&
`${workspaceMapping[workspaceSelected as any]}` !== `${workspaceMapping[workspaceSelected as any]}` !==
router.asPath router.asPath
.split('/') .split("/")
[router.asPath.split('/').length - 1].split('?')[0] [router.asPath.split("/").length - 1].split("?")[0]
) { ) {
router.push( router.push(
'/dashboard/' + "/dashboard/" +
workspaceMapping[workspaceSelected as any] + workspaceMapping[workspaceSelected as any] +
'?Development' "?Development"
); );
localStorage.setItem( localStorage.setItem(
'projectData.id', "projectData.id",
`${workspaceMapping[workspaceSelected as any]}` `${workspaceMapping[workspaceSelected as any]}`
); );
} }
@@ -257,7 +263,7 @@ export default function Layout({ children }: LayoutProps) {
<div> <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="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"> <div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
PROJECT {t("nav:menu.project")}
</div> </div>
{workspaceList.length > 0 ? ( {workspaceList.length > 0 ? (
<Listbox <Listbox
@@ -282,11 +288,11 @@ export default function Layout({ children }: LayoutProps) {
{workspaceList.length > 0 && {workspaceList.length > 0 &&
menuItems.map(({ href, title, emoji }) => ( menuItems.map(({ href, title, emoji }) => (
<li className="mt-0.5 mx-2" key={title}> <li className="mt-0.5 mx-2" key={title}>
{router.asPath.split('/')[1] === href.split('/')[1] && {router.asPath.split("/")[1] === href.split("/")[1] &&
(['project', 'billing', 'org', 'personal'].includes( (["project", "billing", "org", "personal"].includes(
router.asPath.split('/')[2] router.asPath.split("/")[2]
) )
? router.asPath.split('/')[2] === href.split('/')[2] ? router.asPath.split("/")[2] === href.split("/")[2]
: true) ? ( : true) ? (
<div <div
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`} 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> </p>
{title} {title}
</div> </div>
) : router.asPath == '/noprojects' ? ( ) : router.asPath == "/noprojects" ? (
<div <div
className={`flex p-2.5 text-white text-sm rounded`} className={`flex p-2.5 text-white text-sm rounded`}
> >
@@ -323,7 +329,7 @@ export default function Layout({ children }: LayoutProps) {
</ul> </ul>
</div> </div>
<div className="w-full mt-40 mb-4 px-2"> <div className="w-full mt-40 mb-4 px-2">
{router.asPath.split('/')[1] === 'home' ? ( {router.asPath.split("/")[1] === "home" ? (
<div <div
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`} 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 Infisical Guide
<img <img
src={`/images/progress-${ src={`/images/progress-${
totalOnboardingActionsDone == 0 ? '0' : '' totalOnboardingActionsDone == 0 ? "0" : ""
}${totalOnboardingActionsDone == 1 ? '14' : ''}${ }${totalOnboardingActionsDone == 1 ? "14" : ""}${
totalOnboardingActionsDone == 2 ? '28' : '' totalOnboardingActionsDone == 2 ? "28" : ""
}${totalOnboardingActionsDone == 3 ? '43' : ''}${ }${totalOnboardingActionsDone == 3 ? "43" : ""}${
totalOnboardingActionsDone == 4 ? '57' : '' totalOnboardingActionsDone == 4 ? "57" : ""
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`} }${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
height={58} height={58}
width={58} width={58}
alt="progress bar" alt="progress bar"
@@ -359,12 +365,12 @@ export default function Layout({ children }: LayoutProps) {
Infisical Guide Infisical Guide
<img <img
src={`/images/progress-${ src={`/images/progress-${
totalOnboardingActionsDone == 0 ? '0' : '' totalOnboardingActionsDone == 0 ? "0" : ""
}${totalOnboardingActionsDone == 1 ? '14' : ''}${ }${totalOnboardingActionsDone == 1 ? "14" : ""}${
totalOnboardingActionsDone == 2 ? '28' : '' totalOnboardingActionsDone == 2 ? "28" : ""
}${totalOnboardingActionsDone == 3 ? '43' : ''}${ }${totalOnboardingActionsDone == 3 ? "43" : ""}${
totalOnboardingActionsDone == 4 ? '57' : '' totalOnboardingActionsDone == 4 ? "57" : ""
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`} }${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
height={58} height={58}
width={58} width={58}
alt="progress bar" alt="progress bar"
@@ -394,9 +400,7 @@ export default function Layout({ children }: LayoutProps) {
className="text-gray-300 text-7xl mb-8" className="text-gray-300 text-7xl mb-8"
/> />
<p className="text-gray-200 px-6 text-center text-lg max-w-sm"> <p className="text-gray-200 px-6 text-center text-lg max-w-sm">
{' '} {` ${t("common:no-mobile")} `}
To use Infisical, please log in through a device with larger
dimensions.{' '}
</p> </p>
</div> </div>
</> </>
+4 -2
View File
@@ -7,6 +7,7 @@ interface OverrideProps {
keyName: string; keyName: string;
value: string; value: string;
pos: number; pos: number;
comment: string;
} }
interface ToggleProps { interface ToggleProps {
@@ -17,6 +18,7 @@ interface ToggleProps {
value: string; value: string;
pos: number; pos: number;
id: string; id: string;
comment: string;
deleteOverride: (id: string) => void; deleteOverride: (id: string) => void;
sharedToHide: string[]; sharedToHide: string[];
setSharedToHide: (values: string[]) => void; setSharedToHide: (values: string[]) => void;
@@ -46,6 +48,7 @@ export default function Toggle ({
value, value,
pos, pos,
id, id,
comment,
deleteOverride, deleteOverride,
sharedToHide, sharedToHide,
setSharedToHide setSharedToHide
@@ -55,13 +58,12 @@ export default function Toggle ({
checked={enabled} checked={enabled}
onChange={() => { onChange={() => {
if (enabled == false) { if (enabled == false) {
addOverride({ id, keyName, value, pos }); addOverride({ id, keyName, value, pos, comment });
setSharedToHide([ setSharedToHide([
...sharedToHide!, ...sharedToHide!,
id id
]) ])
} else { } else {
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
deleteOverride(id); deleteOverride(id);
} }
setEnabled(!enabled); setEnabled(!enabled);
@@ -1,12 +1,7 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react"; 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"; import Button from "../buttons/Button";
const ActivateBotDialog = ({ const ActivateBotDialog = ({
@@ -16,6 +11,7 @@ const ActivateBotDialog = ({
handleBotActivate, handleBotActivate,
handleIntegrationOption handleIntegrationOption
}) => { }) => {
const { t } = useTranslation();
const submit = async () => { const submit = async () => {
try { try {
@@ -64,18 +60,18 @@ const ActivateBotDialog = ({
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
> >
Grant Infisical access to your secrets {t("integrations:grant-access-to-secrets")}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-2"> <div className="mt-2 mb-2">
<p className="text-sm text-gray-500"> <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> </p>
</div> </div>
<div className="mt-6 max-w-max"> <div className="mt-6 max-w-max">
<Button <Button
onButtonPressed={submit} onButtonPressed={submit}
color="mineshaft" color="mineshaft"
text="Grant access" text={t("integrations:grant-access-button")}
size="md" size="md"
/> />
</div> </div>
@@ -1,4 +1,5 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import addIncidentContact from "~/pages/api/organization/addIncidentContact"; import addIncidentContact from "~/pages/api/organization/addIncidentContact";
@@ -14,6 +15,7 @@ const AddIncidentContactDialog = ({
setIncidentContacts, setIncidentContacts,
}) => { }) => {
let [incidentContactEmail, setIncidentContactEmail] = useState(""); let [incidentContactEmail, setIncidentContactEmail] = useState("");
const { t } = useTranslation();
const submit = () => { const submit = () => {
setIncidentContacts( setIncidentContacts(
@@ -59,17 +61,16 @@ const AddIncidentContactDialog = ({
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400" className="text-lg font-medium leading-6 text-gray-400"
> >
Add an Incident Contact {t("section-incident:add-dialog.title")}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-2"> <div className="mt-2 mb-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
This contact will be notified in the unlikely event of a {t("section-incident:add-dialog.description")}
severe incident.
</p> </p>
</div> </div>
<div className="max-h-28"> <div className="max-h-28">
<InputField <InputField
label="Email" label={t("common:email")}
onChangeHandler={setIncidentContactEmail} onChangeHandler={setIncidentContactEmail}
type="varName" type="varName"
value={incidentContactEmail} value={incidentContactEmail}
@@ -81,7 +82,7 @@ const AddIncidentContactDialog = ({
<Button <Button
onButtonPressed={submit} onButtonPressed={submit}
color="mineshaft" color="mineshaft"
text="Add Incident Contact" text={t("section-incident:add-dialog.add-incident")}
size="md" size="md"
/> />
</div> </div>
@@ -1,5 +1,6 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Trans, useTranslation } from "next-i18next";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button"; import Button from "../buttons/Button";
@@ -15,6 +16,7 @@ const AddProjectMemberDialog = ({
setEmail, setEmail,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation();
return ( return (
<div className="z-50"> <div className="z-50">
@@ -49,48 +51,55 @@ const AddProjectMemberDialog = ({
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" 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>
) : ( ) : (
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" 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> </Dialog.Title>
)} )}
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
{data?.length > 0 ? ( {data?.length > 0 ? (
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm text-gray-500"> <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> </p>
<div className=""> <div className="">
<button <Trans
type="button" i18nKey="section-members:add-dialog.looking-add"
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" components={[
onClick={() => // eslint-disable-next-line react/jsx-key
router.push("/settings/org/" + router.query.id) <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"
If you are looking to add users to your org, onClick={() =>
</button> router.push(
<button "/settings/org/" + router.query.id
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( // eslint-disable-next-line react/jsx-key
"/settings/org/" + router.query.id + "?invite" <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={() =>
click here. router.push(
</button> "/settings/org/" +
router.query.id +
"?invite"
)
}
/>,
]}
/>
</div> </div>
</div> </div>
) : ( ) : (
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Add more users to the organization first. {t("section-members:add-dialog.add-user-org-first")}
</p> </p>
)} )}
</div> </div>
@@ -110,7 +119,7 @@ const AddProjectMemberDialog = ({
<Button <Button
onButtonPressed={submitModal} onButtonPressed={submitModal}
color="mineshaft" color="mineshaft"
text="Add Member" text={t("section-members:add-member")}
size="md" size="md"
/> />
</div> </div>
@@ -120,7 +129,7 @@ const AddProjectMemberDialog = ({
router.push("/settings/org/" + router.query.id) router.push("/settings/org/" + router.query.id)
} }
color="mineshaft" color="mineshaft"
text="Add Users to Organization" text={t("section-members:add-dialog.add-user-to-org")}
size="md" size="md"
/> />
)} )}
@@ -1,40 +1,42 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from "react";
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from "next-i18next";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { Dialog, Transition } from '@headlessui/react'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import nacl from 'tweetnacl'; import { Dialog, Transition } from "@headlessui/react";
import nacl from "tweetnacl";
import addServiceToken from '~/pages/api/serviceToken/addServiceToken'; import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from '../../../public/data/frequentConstants'; import { envMapping } from "../../../public/data/frequentConstants";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric encryptAssymmetric,
} from '../../utilities/cryptography/crypto'; } from "../../utilities/cryptography/crypto";
import Button from '../buttons/Button'; import Button from "../buttons/Button";
import InputField from '../InputField'; import InputField from "../InputField";
import ListBox from '../Listbox'; import ListBox from "../Listbox";
const expiryMapping = { const expiryMapping = {
'1 day': 86400, "1 day": 86400,
'7 days': 604800, "7 days": 604800,
'1 month': 2592000, "1 month": 2592000,
'6 months': 15552000, "6 months": 15552000,
'12 months': 31104000 "12 months": 31104000,
}; };
const AddServiceTokenDialog = ({ const AddServiceTokenDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId, workspaceId,
workspaceName workspaceName,
}) => { }) => {
const [serviceToken, setServiceToken] = useState(''); const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState(''); const [serviceTokenName, setServiceTokenName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState('Development'); const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day'); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation();
const generateServiceToken = async () => { const generateServiceToken = async () => {
const latestFileKey = await getLatestFileKey({ workspaceId }); const latestFileKey = await getLatestFileKey({ workspaceId });
@@ -43,7 +45,7 @@ const AddServiceTokenDialog = ({
ciphertext: latestFileKey.latestKey.encryptedKey, ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce, nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey, publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem('PRIVATE_KEY') privateKey: localStorage.getItem("PRIVATE_KEY"),
}); });
// generate new public/private key pair // generate new public/private key pair
@@ -55,7 +57,7 @@ const AddServiceTokenDialog = ({
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({ const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey, publicKey,
privateKey privateKey,
}); });
let newServiceToken = await addServiceToken({ let newServiceToken = await addServiceToken({
@@ -65,16 +67,16 @@ const AddServiceTokenDialog = ({
expiresIn: expiryMapping[serviceTokenExpiresIn], expiresIn: expiryMapping[serviceTokenExpiresIn],
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce nonce,
}); });
const serviceToken = newServiceToken + ',' + privateKey; const serviceToken = newServiceToken + "," + privateKey;
setServiceToken(serviceToken); setServiceToken(serviceToken);
}; };
function copyToClipboard() { function copyToClipboard() {
// Get the text field // Get the text field
var copyText = document.getElementById('serviceToken'); var copyText = document.getElementById("serviceToken");
// Select the text field // Select the text field
copyText.select(); copyText.select();
@@ -91,8 +93,8 @@ const AddServiceTokenDialog = ({
const closeAddServiceTokenModal = () => { const closeAddServiceTokenModal = () => {
closeModal(); closeModal();
setServiceTokenName(''); setServiceTokenName("");
setServiceToken(''); setServiceToken("");
}; };
return ( return (
@@ -122,27 +124,26 @@ const AddServiceTokenDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" 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.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 <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" 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> </Dialog.Title>
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Specify the name, environment, and expiry period. When {t("section-token:add-dialog.description")}
a token is generated, you will only be able to see it
once before it disappears. Make sure to save it
somewhere.
</p> </p>
</div> </div>
</div> </div>
<div className="max-h-28 mb-2"> <div className="max-h-28 mb-2">
<InputField <InputField
label="Service Token Name" label={t("section-token:add-dialog.name")}
onChangeHandler={setServiceTokenName} onChangeHandler={setServiceTokenName}
type="varName" type="varName"
value={serviceTokenName} value={serviceTokenName}
@@ -155,13 +156,13 @@ const AddServiceTokenDialog = ({
selected={serviceTokenEnv} selected={serviceTokenEnv}
onChange={setServiceTokenEnv} onChange={setServiceTokenEnv}
data={[ data={[
'Development', "Development",
'Staging', "Staging",
'Production', "Production",
'Testing' "Testing",
]} ]}
isFull={true} width="full"
text="Environment: " text={`${t("common:environment")}: `}
/> />
</div> </div>
<div className="max-h-28"> <div className="max-h-28">
@@ -169,14 +170,14 @@ const AddServiceTokenDialog = ({
selected={serviceTokenExpiresIn} selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn} onChange={setServiceTokenExpiresIn}
data={[ data={[
'1 day', "1 day",
'7 days', "7 days",
'1 month', "1 month",
'6 months', "6 months",
'12 months' "12 months",
]} ]}
isFull={true} width="full"
text="Expires in: " text={`${t("common:expired-in")}: `}
/> />
</div> </div>
<div className="max-w-max"> <div className="max-w-max">
@@ -184,10 +185,10 @@ const AddServiceTokenDialog = ({
<Button <Button
onButtonPressed={() => generateServiceToken()} onButtonPressed={() => generateServiceToken()}
color="mineshaft" color="mineshaft"
text="Add Service Token" text={t("section-token:add-dialog.add")}
textDisabled="Add Service Token" textDisabled={t("section-token:add-dialog.add")}
size="md" size="md"
active={serviceTokenName == '' ? false : true} active={serviceTokenName == "" ? false : true}
/> />
</div> </div>
</div> </div>
@@ -198,13 +199,14 @@ const AddServiceTokenDialog = ({
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" 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> </Dialog.Title>
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Once you close this popup, you will never see your {t(
service token again "section-token:add-dialog.copy-service-token-description"
)}
</p> </p>
</div> </div>
</div> </div>
@@ -234,7 +236,7 @@ const AddServiceTokenDialog = ({
)} )}
</button> </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"> <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> </span>
</div> </div>
</div> </div>
@@ -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;
+24 -23
View File
@@ -1,12 +1,13 @@
import { type ChangeEvent, type DragEvent, useState } from 'react'; import { type ChangeEvent, type DragEvent, useState } from "react";
import Image from 'next/image'; import Image from "next/image";
import { faUpload } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from "next-i18next";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from '../basic/buttons/Button'; import Button from "../basic/buttons/Button";
import Error from '../basic/Error'; import Error from "../basic/Error";
import parse from '../utilities/file'; import parse from "../utilities/file";
import guidGenerator from '../utilities/randomId'; import guidGenerator from "../utilities/randomId";
interface DropZoneProps { interface DropZoneProps {
// TODO: change Data type from any // TODO: change Data type from any
@@ -26,8 +27,10 @@ const DropZone = ({
errorDragAndDrop, errorDragAndDrop,
setButtonReady, setButtonReady,
keysExist, keysExist,
numCurrentRows numCurrentRows,
}: DropZoneProps) => { }: DropZoneProps) => {
const { t } = useTranslation();
const handleDragEnter = (e: DragEvent) => { const handleDragEnter = (e: DragEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -43,7 +46,7 @@ const DropZone = ({
e.stopPropagation(); e.stopPropagation();
// set dropEffect to copy i.e copy of the source item // set dropEffect to copy i.e copy of the source item
e.dataTransfer.dropEffect = 'copy'; e.dataTransfer.dropEffect = "copy";
}; };
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -54,7 +57,7 @@ const DropZone = ({
setTimeout(() => setLoading(false), 5000); setTimeout(() => setLoading(false), 5000);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.dataTransfer.dropEffect = 'copy'; e.dataTransfer.dropEffect = "copy";
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
const reader = new FileReader(); const reader = new FileReader();
@@ -69,7 +72,7 @@ const DropZone = ({
pos: numCurrentRows + index, pos: numCurrentRows + index,
key: key, key: key,
value: keyPairs[key as keyof typeof keyPairs], value: keyPairs[key as keyof typeof keyPairs],
type: 'shared' type: "shared",
}; };
}); });
setData(newData); setData(newData);
@@ -96,16 +99,16 @@ const DropZone = ({
reader.onload = (event) => { reader.onload = (event) => {
if (event.target === null || event.target.result === null) return; if (event.target === null || event.target.result === null) return;
const { result } = event.target; const { result } = event.target;
if (typeof result === 'string') { if (typeof result === "string") {
const newData = result const newData = result
.split('\n') .split("\n")
.map((line: string, index: number) => { .map((line: string, index: number) => {
return { return {
id: guidGenerator(), id: guidGenerator(),
pos: numCurrentRows + index, pos: numCurrentRows + index,
key: line.split('=')[0], key: line.split("=")[0],
value: line.split('=').slice(1, line.split('=').length).join('='), value: line.split("=").slice(1, line.split("=").length).join("="),
type: 'shared' type: "shared",
}; };
}); });
setData(newData); setData(newData);
@@ -126,7 +129,7 @@ const DropZone = ({
</div> </div>
) : keysExist ? ( ) : keysExist ? (
<div <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} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -147,11 +150,9 @@ const DropZone = ({
<div className="flex flex-row"> <div className="flex flex-row">
<FontAwesomeIcon <FontAwesomeIcon
icon={faUpload} 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"> <p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
Drag and drop your .env file here to add more keys.
</p>
</div> </div>
{errorDragAndDrop ? ( {errorDragAndDrop ? (
<div className="mt-8 max-w-xl opacity-80"> <div className="mt-8 max-w-xl opacity-80">
@@ -170,7 +171,7 @@ const DropZone = ({
onDrop={handleDrop} onDrop={handleDrop}
> >
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" /> <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 <input
id="fileSelect" id="fileSelect"
type="file" type="file"
@@ -1,4 +1,5 @@
import { Fragment,useState } from 'react'; import { Fragment,useState } from 'react';
import { useTranslation } from "next-i18next";
import { faShuffle } from '@fortawesome/free-solid-svg-icons'; import { faShuffle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react'; 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 GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
const [randomStringLength, setRandomStringLength] = useState(32); const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation();
return <Menu as="div" className="relative inline-block text-left"> return <Menu as="div" className="relative inline-block text-left">
<div> <div>
@@ -51,8 +53,8 @@ const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: st
icon={faShuffle} icon={faShuffle}
/> />
<div className="text-sm justify-between flex flex-row w-full"> <div className="text-sm justify-between flex flex-row w-full">
<p>Generate Random Hex</p> <p>{t("dashboard:sidebar.generate-random-hex")}</p>
<p>digits</p> <p>{t("dashboard:sidebar.digits")}</p>
</div> </div>
</div> </div>
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 "> <div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
+26 -27
View File
@@ -1,10 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from "next-i18next";
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SecretVersionList from 'ee/components/SecretVersionList'; import SecretVersionList from 'ee/components/SecretVersionList';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle'; import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
import DashboardInputField from './DashboardInputField'; import DashboardInputField from './DashboardInputField';
import GenerateSecretMenu from './GenerateSecretMenu'; import GenerateSecretMenu from './GenerateSecretMenu';
@@ -15,6 +17,7 @@ interface SecretProps {
pos: number; pos: number;
type: string; type: string;
id: string; id: string;
comment: string;
} }
interface OverrideProps { interface OverrideProps {
@@ -22,6 +25,7 @@ interface OverrideProps {
keyName: string; keyName: string;
value: string; value: string;
pos: number; pos: number;
comment: string;
} }
interface SideBarProps { interface SideBarProps {
@@ -29,6 +33,7 @@ interface SideBarProps {
data: SecretProps[]; data: SecretProps[];
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyComment: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void; addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void; deleteOverride: (id: string) => void;
buttonReady: boolean; buttonReady: boolean;
@@ -56,6 +61,7 @@ const SideBar = ({
data, data,
modifyKey, modifyKey,
modifyValue, modifyValue,
modifyComment,
addOverride, addOverride,
deleteOverride, deleteOverride,
buttonReady, buttonReady,
@@ -64,29 +70,30 @@ const SideBar = ({
setSharedToHide setSharedToHide
}: SideBarProps) => { }: SideBarProps) => {
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal")); 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'> 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='h-min overflow-y-auto'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center"> <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")}> <div className='p-1' onClick={() => toggleSidebar("None")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/> <FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div> </div>
</div> </div>
<div className='mt-4 px-4 pointer-events-none'> <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 <DashboardInputField
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
type="varName" type="varName"
position={data[0].pos} position={data[0]?.pos}
value={data[0].key} value={data[0]?.key}
isDuplicate={false} isDuplicate={false}
blurred={false} blurred={false}
/> />
</div> </div>
{data.filter(secret => secret.type == "shared")[0]?.value {data.filter(secret => secret.type == "shared")[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}> ? <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 <DashboardInputField
onChangeHandler={modifyValue} onChangeHandler={modifyValue}
type="value" type="value"
@@ -100,21 +107,22 @@ const SideBar = ({
</div> </div>
</div> </div>
: <div className='px-4 text-sm text-bunker-300 pt-4'> : <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> <span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
This secret is personal. It is not shared with any of your teammates. {t("dashboard:sidebar.personal-explanation")}
</div>} </div>}
<div className='mt-4 px-4'> <div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value && {data.filter(secret => secret.type == "shared")[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'> <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 <Toggle
enabled={overrideEnabled} enabled={overrideEnabled}
setEnabled={setOverrideEnabled} setEnabled={setOverrideEnabled}
addOverride={addOverride} addOverride={addOverride}
keyName={data[0].key} keyName={data[0]?.key}
value={data[0].value} value={data[0]?.value}
pos={data[0].pos} pos={data[0]?.pos}
id={data[0].id} id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride} deleteOverride={deleteOverride}
sharedToHide={sharedToHide} sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide} setSharedToHide={setSharedToHide}
@@ -124,13 +132,13 @@ const SideBar = ({
<DashboardInputField <DashboardInputField
onChangeHandler={modifyValue} onChangeHandler={modifyValue}
type="value" type="value"
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos} 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} value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
isDuplicate={false} isDuplicate={false}
blurred={true} blurred={true}
/> />
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'> <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> </div>
</div> </div>
@@ -143,21 +151,12 @@ const SideBar = ({
isFull={true} isFull={true}
/> />
</div> */} </div> */}
<div className={`relative mt-4 px-4 pt-4`}> <SecretVersionList secretId={data[0]?.id} />
<div className='flex flex-row justify-between'> <CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
<p className='text-sm text-bunker-300'>Comments & notes</p>
<div className="bg-yellow rounded-md h-min">
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
</div>
</div>
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
Leave your comment here...
</div>
</div>
</div> </div>
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}> <div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button <Button
text="Save Changes" text={String(t("common:save-changes"))}
onButtonPressed={savePush} onButtonPressed={savePush}
color="primary" color="primary"
size="md" size="md"
@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "next-i18next";
import CloudIntegration from "./CloudIntegration"; import CloudIntegration from "./CloudIntegration";
@@ -24,12 +25,14 @@ const CloudIntegrationSection = ({
integrationOptionPress, integrationOptionPress,
integrationAuths integrationAuths
}: Props) => { }: Props) => {
const { t } = useTranslation();
return ( return (
<> <>
<div className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}> <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"> <p className="text-base text-gray-400">
Click on an integration to begin syncing secrets to it. {t("integrations:click-to-start")}
</p> </p>
</div> </div>
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl"> <div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl">
@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { useTranslation } from "next-i18next";
import FrameworkIntegration from "./FrameworkIntegration"; import FrameworkIntegration from "./FrameworkIntegration";
@@ -15,24 +16,26 @@ interface Props {
} }
const FrameworkIntegrationSection = ({ frameworks }: Props) => { const FrameworkIntegrationSection = ({ frameworks }: Props) => {
return ( const { t } = useTranslation();
<>
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2"> return (
<h1 className="font-semibold text-3xl">Framework Integrations</h1> <>
<p className="text-base text-gray-400"> <div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
Click on a framework to get the setup instructions. <h1 className="font-semibold text-3xl">{t("integrations:framework-integrations")}</h1>
</p> <p className="text-base text-gray-400">
</div> {t("integrations:click-to-setup")}
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl"> </p>
{frameworks.map((framework) => ( </div>
<FrameworkIntegration <div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
framework={framework} {frameworks.map((framework) => (
key={`framework-integration-${framework.slug}`} <FrameworkIntegration
/> framework={framework}
))} key={`framework-integration-${framework.slug}`}
</div> />
</> ))}
); </div>
</>
);
} }
export default FrameworkIntegrationSection; export default FrameworkIntegrationSection;
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react/jsx-key */ /* eslint-disable react/jsx-key */
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useMemo, useState } from "react";
import Image from 'next/image'; import Image from "next/image";
import { useRouter } from 'next/router'; import { useRouter } from "next/router";
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons'; import { TFunction, useTranslation } from "next-i18next";
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons'; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faBook, faBook,
@@ -12,39 +12,39 @@ import {
faEnvelope, faEnvelope,
faGear, faGear,
faPlus, faPlus,
faRightFromBracket faRightFromBracket,
} from '@fortawesome/free-solid-svg-icons'; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from "@headlessui/react";
import logout from '~/pages/api/auth/Logout'; 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 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} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
'Join Slack Forum', t("nav:support.slack"),
'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g' "https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
'Read Docs', t("nav:support.docs"),
'https://infisical.com/docs/getting-started/introduction' "https://infisical.com/docs/getting-started/introduction",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
'Open a GitHub Issue', t("nav:support.issue"),
'https://github.com/Infisical/infisical-cli/issues' "https://github.com/Infisical/infisical-cli/issues",
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
'Send us an Email', t("nav:support.email"),
'mailto:[email protected]' "mailto:[email protected]",
] ],
]; ];
export interface ICurrentOrg { export interface ICurrentOrg {
@@ -68,6 +68,10 @@ export default function Navbar() {
const [orgs, setOrgs] = useState([]); const [orgs, setOrgs] = useState([]);
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>(); const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
const { t } = useTranslation();
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const userData = await getUser(); const userData = await getUser();
@@ -75,16 +79,16 @@ export default function Navbar() {
const orgsData = await getOrganizations(); const orgsData = await getOrganizations();
setOrgs(orgsData); setOrgs(orgsData);
const currentOrg = await getOrganization({ const currentOrg = await getOrganization({
orgId: String(localStorage.getItem('orgData.id')) orgId: String(localStorage.getItem("orgData.id")),
}); });
setCurrentOrg(currentOrg); setCurrentOrg(currentOrg);
})(); })();
}, []); }, []);
const closeApp = async () => { const closeApp = async () => {
console.log('Logging out...'); console.log("Logging out...");
await logout(); await logout();
router.push('/login'); router.push("/login");
}; };
return ( return (
@@ -119,18 +123,17 @@ export default function Navbar() {
leaveTo="transform opacity-0 scale-95" 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"> <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) => ( {supportOptionsList.map(([icon, text, url]) => (
// eslint-disable-next-line react/jsx-no-target-blank
<a <a
key={guidGenerator()} key={guidGenerator()}
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
href={String(option[2])} href={String(url)}
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5" 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"> <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]} {icon}
<div className="text-sm">{option[1]}</div> <div className="text-sm">{text}</div>
</div> </div>
</a> </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"> <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="px-1 py-1 ">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <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>
<div <div
onClick={() => 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" 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 className="flex items-center justify-between w-full">
<div> <div>
<p className="text-gray-300 px-2 pt-1 text-sm"> <p className="text-gray-300 px-2 pt-1 text-sm">
{' '} {" "}
{user?.firstName} {user?.lastName} {user?.firstName} {user?.lastName}
</p> </p>
<p className="text-gray-400 px-2 pb-1 text-xs"> <p className="text-gray-400 px-2 pb-1 text-xs">
{' '} {" "}
{user?.email} {user?.email}
</p> </p>
</div> </div>
@@ -190,11 +193,11 @@ export default function Navbar() {
</div> </div>
<div className="px-2 pt-2"> <div className="px-2 pt-2">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <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>
<div <div
onClick={() => 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" 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 <div
onClick={() => 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" 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" className="text-lg pl-1.5 pr-3"
icon={faCoins} icon={faCoins}
/> />
<div className="text-sm">Usage & Billing</div> <div className="text-sm">{t("nav:user.usage-billing")}</div>
</div> </div>
</button> </button>
<button <button
@@ -235,7 +238,7 @@ export default function Navbar() {
<div <div
onClick={() => onClick={() =>
router.push( 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" 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"> <span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="ml-1" /> <FontAwesomeIcon icon={faPlus} className="ml-1" />
</span> </span>
<div className="text-sm ml-1">Invite Members</div> <div className="text-sm ml-1">{t("nav:user.invite")}</div>
</div> </div>
</button> </button>
</div> </div>
{orgs?.length > 1 && ( {orgs?.length > 1 && (
<div className="px-1 pt-1"> <div className="px-1 pt-1">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <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>
<div className="flex flex-col items-start px-1 mt-3 mb-2"> <div className="flex flex-col items-start px-1 mt-3 mb-2">
{orgs {orgs
.filter( .filter(
(org: { _id: string }) => (org: { _id: string }) =>
org._id != localStorage.getItem('orgData.id') org._id != localStorage.getItem("orgData.id")
) )
.map((org: { _id: string; name: string }) => ( .map((org: { _id: string; name: string }) => (
<div <div
key={guidGenerator()} key={guidGenerator()}
onClick={() => { onClick={() => {
localStorage.setItem('orgData.id', org._id); localStorage.setItem("orgData.id", org._id);
router.reload(); router.reload();
}} }}
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md" 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} onClick={closeApp}
className={`${ className={`${
active active
? 'bg-red font-semibold text-white' ? "bg-red font-semibold text-white"
: 'text-gray-400' : "text-gray-400"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`} } 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"> <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" className="text-lg ml-1.5 mr-3"
icon={faRightFromBracket} icon={faRightFromBracket}
/> />
Log Out {t("common:logout")}
</div> </div>
</button> </button>
)} )}
@@ -16,12 +16,19 @@ export default class SecurityClient {
const req = new Request(resource, options); const req = new Request(resource, options);
if (this.#token == '') { 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) { if (this.#token) {
req.headers.set('Authorization', 'Bearer ' + this.#token); req.headers.set('Authorization', 'Bearer ' + this.#token);
return fetch(req);
} }
return fetch(req);
} }
} }
@@ -52,6 +52,7 @@ const attemptLogin = async (
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } = const { token, publicKey, encryptedPrivateKey, iv, tag } =
await login2(email, clientProof); await login2(email, clientProof);
SecurityClient.setToken(token); SecurityClient.setToken(token);
const privateKey = Aes256Gcm.decrypt({ const privateKey = Aes256Gcm.decrypt({
@@ -108,15 +109,16 @@ const attemptLogin = async (
if (isSignUp) { if (isSignUp) {
await pushKeys({ await pushKeys({
obj: { obj: {
DATABASE_URL: [ sDATABASE_URL: [
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net', 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
'personal' 'This is an example of secret referencing.'
], ],
DB_USERNAME: ['user1234', 'personal'], sDB_USERNAME: ['OVERRIDE_THIS', ''],
DB_PASSWORD: ['example_password', 'personal'], sDB_PASSWORD: ['OVERRIDE_THIS', ''],
TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'], 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.'],
WEBSITE_URL: ['http://localhost:3000', 'shared'], 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.'],
STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared'] sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
sWEBSITE_URL: ['http://localhost:3000', ''],
}, },
workspaceId: projectToLogin, workspaceId: projectToLogin,
env: 'Development' env: 'Development'
@@ -10,6 +10,14 @@ const {
const nacl = require('tweetnacl'); const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util'); nacl.util = require('tweetnacl-util');
interface SecretProps {
key: string;
value: string;
type: 'personal' | 'shared';
comment: string;
id: string;
}
interface Props { interface Props {
env: keyof typeof envMapping; env: keyof typeof envMapping;
setFileState: any; setFileState: any;
@@ -34,12 +42,12 @@ const getSecretsForProject = async ({
} catch (error) { } catch (error) {
console.log('ERROR: Not able to access the latest file'); 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); setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = []; const tempFileState: SecretProps[] = [];
if (file.key) { if (file.key) {
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -64,10 +72,25 @@ const getSecretsForProject = async ({
tag: secretPair.secretValue.tag, tag: secretPair.secretValue.tag,
key 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({ tempFileState.push({
id: secretPair._id,
key: plainTextKey, key: plainTextKey,
value: plainTextValue, value: plainTextValue,
type: secretPair.type type: secretPair.type,
comment: plainTextComment
}); });
}); });
} }
@@ -76,22 +99,24 @@ const getSecretsForProject = async ({
setData( setData(
tempFileState.map((line, index) => { tempFileState.map((line, index) => {
return { return {
id: guidGenerator(), id: line['id'],
pos: index, pos: index,
key: line['key'], key: line['key'],
value: line['value'], value: line['value'],
type: line['type'] type: line['type'],
comment: line['comment']
}; };
}) })
); );
return tempFileState.map((line, index) => { return tempFileState.map((line, index) => {
return { return {
id: guidGenerator(), id: line['id'],
pos: index, pos: index,
key: line['key'], key: line['key'],
value: line['value'], value: line['value'],
type: line['type'] type: line['type'],
comment: line['comment']
}; };
}); });
} catch (error) { } catch (error) {
@@ -47,9 +47,9 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
const secrets = Object.keys(obj).map((key) => { const secrets = Object.keys(obj).map((key) => {
// encrypt key // encrypt key
const { const {
ciphertext: ciphertextKey, ciphertext: secretKeyCiphertext,
iv: ivKey, iv: secretKeyIV,
tag: tagKey, tag: secretKeyTag,
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: key.slice(1), plaintext: key.slice(1),
key: randomBytes, key: randomBytes,
@@ -57,25 +57,39 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
// encrypt value // encrypt value
const { const {
ciphertext: ciphertextValue, ciphertext: secretValueCiphertext,
iv: ivValue, iv: secretValueIV,
tag: tagValue, tag: secretValueTag,
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: obj[key as keyof typeof obj][0], plaintext: obj[key as keyof typeof obj][0],
key: randomBytes, 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"; const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return { return {
ciphertextKey, secretKeyCiphertext,
ivKey, secretKeyIV,
tagKey, secretKeyTag,
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"), secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
ciphertextValue, secretValueCiphertext,
ivValue, secretValueIV,
tagValue, secretValueTag,
hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"), 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, type: visibility,
}; };
}); });
@@ -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,
},
};
};
+5
View File
@@ -16,3 +16,8 @@ export const publicPaths = [
`/terms`, `/terms`,
`/subprocessors`, `/subprocessors`,
]; ];
export const languageMap = {
en: "English",
ko: "한국어",
};

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