mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue developing service token v2
This commit is contained in:
1
.github/workflows/release_build.yml
vendored
1
.github/workflows/release_build.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,6 +12,8 @@ node_modules
|
||||
.DS_Store
|
||||
|
||||
/dist
|
||||
/completions/
|
||||
/manpages/
|
||||
|
||||
# frontend
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
# - cd cli && go mod tidy
|
||||
# # you may remove this if you don't need go generate
|
||||
# - cd cli && go generate ./...
|
||||
before:
|
||||
hooks:
|
||||
- ./cli/scripts/completions.sh
|
||||
- ./cli/scripts/manpages.sh
|
||||
|
||||
builds:
|
||||
- id: darwin-build
|
||||
binary: infisical
|
||||
@@ -30,13 +35,13 @@ builds:
|
||||
- openbsd
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- "386"
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
@@ -44,6 +49,16 @@ builds:
|
||||
goarch: "386"
|
||||
dir: ./cli
|
||||
|
||||
archives:
|
||||
- format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- README*
|
||||
- LICENSE*
|
||||
- manpages/*
|
||||
- completions/*
|
||||
|
||||
release:
|
||||
replace_existing_draft: true
|
||||
mode: 'replace'
|
||||
@@ -85,13 +100,22 @@ nfpms:
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
description: The offical Infisical CLI
|
||||
license: Apache 2.0
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
- apk
|
||||
- archlinux
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: ./completions/infisical.bash
|
||||
dst: /etc/bash_completion.d/infisical
|
||||
- src: ./completions/infisical.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
||||
- src: ./completions/infisical.zsh
|
||||
dst: /usr/share/zsh/site-functions/_infisical
|
||||
- src: ./manpages/infisical.1.gz
|
||||
dst: /usr/share/man/man1/infisical.1.gz
|
||||
scoop:
|
||||
bucket:
|
||||
owner: Infisical
|
||||
@@ -101,7 +125,31 @@ scoop:
|
||||
email: ai@infisical.com
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
license: Apache-2.0
|
||||
license: MIT
|
||||
aurs:
|
||||
-
|
||||
name: infisical-bin
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
maintainers:
|
||||
- Infisical, Inc <support@infisical.com>
|
||||
license: MIT
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
|
||||
package: |-
|
||||
# bin
|
||||
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||
# license
|
||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
|
||||
# completions
|
||||
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
|
||||
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
|
||||
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
|
||||
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
|
||||
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
|
||||
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
||||
# man pages
|
||||
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
||||
# dockers:
|
||||
# - dockerfile: goreleaser.dockerfile
|
||||
# goos: linux
|
||||
|
||||
28
README.md
28
README.md
@@ -41,7 +41,7 @@
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||
- **Personal/Shared** scoping for environment variables
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon)
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
||||
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
||||
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
@@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
|
||||
✔️ Ruby on Rails
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
|
||||
✔️ Vue
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/vue?ref=github.com">
|
||||
✔️ Vue
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/rails?ref=github.com">
|
||||
✔️ Ruby on Rails
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="middle">
|
||||
<a href="https://infisical.com/docs/integrations/frameworks/dotnet?ref=github.com">
|
||||
✔️ .NET
|
||||
</a>
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
And more...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -321,4 +331,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
|
||||
## 🌎 Translations
|
||||
|
||||
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
|
||||
|
||||
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
|
||||
12
backend/package-lock.json
generated
12
backend/package-lock.json
generated
@@ -6677,9 +6677,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -17175,9 +17175,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
|
||||
@@ -14,28 +14,31 @@ import { apiLimiter } from './helpers/rateLimiter';
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
} from './ee/routes';
|
||||
|
||||
} from './ee/routes/v1';
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
bot as botRouter,
|
||||
organization as organizationRouter,
|
||||
workspace as workspaceRouter,
|
||||
membershipOrg as membershipOrgRouter,
|
||||
membership as membershipRouter,
|
||||
key as keyRouter,
|
||||
inviteOrg as inviteOrgRouter,
|
||||
user as userRouter,
|
||||
userAction as userActionRouter,
|
||||
secret as secretRouter,
|
||||
serviceToken as serviceTokenRouter,
|
||||
password as passwordRouter,
|
||||
stripe as stripeRouter,
|
||||
integration as integrationRouter,
|
||||
integrationAuth as integrationAuthRouter,
|
||||
serviceTokenData as serviceTokenDataRouter
|
||||
} from './routes';
|
||||
signup as v1SignupRouter,
|
||||
auth as v1AuthRouter,
|
||||
bot as v1BotRouter,
|
||||
organization as v1OrganizationRouter,
|
||||
workspace as v1WorkspaceRouter,
|
||||
membershipOrg as v1MembershipOrgRouter,
|
||||
membership as v1MembershipRouter,
|
||||
key as v1KeyRouter,
|
||||
inviteOrg as v1InviteOrgRouter,
|
||||
user as v1UserRouter,
|
||||
userAction as v1UserActionRouter,
|
||||
secret as v1SecretRouter,
|
||||
serviceToken as v1ServiceTokenRouter,
|
||||
serviceTokenData as v1ServiceTokenDataRouter,
|
||||
password as v1PasswordRouter,
|
||||
stripe as v1StripeRouter,
|
||||
integration as v1IntegrationRouter,
|
||||
integrationAuth as v1IntegrationAuthRouter
|
||||
} from './routes/v1';
|
||||
import {
|
||||
secret as v2SecretRouter,
|
||||
workspace as v2WorkspaceRouter
|
||||
} from './routes/v2';
|
||||
|
||||
import { getLogger } from './utils/logger';
|
||||
import { RouteNotFoundError } from './utils/errors';
|
||||
@@ -64,29 +67,33 @@ if (NODE_ENV === 'production') {
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
// /ee routers
|
||||
// (EE) routes
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
|
||||
// routers
|
||||
app.use('/api/v1/signup', signupRouter);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
app.use('/api/v1/bot', botRouter);
|
||||
app.use('/api/v1/user', userRouter);
|
||||
app.use('/api/v1/user-action', userActionRouter);
|
||||
app.use('/api/v1/organization', organizationRouter);
|
||||
app.use('/api/v1/workspace', workspaceRouter);
|
||||
app.use('/api/v1/membership-org', membershipOrgRouter);
|
||||
app.use('/api/v1/membership', membershipRouter);
|
||||
app.use('/api/v1/key', keyRouter);
|
||||
app.use('/api/v1/invite-org', inviteOrgRouter);
|
||||
app.use('/api/v1/secret', secretRouter);
|
||||
app.use('/api/v1/service-token', serviceTokenRouter);
|
||||
app.use('/api/v1/password', passwordRouter);
|
||||
app.use('/api/v1/stripe', stripeRouter);
|
||||
app.use('/api/v1/integration', integrationRouter);
|
||||
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
||||
app.use('/api/v1/service-token-data', serviceTokenDataRouter);
|
||||
// v1 routes
|
||||
app.use('/api/v1/signup', v1SignupRouter);
|
||||
app.use('/api/v1/auth', v1AuthRouter);
|
||||
app.use('/api/v1/bot', v1BotRouter);
|
||||
app.use('/api/v1/user', v1UserRouter);
|
||||
app.use('/api/v1/user-action', v1UserActionRouter);
|
||||
app.use('/api/v1/organization', v1OrganizationRouter);
|
||||
app.use('/api/v1/workspace', v1WorkspaceRouter);
|
||||
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
|
||||
app.use('/api/v1/membership', v1MembershipRouter);
|
||||
app.use('/api/v1/key', v1KeyRouter);
|
||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||
app.use('/api/v1/secret', v1SecretRouter);
|
||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
||||
app.use('/api/v1/service-token-data', v1ServiceTokenDataRouter);
|
||||
app.use('/api/v1/password', v1PasswordRouter);
|
||||
app.use('/api/v1/stripe', v1StripeRouter);
|
||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
|
||||
// v2 routes
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||
app.use('/api/v2/secret', v2SecretRouter);
|
||||
|
||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||
app.use((req, res, next)=>{
|
||||
|
||||
@@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User } from '../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
||||
import { User } from '../../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_SECRET
|
||||
} from '../config';
|
||||
} from '../../config';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Bot, BotKey } from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
import { Bot, BotKey } from '../../models';
|
||||
import { createBot } from '../../helpers/bot';
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
@@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
|
||||
import * as userActionController from './userActionController';
|
||||
import * as userController from './userController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
import * as serviceTokenDataController from './serviceTokenDataController';
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -31,5 +32,6 @@ export {
|
||||
stripeController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController
|
||||
workspaceController,
|
||||
serviceTokenDataController
|
||||
};
|
||||
@@ -2,10 +2,10 @@ import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
import { getApps, revokeAccess } from '../integrations';
|
||||
import { IntegrationAuth, Integration } from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
|
||||
export const getIntegrationOptions = async (
|
||||
req: Request,
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Integration, Bot, BotKey } from '../models';
|
||||
import { EventService } from '../services';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { Integration, Bot, BotKey } from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface Key {
|
||||
encryptedKey: string;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../models';
|
||||
import { findMembership } from '../helpers/membership';
|
||||
import { GRANTED } from '../variables';
|
||||
import { Key } from '../../models';
|
||||
import { findMembership } from '../../helpers/membership';
|
||||
import { GRANTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, MembershipOrg, User, Key } from '../models';
|
||||
import { Membership, MembershipOrg, User, Key } from '../../models';
|
||||
import {
|
||||
findMembership,
|
||||
deleteMembership as deleteMember
|
||||
} from '../helpers/membership';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { SITE_URL } from '../config';
|
||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
|
||||
} from '../../helpers/membership';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { SITE_URL } from '../../config';
|
||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../helpers/organization';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// TODO
|
||||
|
||||
let membershipToChangeRole;
|
||||
try {
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change organization membership role'
|
||||
});
|
||||
}
|
||||
// try {
|
||||
// } catch (err) {
|
||||
// Sentry.setUser({ email: req.user.email });
|
||||
// Sentry.captureException(err);
|
||||
// return res.status(400).send({
|
||||
// message: 'Failed to change organization membership role'
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
membershipOrg: membershipToChangeRole
|
||||
@@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email magic link verification for complete account'
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
@@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
code
|
||||
});
|
||||
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
// membership can be approved and redirected to login/dashboard
|
||||
membershipOrg.status = ACCEPTED;
|
||||
await membershipOrg.save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_CARD_AUTH
|
||||
} from '../config';
|
||||
} from '../../config';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
Organization,
|
||||
Workspace,
|
||||
IncidentContactOrg
|
||||
} from '../models';
|
||||
import { createOrganization as create } from '../helpers/organization';
|
||||
import { addMembershipsOrg } from '../helpers/membershipOrg';
|
||||
import { OWNER, ACCEPTED } from '../variables';
|
||||
} from '../../models';
|
||||
import { createOrganization as create } from '../../helpers/organization';
|
||||
import { addMembershipsOrg } from '../../helpers/membershipOrg';
|
||||
import { OWNER, ACCEPTED } from '../../variables';
|
||||
|
||||
const productToPriceMap = {
|
||||
starter: STRIPE_PRODUCT_STARTER,
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, Token, BackupPrivateKey } from '../models';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
|
||||
import { User, Token, BackupPrivateKey } from '../../models';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key, Secret } from '../models';
|
||||
import { Key, Secret } from '../../models';
|
||||
import {
|
||||
pushSecrets as push,
|
||||
v1PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
reformatPullSecrets
|
||||
} from '../helpers/secret';
|
||||
import { pushKeys } from '../helpers/key';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { EventService } from '../services';
|
||||
import { ENV_SET } from '../variables';
|
||||
import { postHogClient } from '../services';
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { postHogClient } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@@ -21,6 +21,10 @@ interface PushSecret {
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ServiceToken } from '../models';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { ENV_SET } from '../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../config';
|
||||
import { ServiceToken } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service token on request
|
||||
@@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../config';
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceToken = async (req: Request, res: Response) => {
|
||||
// get service token
|
||||
return res.status(200).send({
|
||||
serviceToken: req.serviceToken
|
||||
});
|
||||
@@ -73,13 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* SERVICE_TOKEN: <JWT_SERVICE_TOKEN>,<PRIVATE_KEY_SERVICE_TOKEN>
|
||||
* - <JWT_SERVICE_TOKEN> authorizes the service token for "service token"-only endpoints.
|
||||
* - <PRIVATE_KEY_SERVICE_TOKEN> authorizes the service token to pull secrets via that endpoint.
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
};
|
||||
102
backend/src/controllers/v1/serviceTokenDataController.ts
Normal file
102
backend/src/controllers/v1/serviceTokenDataController.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import {
|
||||
SALT_ROUNDS
|
||||
} from '../../config';
|
||||
|
||||
/**
|
||||
* Return service token data associated with service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => ({
|
||||
serviceTokenData: req.serviceTokenData
|
||||
});
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
* environment [environment].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceToken, serviceTokenData;
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
expiresIn
|
||||
} = req.body;
|
||||
|
||||
// create 41-char service token with first 9-char being the prefix
|
||||
serviceToken = `st.${crypto.randomBytes(19).toString('hex')}`;
|
||||
|
||||
const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS);
|
||||
|
||||
// compute access token expiration date
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user: req.user._id,
|
||||
expiresAt,
|
||||
prefix: serviceToken.substring(0, 9),
|
||||
serviceTokenHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create service token data'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
try {
|
||||
const { serviceTokenDataId } = req.params;
|
||||
|
||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete service token data'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { User, MembershipOrg } from '../models';
|
||||
import { completeAccount } from '../helpers/user';
|
||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||
import { User, MembershipOrg } from '../../models';
|
||||
import { completeAccount } from '../../helpers/user';
|
||||
import {
|
||||
sendEmailVerification,
|
||||
checkEmailVerification,
|
||||
initializeDefaultOrg
|
||||
} from '../helpers/signup';
|
||||
import { issueTokens, createToken } from '../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../variables';
|
||||
} from '../../helpers/signup';
|
||||
import { issueTokens, createToken } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { UserAction } from '../models';
|
||||
import { UserAction } from '../../models';
|
||||
|
||||
/**
|
||||
* Add user action [action]
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
IntegrationAuth,
|
||||
IUser,
|
||||
ServiceToken,
|
||||
ServiceTokenData,
|
||||
} from '../models';
|
||||
ServiceTokenData
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../helpers/workspace';
|
||||
import { addMemberships } from '../helpers/membership';
|
||||
import { ADMIN, COMPLETED, GRANTED } from '../variables';
|
||||
} from '../../helpers/workspace';
|
||||
import { addMemberships } from '../../helpers/membership';
|
||||
import { ADMIN, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
5
backend/src/controllers/v2/index.ts
Normal file
5
backend/src/controllers/v2/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
workspaceController
|
||||
}
|
||||
0
backend/src/controllers/v2/secretController.ts
Normal file
0
backend/src/controllers/v2/secretController.ts
Normal file
194
backend/src/controllers/v2/workspaceController.ts
Normal file
194
backend/src/controllers/v2/workspaceController.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Workspace,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Key,
|
||||
IUser,
|
||||
ServiceToken,
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../../helpers/workspace';
|
||||
import {
|
||||
v2PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
reformatPullSecrets
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { addMemberships } from '../../helpers/membership';
|
||||
import { postHogClient, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables';
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||
);
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to upload workspace secrets'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully uploaded workspace secrets'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
try {
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
let userId;
|
||||
if (req.user) {
|
||||
userId = req.user._id.toString();
|
||||
} else if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
let key;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate('sender', '+publicKey');
|
||||
|
||||
if (!key) throw new Error('Failed to find workspace key');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace key'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
key
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretVersion } from '../models';
|
||||
import { SecretVersion } from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../models';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
@@ -4,14 +4,16 @@ import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
} from '../../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables';
|
||||
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { stripeController } from '../controllers';
|
||||
import { stripeController } from '../../controllers/v1';
|
||||
|
||||
router.post('/webhook', stripeController.handleWebhook);
|
||||
|
||||
@@ -4,14 +4,16 @@ import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
} from '../../../middleware';
|
||||
import { param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { workspaceController } from '../controllers';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../../variables';
|
||||
import { workspaceController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -1,15 +1,69 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import bcrypt from 'bcrypt';
|
||||
import {
|
||||
User
|
||||
User,
|
||||
ServiceTokenData
|
||||
} from '../models';
|
||||
import {
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_LIFETIME,
|
||||
JWT_REFRESH_SECRET
|
||||
JWT_REFRESH_SECRET,
|
||||
SALT_ROUNDS
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
* Attach auth payload
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue
|
||||
*/
|
||||
const attachAuthPayload = async ({
|
||||
authTokenValue
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
}) => {
|
||||
let serviceTokenHash, decodedToken; // intermediate variables
|
||||
let serviceTokenData, user; // payloads
|
||||
try {
|
||||
switch (authTokenValue.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
// case: service token auth mode
|
||||
serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS);
|
||||
serviceTokenData = await ServiceTokenData
|
||||
.findOne({
|
||||
serviceTokenHash
|
||||
})
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw new Error('Account not found error');
|
||||
}
|
||||
|
||||
return serviceTokenData;
|
||||
default:
|
||||
// case: JWT token auth mode
|
||||
decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
|
||||
);
|
||||
|
||||
user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user)
|
||||
throw new Error('Account not found error');
|
||||
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Unable to authenticate due to partially set up account');
|
||||
|
||||
return user;
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error('Failed to attach auth payload');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
||||
* @param {Object} obj
|
||||
@@ -99,4 +153,9 @@ const createToken = ({
|
||||
}
|
||||
};
|
||||
|
||||
export { createToken, issueTokens, clearTokens };
|
||||
export {
|
||||
attachAuthPayload,
|
||||
createToken,
|
||||
issueTokens,
|
||||
clearTokens
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
decryptSymmetric,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { ENCRYPTION_KEY } from '../config';
|
||||
import { SECRET_SHARED } from '../variables';
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const validateMembership = async ({
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
import { LICENSE_KEY } from '../config';
|
||||
|
||||
interface PushSecret {
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
@@ -25,9 +24,29 @@ interface PushSecret {
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
interface Update {
|
||||
[index: string]: any;
|
||||
}
|
||||
@@ -45,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {Object[]} obj.secrets - secrets to push
|
||||
*/
|
||||
const pushSecrets = async ({
|
||||
const v1PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
@@ -54,7 +73,7 @@ const pushSecrets = async ({
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: PushSecret[];
|
||||
secrets: V1PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
@@ -93,8 +112,9 @@ const pushSecrets = async ({
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) {
|
||||
// case: filter secrets where value changed
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|
||||
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
|
||||
// case: filter secrets where value or comment changed
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,14 +133,22 @@ const pushSecrets = async ({
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue
|
||||
hashValue,
|
||||
ciphertextComment,
|
||||
ivComment,
|
||||
tagComment,
|
||||
hashComment
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
secretValueCiphertext: ciphertextValue,
|
||||
secretValueIV: ivValue,
|
||||
secretValueTag: tagValue,
|
||||
secretValueHash: hashValue
|
||||
secretValueHash: hashValue,
|
||||
secretCommentCiphertext: ciphertextComment,
|
||||
secretCommentIV: ivComment,
|
||||
secretCommentTag: tagComment,
|
||||
secretCommentHash: hashComment,
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
@@ -192,7 +220,254 @@ const pushSecrets = async ({
|
||||
secretValueCiphertext: s.ciphertextValue,
|
||||
secretValueIV: s.ivValue,
|
||||
secretValueTag: s.tagValue,
|
||||
secretValueHash: s.hashValue
|
||||
secretValueHash: s.hashValue,
|
||||
secretCommentCiphertext: s.ciphertextComment,
|
||||
secretCommentIV: s.ivComment,
|
||||
secretCommentTag: s.tagComment,
|
||||
secretCommentHash: s.hashComment
|
||||
};
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
obj['user' as keyof typeof obj] = userId;
|
||||
}
|
||||
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to push shared and personal secrets');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Push secrets for user with id [userId] to workspace
|
||||
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||
* 1. Handle shared secrets (insert, delete)
|
||||
* 2. handle personal secrets (insert, delete)
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user to push secrets for
|
||||
* @param {String} obj.workspaceId - id of workspace to push to
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {Object[]} obj.secrets - secrets to push
|
||||
*/
|
||||
const v2PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: V2PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
|
||||
// handle deleting secrets
|
||||
const toDelete = oldSecrets
|
||||
.filter(
|
||||
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||
)
|
||||
.map((s) => s._id);
|
||||
if (toDelete.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|
||||
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
|
||||
// case: filter secrets where value or comment changed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: filter (legacy) secrets that were not versioned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map((s) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
return ({
|
||||
secret: s._id,
|
||||
version: s.version ? s.version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map(({
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}, idx) => {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash
|
||||
};
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
@@ -315,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
iv: s.secretValueIV,
|
||||
tag: s.secretValueTag,
|
||||
hash: s.secretValueHash
|
||||
},
|
||||
secretComment: {
|
||||
workspace: s.workspace,
|
||||
ciphertext: s.secretCommentCiphertext,
|
||||
iv: s.secretCommentIV,
|
||||
tag: s.secretCommentTag,
|
||||
hash: s.secretCommentHash
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -326,73 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
return reformatedSecrets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return decrypted secrets in format [format]
|
||||
* @param {Object} obj
|
||||
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
|
||||
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
|
||||
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
|
||||
* @return {String|Object} (decrypted) secrets also called the content
|
||||
*/
|
||||
const decryptSecrets = ({
|
||||
secrets,
|
||||
key,
|
||||
format
|
||||
}: {
|
||||
secrets: PushSecret[];
|
||||
key: string;
|
||||
format: DecryptSecretType;
|
||||
}) => {
|
||||
// init content
|
||||
let content: any = format === 'text' ? '' : {};
|
||||
|
||||
// decrypt secrets
|
||||
secrets.forEach((s, idx) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: s.ciphertextKey,
|
||||
iv: s.ivKey,
|
||||
tag: s.tagKey,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: s.ciphertextValue,
|
||||
iv: s.ivValue,
|
||||
tag: s.tagValue,
|
||||
key
|
||||
});
|
||||
|
||||
switch (format) {
|
||||
case 'text':
|
||||
content += secretKey;
|
||||
content += '=';
|
||||
content += secretValue;
|
||||
|
||||
if (idx < secrets.length) {
|
||||
content += '\n';
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
content[secretKey] = secretValue;
|
||||
break;
|
||||
case 'expanded':
|
||||
content[secretKey] = {
|
||||
...s,
|
||||
plaintextKey: secretKey,
|
||||
plaintextValue: secretValue
|
||||
};
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export {
|
||||
pushSecrets,
|
||||
v1PushSecrets,
|
||||
v2PushSecrets,
|
||||
pullSecrets,
|
||||
reformatPullSecrets,
|
||||
decryptSecrets
|
||||
reformatPullSecrets
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SECRET_GITHUB
|
||||
} from '../config';
|
||||
import { user } from '../routes';
|
||||
|
||||
interface ExchangeCodeHerokuResponse {
|
||||
token_type: string;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { User } from '../models';
|
||||
import { User, ServiceTokenData } from '../models';
|
||||
import {
|
||||
attachAuthPayload
|
||||
} from '../helpers/auth';
|
||||
import { JWT_AUTH_SECRET } from '../config';
|
||||
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
@@ -11,34 +14,58 @@ declare module 'jsonwebtoken' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if JWT (auth) token on request is valid (e.g. not expired),
|
||||
* if there is an associated user, and if that user is fully setup.
|
||||
* @param req - express request object
|
||||
* @param res - express response object
|
||||
* @param next - express next function
|
||||
* Validate if token on request is valid (e.g. not expired) for various auth modes:
|
||||
* - If token is a JWT token, then check if there is an associated user
|
||||
* and if user is fully setup.
|
||||
* - If token is a service token (st), then check if there is associated
|
||||
* service token data.
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
|
||||
* @returns
|
||||
*/
|
||||
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
// JWT authentication middleware
|
||||
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
|
||||
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
|
||||
const requireAuth = ({
|
||||
acceptedAuthModes = ['jwt']
|
||||
}: {
|
||||
acceptedAuthModes: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||
if(AUTH_TOKEN_TYPE === null)
|
||||
return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
|
||||
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
|
||||
return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
|
||||
if(AUTH_TOKEN_VALUE === null)
|
||||
return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
|
||||
|
||||
// validate auth mode
|
||||
let authMode;
|
||||
switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
authMode = 'st';
|
||||
break;
|
||||
default:
|
||||
authMode = 'jwt';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
|
||||
);
|
||||
// attach auth request payload
|
||||
const payload = await attachAuthPayload({
|
||||
authTokenValue: AUTH_TOKEN_VALUE
|
||||
});
|
||||
|
||||
switch (authMode) {
|
||||
case 'st':
|
||||
req.serviceTokenData = payload;
|
||||
break;
|
||||
default:
|
||||
req.user = payload;
|
||||
break;
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
|
||||
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
|
||||
if (!user?.publicKey)
|
||||
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
|
||||
|
||||
req.user = user;
|
||||
return next();
|
||||
};
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
export default requireAuth;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ServiceToken, ServiceTokenData } from '../models';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
import { AccountNotFoundError } from '../utils/errors';
|
||||
import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
@@ -15,21 +15,25 @@ const requireServiceTokenDataAuth = ({
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
const { serviceTokenDataId } = req[location];
|
||||
|
||||
const serviceTokenData = await ServiceTokenData
|
||||
.findById(req[location].serviceTokenDataId)
|
||||
.select('+encryptedKey +iv +tag');
|
||||
|
||||
|
||||
if (!serviceTokenData) {
|
||||
return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: serviceTokenData.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
if (req.user) {
|
||||
// case: jwt auth
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: serviceTokenData.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
}
|
||||
|
||||
req.serviceTokenData = serviceTokenData;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ type req = 'params' | 'body' | 'query';
|
||||
* Validate if user on request is a member with proper roles for workspace
|
||||
* on request params.
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses for JWT auth
|
||||
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||
*/
|
||||
const requireWorkspaceAuth = ({
|
||||
@@ -22,17 +22,28 @@ const requireWorkspaceAuth = ({
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// workspace authorization middleware
|
||||
|
||||
try {
|
||||
const membership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: req[location].workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
const { workspaceId } = req[location];
|
||||
|
||||
req.membership = membership;
|
||||
if (req.user) {
|
||||
// case: jwt auth
|
||||
const membership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.membership = membership;
|
||||
}
|
||||
|
||||
if (
|
||||
req.serviceTokenData
|
||||
&& req.serviceTokenData.workspace !== workspaceId
|
||||
&& req.serviceTokenData.environment !== req.body.environment
|
||||
)
|
||||
// case: st auth
|
||||
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface ISecret {
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
@@ -82,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretCommentCiphertext: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
secretCommentIV: {
|
||||
type: String, // symmetric
|
||||
required: false
|
||||
},
|
||||
secretCommentTag: {
|
||||
type: String, // symmetric
|
||||
required: false
|
||||
},
|
||||
secretCommentHash: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IServiceTokenData {
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string; // TODO: adapt to upcoming environment id
|
||||
user: Types.ObjectId;
|
||||
expiresAt: Date;
|
||||
prefix: string;
|
||||
serviceTokenHash: string;
|
||||
@@ -28,6 +29,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date
|
||||
},
|
||||
@@ -38,7 +44,8 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
serviceTokenHash: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true
|
||||
required: true,
|
||||
select: true
|
||||
},
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { authController } from '../controllers';
|
||||
import { loginLimiter } from '../helpers/rateLimiter';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { authController } from '../../controllers/v1';
|
||||
import { loginLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post('/token', validateRequest, authController.getNewToken);
|
||||
|
||||
@@ -25,7 +25,19 @@ router.post(
|
||||
authController.login2
|
||||
);
|
||||
|
||||
router.post('/logout', requireAuth, authController.logout);
|
||||
router.post('/checkAuth', requireAuth, authController.checkAuth);
|
||||
router.post(
|
||||
'/logout',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
authController.logout
|
||||
);
|
||||
router.post(
|
||||
'/checkAuth',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
authController.checkAuth
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -6,13 +6,15 @@ import {
|
||||
requireBotAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { botController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
} from '../../middleware';
|
||||
import { botController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -24,7 +26,9 @@ router.get(
|
||||
|
||||
router.patch(
|
||||
'/:botId/active',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireBotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -4,14 +4,16 @@ import {
|
||||
requireAuth,
|
||||
requireIntegrationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../controllers';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -29,7 +31,9 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:integrationId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -6,19 +6,23 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { integrationAuthController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { integrationAuthController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/integration-options',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
integrationAuthController.getIntegrationOptions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/oauth-token',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED],
|
||||
@@ -33,7 +37,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/apps',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -45,7 +51,9 @@ router.get(
|
||||
|
||||
router.delete(
|
||||
'/:integrationAuthId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED],
|
||||
@@ -1,12 +1,14 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipOrgController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/signup',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
|
||||
body('organizationId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@@ -4,14 +4,16 @@ import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { keyController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { keyController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -24,7 +26,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/latest',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -1,12 +1,14 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipController } from '../../controllers/v1';
|
||||
|
||||
router.get( // used for CLI (deprecate)
|
||||
'/:workspaceId/connect',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.validateMembership
|
||||
@@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate)
|
||||
|
||||
router.delete(
|
||||
'/:membershipId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
param('membershipId').exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.deleteMembership
|
||||
@@ -22,7 +26,9 @@ router.delete(
|
||||
|
||||
router.post(
|
||||
'/:membershipId/change-role',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('role').exists().trim(),
|
||||
validateRequest,
|
||||
membershipController.changeMembershipRole
|
||||
@@ -1,13 +1,15 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipOrgController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
// TODO
|
||||
'/membershipOrg/:membershipOrgId/change-role',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
param('membershipOrgId'),
|
||||
validateRequest,
|
||||
membershipOrgController.changeMembershipOrgRole
|
||||
@@ -15,7 +17,9 @@ router.post(
|
||||
|
||||
router.delete(
|
||||
'/:membershipOrgId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
param('membershipOrgId').exists().trim(),
|
||||
validateRequest,
|
||||
membershipOrgController.deleteMembershipOrg
|
||||
@@ -5,19 +5,23 @@ import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables';
|
||||
import { organizationController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
|
||||
import { organizationController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
organizationController.getOrganizations
|
||||
);
|
||||
|
||||
router.post( // not used on frontend
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('organizationName').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
organizationController.createOrganization
|
||||
@@ -25,7 +29,9 @@ router.post( // not used on frontend
|
||||
|
||||
router.get(
|
||||
'/:organizationId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -37,7 +43,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:organizationId/users',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -49,7 +57,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:organizationId/my-workspaces',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -61,7 +71,9 @@ router.get(
|
||||
|
||||
router.patch(
|
||||
'/:organizationId/name',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -74,7 +86,9 @@ router.patch(
|
||||
|
||||
router.get(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -86,7 +100,9 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -99,7 +115,9 @@ router.post(
|
||||
|
||||
router.delete(
|
||||
'/:organizationId/incidentContactOrg',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -112,7 +130,9 @@ router.delete(
|
||||
|
||||
router.post(
|
||||
'/:organizationId/customer-portal-session',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -124,7 +144,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:organizationId/subscriptions',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
@@ -1,13 +1,15 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { passwordController } from '../controllers';
|
||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { passwordController } from '../../controllers/v1';
|
||||
import { passwordLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/srp1',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientPublicKey').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
passwordController.srp1
|
||||
@@ -16,7 +18,9 @@ router.post(
|
||||
router.post(
|
||||
'/change-password',
|
||||
passwordLimiter,
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd
|
||||
body('iv').exists().trim().notEmpty(), // new iv for private key
|
||||
@@ -54,7 +58,9 @@ router.get(
|
||||
router.post(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('clientProof').exists().trim().notEmpty(),
|
||||
body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key
|
||||
body('iv').exists().trim().notEmpty(), // new iv for (backup) private key
|
||||
@@ -5,14 +5,16 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -28,7 +30,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { serviceTokenController } from '../controllers';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { serviceTokenController } from '../../controllers/v1';
|
||||
|
||||
// Note to devs: service-token to be deprecated in favor of api-key
|
||||
// note: deprecate service-token routes in favor of service-token data routes/structure
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
@@ -20,7 +20,9 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED],
|
||||
63
backend/src/routes/v1/serviceTokenData.ts
Normal file
63
backend/src/routes/v1/serviceTokenData.ts
Normal 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';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { signupController } from '../controllers';
|
||||
import { signupLimiter } from '../helpers/rateLimiter';
|
||||
import { requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { signupController } from '../../controllers/v1';
|
||||
import { signupLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/email/signup',
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { stripeController } from '../controllers';
|
||||
import { stripeController } from '../../controllers/v1';
|
||||
|
||||
router.post('/webhook', stripeController.handleWebhook);
|
||||
|
||||
14
backend/src/routes/v1/user.ts
Normal file
14
backend/src/routes/v1/user.ts
Normal 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';
|
||||
const router = express.Router();
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { body, query } from 'express-validator';
|
||||
import { userActionController } from '../controllers';
|
||||
import { userActionController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('action'),
|
||||
validateRequest,
|
||||
userActionController.addUserAction
|
||||
@@ -14,7 +16,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
query('action'),
|
||||
validateRequest,
|
||||
userActionController.getUserAction
|
||||
@@ -5,13 +5,15 @@ import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { workspaceController, membershipController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { workspaceController, membershipController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/keys',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -23,7 +25,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/users',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -33,11 +37,19 @@ router.get(
|
||||
workspaceController.getWorkspaceMemberships
|
||||
);
|
||||
|
||||
router.get('/', requireAuth, workspaceController.getWorkspaces);
|
||||
router.get(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
workspaceController.getWorkspaces
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -49,7 +61,9 @@ router.get(
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('workspaceName').exists().trim().notEmpty(),
|
||||
body('organizationId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@@ -58,7 +72,9 @@ router.post(
|
||||
|
||||
router.delete(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -70,7 +86,9 @@ router.delete(
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/name',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
@@ -83,7 +101,9 @@ router.post(
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/invite-signup',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -96,7 +116,9 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/integrations',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -108,7 +130,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/authorizations',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -120,7 +144,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/service-tokens', // deprecate
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
@@ -132,7 +158,9 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/service-token-data',
|
||||
requireAuth,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
7
backend/src/routes/v2/index.ts
Normal file
7
backend/src/routes/v2/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import secret from './secret';
|
||||
import workspace from './workspace';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
}
|
||||
4
backend/src/routes/v2/secret.ts
Normal file
4
backend/src/routes/v2/secret.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
|
||||
export default router;
|
||||
62
backend/src/routes/v2/workspace.ts
Normal file
62
backend/src/routes/v2/workspace.ts
Normal 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,6 +4,8 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/99designs/keyring v1.2.2
|
||||
github.com/muesli/mango-cobra v1.2.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/term v0.3.0
|
||||
@@ -22,6 +24,8 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/muesli/mango v0.1.0 // indirect
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
|
||||
@@ -56,6 +56,14 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
|
||||
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
|
||||
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
|
||||
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
|
||||
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
|
||||
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
|
||||
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
|
||||
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
|
||||
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
|
||||
35
cli/packages/cmd/man.go
Normal file
35
cli/packages/cmd/man.go
Normal 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)
|
||||
}
|
||||
@@ -14,8 +14,8 @@ var rootCmd = &cobra.Command{
|
||||
Use: "infisical",
|
||||
Short: "Infisical CLI is used to inject environment variables into any process",
|
||||
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
||||
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
|
||||
Version: "0.1.15",
|
||||
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
|
||||
Version: "0.1.16",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
||||
8
cli/scripts/completions.sh
Executable file
8
cli/scripts/completions.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
rm -rf completions
|
||||
mkdir completions
|
||||
cd cli
|
||||
for sh in bash zsh fish; do
|
||||
go run . completion "$sh" > "../completions/infisical.$sh"
|
||||
done
|
||||
6
cli/scripts/manpages.sh
Executable file
6
cli/scripts/manpages.sh
Executable 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"
|
||||
@@ -48,6 +48,8 @@ services:
|
||||
- ./frontend/public:/app/public
|
||||
- ./frontend/styles:/app/styles
|
||||
- ./frontend/components:/app/components
|
||||
- ./frontend/locales:/app/locales
|
||||
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
||||
env_file: .env
|
||||
environment:
|
||||
- NEXT_PUBLIC_ENV=development
|
||||
|
||||
@@ -88,6 +88,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo
|
||||
sudo apt-get update && sudo apt-get install -y infisical
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Arch Linux">
|
||||
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
|
||||
|
||||
```bash
|
||||
yay -S infisical-bin
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
27
docs/integrations/frameworks/dotnet.mdx
Normal file
27
docs/integrations/frameworks/dotnet.mdx
Normal 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
|
||||
```
|
||||
@@ -166,7 +166,8 @@
|
||||
"integrations/frameworks/django",
|
||||
"integrations/frameworks/flask",
|
||||
"integrations/frameworks/laravel",
|
||||
"integrations/frameworks/rails"
|
||||
"integrations/frameworks/rails",
|
||||
"integrations/frameworks/dotnet"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -32,3 +32,5 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
.vercel
|
||||
.env.infisical
|
||||
|
||||
.vscode
|
||||
@@ -48,6 +48,7 @@ export default function RouteGuard({ children }) {
|
||||
// Check if the user is authenticated
|
||||
const response = await checkAuth();
|
||||
// #TODO: figure our why sometimes it doesn't output a response
|
||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||
if (!publicPaths.includes(path)) {
|
||||
try {
|
||||
if (response.status !== 200) {
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faBookOpen,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faUser
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
|
||||
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
|
||||
import checkUserAction from '~/pages/api/userActions/checkUserAction';
|
||||
import addUserToWorkspace from '~/pages/api/workspace/addUserToWorkspace';
|
||||
import createWorkspace from '~/pages/api/workspace/createWorkspace';
|
||||
import getWorkspaces from '~/pages/api/workspace/getWorkspaces';
|
||||
import uploadKeys from '~/pages/api/workspace/uploadKeys';
|
||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
||||
import checkUserAction from "~/pages/api/userActions/checkUserAction";
|
||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||
import uploadKeys from "~/pages/api/workspace/uploadKeys";
|
||||
|
||||
import NavBarDashboard from '../navigation/NavBarDashboard';
|
||||
import onboardingCheck from '../utilities/checks/OnboardingCheck';
|
||||
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from '../utilities/cryptography/crypto';
|
||||
import Button from './buttons/Button';
|
||||
import AddWorkspaceDialog from './dialog/AddWorkspaceDialog';
|
||||
import Listbox from './Listbox';
|
||||
encryptAssymmetric,
|
||||
} from "../utilities/cryptography/crypto";
|
||||
import Button from "./buttons/Button";
|
||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
||||
import Listbox from "./Listbox";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -41,15 +42,17 @@ interface LayoutProps {
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const router = useRouter();
|
||||
const [workspaceList, setWorkspaceList] = useState([]);
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] =
|
||||
useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
function closeModal() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
@@ -75,35 +78,35 @@ export default function Layout({ children }: LayoutProps) {
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage('orgData.id')
|
||||
organizationId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
if (addAllUsers) {
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage('orgData.id')
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status == 'accepted') {
|
||||
if (user.status == "accepted") {
|
||||
const result = await addUserToWorkspace(
|
||||
user.user.email,
|
||||
newWorkspaceId
|
||||
);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY');
|
||||
const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
uploadKeys(
|
||||
@@ -116,11 +119,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||
}
|
||||
});
|
||||
}
|
||||
router.push('/dashboard/' + newWorkspaceId + '?Development');
|
||||
router.push("/dashboard/" + newWorkspaceId + "?Development");
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName('');
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
console.error('A project with this name already exists.');
|
||||
console.error("A project with this name already exists.");
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -131,62 +134,65 @@ export default function Layout({ children }: LayoutProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
href:
|
||||
'/dashboard/' +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
'?Development',
|
||||
title: 'Secrets',
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: '/users/' + workspaceMapping[workspaceSelected as any],
|
||||
title: 'Members',
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: '/integrations/' + workspaceMapping[workspaceSelected as any],
|
||||
title: 'Integrations',
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
href: '/settings/project/' + workspaceMapping[workspaceSelected as any],
|
||||
title: 'Project Settings',
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
];
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
href:
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
"?Development",
|
||||
title: t("nav:menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />,
|
||||
},
|
||||
{
|
||||
href: "/users/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.members"),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />,
|
||||
},
|
||||
{
|
||||
href: "/integrations/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
||||
},
|
||||
{
|
||||
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.project-settings"),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />,
|
||||
},
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage('orgData.id') === '') {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem('orgData.id', userOrgs[0]._id);
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage('orgData.id')
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
userWorkspaces.length == 0 &&
|
||||
router.asPath != '/noprojects' &&
|
||||
!router.asPath.includes('settings')
|
||||
router.asPath != "/noprojects" &&
|
||||
!router.asPath.includes("settings")
|
||||
) {
|
||||
router.push('/noprojects');
|
||||
} else if (router.asPath != '/noprojects') {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath != "/noprojects") {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0];
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
intendedWorkspaceId != 'heroku' &&
|
||||
intendedWorkspaceId != "heroku" &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push('/dashboard/' + userWorkspaces[0]._id + '?Development');
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id + "?Development");
|
||||
} else {
|
||||
setWorkspaceList(
|
||||
userWorkspaces.map((workspace: any) => workspace.name)
|
||||
@@ -195,7 +201,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [
|
||||
workspace.name,
|
||||
workspace._id
|
||||
workspace._id,
|
||||
])
|
||||
) as any
|
||||
);
|
||||
@@ -203,12 +209,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [
|
||||
workspace._id,
|
||||
workspace.name
|
||||
workspace.name,
|
||||
])
|
||||
)[
|
||||
router.asPath
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0]
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0]
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -224,16 +230,16 @@ export default function Layout({ children }: LayoutProps) {
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0]
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
router.push(
|
||||
'/dashboard/' +
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any] +
|
||||
'?Development'
|
||||
"?Development"
|
||||
);
|
||||
localStorage.setItem(
|
||||
'projectData.id',
|
||||
"projectData.id",
|
||||
`${workspaceMapping[workspaceSelected as any]}`
|
||||
);
|
||||
}
|
||||
@@ -257,7 +263,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<div>
|
||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
PROJECT
|
||||
{t("nav:menu.project")}
|
||||
</div>
|
||||
{workspaceList.length > 0 ? (
|
||||
<Listbox
|
||||
@@ -282,11 +288,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{workspaceList.length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mt-0.5 mx-2" key={title}>
|
||||
{router.asPath.split('/')[1] === href.split('/')[1] &&
|
||||
(['project', 'billing', 'org', 'personal'].includes(
|
||||
router.asPath.split('/')[2]
|
||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
||||
(["project", "billing", "org", "personal"].includes(
|
||||
router.asPath.split("/")[2]
|
||||
)
|
||||
? router.asPath.split('/')[2] === href.split('/')[2]
|
||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
||||
: true) ? (
|
||||
<div
|
||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
||||
@@ -297,7 +303,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath == '/noprojects' ? (
|
||||
) : router.asPath == "/noprojects" ? (
|
||||
<div
|
||||
className={`flex p-2.5 text-white text-sm rounded`}
|
||||
>
|
||||
@@ -323,7 +329,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full mt-40 mb-4 px-2">
|
||||
{router.asPath.split('/')[1] === 'home' ? (
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div
|
||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
||||
>
|
||||
@@ -334,12 +340,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${
|
||||
totalOnboardingActionsDone == 0 ? '0' : ''
|
||||
}${totalOnboardingActionsDone == 1 ? '14' : ''}${
|
||||
totalOnboardingActionsDone == 2 ? '28' : ''
|
||||
}${totalOnboardingActionsDone == 3 ? '43' : ''}${
|
||||
totalOnboardingActionsDone == 4 ? '57' : ''
|
||||
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`}
|
||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
@@ -359,12 +365,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${
|
||||
totalOnboardingActionsDone == 0 ? '0' : ''
|
||||
}${totalOnboardingActionsDone == 1 ? '14' : ''}${
|
||||
totalOnboardingActionsDone == 2 ? '28' : ''
|
||||
}${totalOnboardingActionsDone == 3 ? '43' : ''}${
|
||||
totalOnboardingActionsDone == 4 ? '57' : ''
|
||||
}${totalOnboardingActionsDone == 5 ? '71' : ''}.svg`}
|
||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
@@ -394,9 +400,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
className="text-gray-300 text-7xl mb-8"
|
||||
/>
|
||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||
{' '}
|
||||
To use Infisical, please log in through a device with larger
|
||||
dimensions.{' '}
|
||||
{` ${t("common:no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ interface OverrideProps {
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
@@ -17,6 +18,7 @@ interface ToggleProps {
|
||||
value: string;
|
||||
pos: number;
|
||||
id: string;
|
||||
comment: string;
|
||||
deleteOverride: (id: string) => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
@@ -46,6 +48,7 @@ export default function Toggle ({
|
||||
value,
|
||||
pos,
|
||||
id,
|
||||
comment,
|
||||
deleteOverride,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
@@ -55,13 +58,12 @@ export default function Toggle ({
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride({ id, keyName, value, pos });
|
||||
addOverride({ id, keyName, value, pos, comment });
|
||||
setSharedToHide([
|
||||
...sharedToHide!,
|
||||
id
|
||||
])
|
||||
} else {
|
||||
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
|
||||
deleteOverride(id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
|
||||
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
@@ -16,6 +11,7 @@ const ActivateBotDialog = ({
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
@@ -64,18 +60,18 @@ const ActivateBotDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
{t("integrations:grant-access-to-secrets")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
||||
{t("integrations:why-infisical-needs-access")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
text={t("integrations:grant-access-button")}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import addIncidentContact from "~/pages/api/organization/addIncidentContact";
|
||||
@@ -14,6 +15,7 @@ const AddIncidentContactDialog = ({
|
||||
setIncidentContacts,
|
||||
}) => {
|
||||
let [incidentContactEmail, setIncidentContactEmail] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = () => {
|
||||
setIncidentContacts(
|
||||
@@ -59,17 +61,16 @@ const AddIncidentContactDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Add an Incident Contact
|
||||
{t("section-incident:add-dialog.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
This contact will be notified in the unlikely event of a
|
||||
severe incident.
|
||||
{t("section-incident:add-dialog.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
label={t("common:email")}
|
||||
onChangeHandler={setIncidentContactEmail}
|
||||
type="varName"
|
||||
value={incidentContactEmail}
|
||||
@@ -81,7 +82,7 @@ const AddIncidentContactDialog = ({
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Add Incident Contact"
|
||||
text={t("section-incident:add-dialog.add-incident")}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
@@ -15,6 +16,7 @@ const AddProjectMemberDialog = ({
|
||||
setEmail,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="z-50">
|
||||
@@ -49,48 +51,55 @@ const AddProjectMemberDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
Add a member to your project
|
||||
{t("section-members:add-dialog.add-member-to-project")}
|
||||
</Dialog.Title>
|
||||
) : (
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
All the users in your organization are already invited.
|
||||
{t("section-members:add-dialog.already-all-invited")}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
{data?.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
The user will receive an email with the instructions.
|
||||
{t("section-members:add-dialog.user-will-email")}
|
||||
</p>
|
||||
<div className="">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push("/settings/org/" + router.query.id)
|
||||
}
|
||||
>
|
||||
If you are looking to add users to your org,
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
"/settings/org/" + router.query.id + "?invite"
|
||||
)
|
||||
}
|
||||
>
|
||||
click here.
|
||||
</button>
|
||||
<Trans
|
||||
i18nKey="section-members:add-dialog.looking-add"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
"/settings/org/" + router.query.id
|
||||
)
|
||||
}
|
||||
/>,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
"/settings/org/" +
|
||||
router.query.id +
|
||||
"?invite"
|
||||
)
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">
|
||||
Add more users to the organization first.
|
||||
{t("section-members:add-dialog.add-user-org-first")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -110,7 +119,7 @@ const AddProjectMemberDialog = ({
|
||||
<Button
|
||||
onButtonPressed={submitModal}
|
||||
color="mineshaft"
|
||||
text="Add Member"
|
||||
text={t("section-members:add-member")}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
@@ -120,7 +129,7 @@ const AddProjectMemberDialog = ({
|
||||
router.push("/settings/org/" + router.query.id)
|
||||
}
|
||||
color="mineshaft"
|
||||
text="Add Users to Organization"
|
||||
text={t("section-members:add-dialog.add-user-to-org")}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import nacl from 'tweetnacl';
|
||||
import { Fragment, useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import nacl from "tweetnacl";
|
||||
|
||||
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import { envMapping } from '../../../public/data/frequentConstants';
|
||||
import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from '../../utilities/cryptography/crypto';
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
import ListBox from '../Listbox';
|
||||
encryptAssymmetric,
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
import ListBox from "../Listbox";
|
||||
|
||||
const expiryMapping = {
|
||||
'1 day': 86400,
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000
|
||||
"1 day": 86400,
|
||||
"7 days": 604800,
|
||||
"1 month": 2592000,
|
||||
"6 months": 15552000,
|
||||
"12 months": 31104000,
|
||||
};
|
||||
|
||||
const AddServiceTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
workspaceId,
|
||||
workspaceName
|
||||
workspaceName,
|
||||
}) => {
|
||||
const [serviceToken, setServiceToken] = useState('');
|
||||
const [serviceTokenName, setServiceTokenName] = useState('');
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState('Development');
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const generateServiceToken = async () => {
|
||||
const latestFileKey = await getLatestFileKey({ workspaceId });
|
||||
@@ -43,7 +45,7 @@ const AddServiceTokenDialog = ({
|
||||
ciphertext: latestFileKey.latestKey.encryptedKey,
|
||||
nonce: latestFileKey.latestKey.nonce,
|
||||
publicKey: latestFileKey.latestKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY')
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY"),
|
||||
});
|
||||
|
||||
// generate new public/private key pair
|
||||
@@ -55,7 +57,7 @@ const AddServiceTokenDialog = ({
|
||||
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey
|
||||
privateKey,
|
||||
});
|
||||
|
||||
let newServiceToken = await addServiceToken({
|
||||
@@ -65,16 +67,16 @@ const AddServiceTokenDialog = ({
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce
|
||||
nonce,
|
||||
});
|
||||
|
||||
const serviceToken = newServiceToken + ',' + privateKey;
|
||||
const serviceToken = newServiceToken + "," + privateKey;
|
||||
setServiceToken(serviceToken);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
var copyText = document.getElementById('serviceToken');
|
||||
var copyText = document.getElementById("serviceToken");
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
@@ -91,8 +93,8 @@ const AddServiceTokenDialog = ({
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
closeModal();
|
||||
setServiceTokenName('');
|
||||
setServiceToken('');
|
||||
setServiceTokenName("");
|
||||
setServiceToken("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -122,27 +124,26 @@ const AddServiceTokenDialog = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
{serviceToken == '' ? (
|
||||
{serviceToken == "" ? (
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
Add a service token for {workspaceName}
|
||||
{t("section-token:add-dialog.title", {
|
||||
target: workspaceName,
|
||||
})}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
Specify the name, environment, and expiry period. When
|
||||
a token is generated, you will only be able to see it
|
||||
once before it disappears. Make sure to save it
|
||||
somewhere.
|
||||
{t("section-token:add-dialog.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<InputField
|
||||
label="Service Token Name"
|
||||
label={t("section-token:add-dialog.name")}
|
||||
onChangeHandler={setServiceTokenName}
|
||||
type="varName"
|
||||
value={serviceTokenName}
|
||||
@@ -155,13 +156,13 @@ const AddServiceTokenDialog = ({
|
||||
selected={serviceTokenEnv}
|
||||
onChange={setServiceTokenEnv}
|
||||
data={[
|
||||
'Development',
|
||||
'Staging',
|
||||
'Production',
|
||||
'Testing'
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
isFull={true}
|
||||
text="Environment: "
|
||||
width="full"
|
||||
text={`${t("common:environment")}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
@@ -169,14 +170,14 @@ const AddServiceTokenDialog = ({
|
||||
selected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months'
|
||||
"1 day",
|
||||
"7 days",
|
||||
"1 month",
|
||||
"6 months",
|
||||
"12 months",
|
||||
]}
|
||||
isFull={true}
|
||||
text="Expires in: "
|
||||
width="full"
|
||||
text={`${t("common:expired-in")}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-max">
|
||||
@@ -184,10 +185,10 @@ const AddServiceTokenDialog = ({
|
||||
<Button
|
||||
onButtonPressed={() => generateServiceToken()}
|
||||
color="mineshaft"
|
||||
text="Add Service Token"
|
||||
textDisabled="Add Service Token"
|
||||
text={t("section-token:add-dialog.add")}
|
||||
textDisabled={t("section-token:add-dialog.add")}
|
||||
size="md"
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
active={serviceTokenName == "" ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,13 +199,14 @@ const AddServiceTokenDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
Copy your service token
|
||||
{t("section-token:add-dialog.copy-service-token")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
Once you close this popup, you will never see your
|
||||
service token again
|
||||
{t(
|
||||
"section-token:add-dialog.copy-service-token-description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +236,7 @@ const AddServiceTokenDialog = ({
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
||||
Click to Copy
|
||||
{t("common.click-to-copy")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
20
frontend/components/dashboard/CommentField.tsx
Normal file
20
frontend/components/dashboard/CommentField.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
/**
|
||||
* This is the text field where people can add comments to particular secrets.
|
||||
*/
|
||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className={`relative mt-4 px-4 pt-4`}>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.comments")}</p>
|
||||
<textarea
|
||||
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default CommentField;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { type ChangeEvent, type DragEvent, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { type ChangeEvent, type DragEvent, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
import parse from '../utilities/file';
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import parse from "../utilities/file";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
interface DropZoneProps {
|
||||
// TODO: change Data type from any
|
||||
@@ -26,8 +27,10 @@ const DropZone = ({
|
||||
errorDragAndDrop,
|
||||
setButtonReady,
|
||||
keysExist,
|
||||
numCurrentRows
|
||||
numCurrentRows,
|
||||
}: DropZoneProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -43,7 +46,7 @@ const DropZone = ({
|
||||
e.stopPropagation();
|
||||
|
||||
// set dropEffect to copy i.e copy of the source item
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -54,7 +57,7 @@ const DropZone = ({
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const reader = new FileReader();
|
||||
@@ -69,7 +72,7 @@ const DropZone = ({
|
||||
pos: numCurrentRows + index,
|
||||
key: key,
|
||||
value: keyPairs[key as keyof typeof keyPairs],
|
||||
type: 'shared'
|
||||
type: "shared",
|
||||
};
|
||||
});
|
||||
setData(newData);
|
||||
@@ -96,16 +99,16 @@ const DropZone = ({
|
||||
reader.onload = (event) => {
|
||||
if (event.target === null || event.target.result === null) return;
|
||||
const { result } = event.target;
|
||||
if (typeof result === 'string') {
|
||||
if (typeof result === "string") {
|
||||
const newData = result
|
||||
.split('\n')
|
||||
.split("\n")
|
||||
.map((line: string, index: number) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key: line.split('=')[0],
|
||||
value: line.split('=').slice(1, line.split('=').length).join('='),
|
||||
type: 'shared'
|
||||
key: line.split("=")[0],
|
||||
value: line.split("=").slice(1, line.split("=").length).join("="),
|
||||
type: "shared",
|
||||
};
|
||||
});
|
||||
setData(newData);
|
||||
@@ -126,7 +129,7 @@ const DropZone = ({
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-bunker outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-gray-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
@@ -147,11 +150,9 @@ const DropZone = ({
|
||||
<div className="flex flex-row">
|
||||
<FontAwesomeIcon
|
||||
icon={faUpload}
|
||||
className="text-gray-300 text-3xl mr-6"
|
||||
className="text-bunker-300 text-3xl mr-6"
|
||||
/>
|
||||
<p className="text-gray-300 mt-1">
|
||||
Drag and drop your .env file here to add more keys.
|
||||
</p>
|
||||
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
|
||||
</div>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="mt-8 max-w-xl opacity-80">
|
||||
@@ -170,7 +171,7 @@ const DropZone = ({
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
||||
<p className="">Drag and drop your .env file here.</p>
|
||||
<p className="">{t("common:drop-zone")}</p>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fragment,useState } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
@@ -10,6 +11,7 @@ import { Menu, Transition } from '@headlessui/react';
|
||||
*/
|
||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
@@ -51,8 +53,8 @@ const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: st
|
||||
icon={faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
<p>{t("dashboard:sidebar.generate-random-hex")}</p>
|
||||
<p>{t("dashboard:sidebar.digits")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SecretVersionList from 'ee/components/SecretVersionList';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
@@ -15,6 +17,7 @@ interface SecretProps {
|
||||
pos: number;
|
||||
type: string;
|
||||
id: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
@@ -22,6 +25,7 @@ interface OverrideProps {
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
@@ -29,6 +33,7 @@ interface SideBarProps {
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
deleteOverride: (id: string) => void;
|
||||
buttonReady: boolean;
|
||||
@@ -56,6 +61,7 @@ const SideBar = ({
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyComment,
|
||||
addOverride,
|
||||
deleteOverride,
|
||||
buttonReady,
|
||||
@@ -64,29 +70,30 @@ const SideBar = ({
|
||||
setSharedToHide
|
||||
}: SideBarProps) => {
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">Secret</p>
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>Key</p>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0].pos}
|
||||
value={data[0].key}
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>Value</p>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
@@ -100,21 +107,22 @@ const SideBar = ({
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
|
||||
This secret is personal. It is not shared with any of your teammates.
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
|
||||
{t("dashboard:sidebar.personal-explanation")}
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0].key}
|
||||
value={data[0].value}
|
||||
pos={data[0].pos}
|
||||
id={data[0].id}
|
||||
keyName={data[0]?.key}
|
||||
value={data[0]?.value}
|
||||
pos={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
comment={data[0]?.comment}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
@@ -124,13 +132,13 @@ const SideBar = ({
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].value : data[0].value}
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0].pos : data[0].pos} />
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,21 +151,12 @@ const SideBar = ({
|
||||
isFull={true}
|
||||
/>
|
||||
</div> */}
|
||||
<div className={`relative mt-4 px-4 pt-4`}>
|
||||
<div className='flex flex-row justify-between'>
|
||||
<p className='text-sm text-bunker-300'>Comments & notes</p>
|
||||
<div className="bg-yellow rounded-md h-min">
|
||||
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
|
||||
Leave your comment here...
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
</div>
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
text={String(t("common:save-changes"))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import CloudIntegration from "./CloudIntegration";
|
||||
|
||||
@@ -24,12 +25,14 @@ const CloudIntegrationSection = ({
|
||||
integrationOptionPress,
|
||||
integrationAuths
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}>
|
||||
<h1 className="font-semibold text-3xl">Cloud Integrations</h1>
|
||||
<h1 className="font-semibold text-3xl">{t("integrations:cloud-integrations")}</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on an integration to begin syncing secrets to it.
|
||||
{t("integrations:click-to-start")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import FrameworkIntegration from "./FrameworkIntegration";
|
||||
|
||||
@@ -15,24 +16,26 @@ interface Props {
|
||||
}
|
||||
|
||||
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">Framework Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on a framework to get the setup instructions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">{t("integrations:framework-integrations")}</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
{t("integrations:click-to-setup")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FrameworkIntegrationSection;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { TFunction, useTranslation } from "next-i18next";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
@@ -12,39 +12,39 @@ import {
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
faRightFromBracket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
|
||||
import logout from '~/pages/api/auth/Logout';
|
||||
import getOrganization from '~/pages/api/organization/GetOrg';
|
||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
||||
import getUser from '~/pages/api/user/getUser';
|
||||
import logout from "~/pages/api/auth/Logout";
|
||||
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
import getOrganization from "../../pages/api/organization/GetOrg";
|
||||
import getOrganizations from "../../pages/api/organization/getOrgs";
|
||||
import getUser from "../../pages/api/user/getUser";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
const supportOptions = [
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||
'Join Slack Forum',
|
||||
'https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g'
|
||||
t("nav:support.slack"),
|
||||
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
'Read Docs',
|
||||
'https://infisical.com/docs/getting-started/introduction'
|
||||
t("nav:support.docs"),
|
||||
"https://infisical.com/docs/getting-started/introduction",
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||
'Open a GitHub Issue',
|
||||
'https://github.com/Infisical/infisical-cli/issues'
|
||||
t("nav:support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues",
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
'Send us an Email',
|
||||
'mailto:support@infisical.com'
|
||||
]
|
||||
t("nav:support.email"),
|
||||
"mailto:support@infisical.com",
|
||||
],
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
@@ -68,6 +68,10 @@ export default function Navbar() {
|
||||
const [orgs, setOrgs] = useState([]);
|
||||
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const userData = await getUser();
|
||||
@@ -75,16 +79,16 @@ export default function Navbar() {
|
||||
const orgsData = await getOrganizations();
|
||||
setOrgs(orgsData);
|
||||
const currentOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem('orgData.id'))
|
||||
orgId: String(localStorage.getItem("orgData.id")),
|
||||
});
|
||||
setCurrentOrg(currentOrg);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
console.log('Logging out...');
|
||||
console.log("Logging out...");
|
||||
await logout();
|
||||
router.push('/login');
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -119,18 +123,17 @@ export default function Navbar() {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
|
||||
{supportOptions.map((option) => (
|
||||
// eslint-disable-next-line react/jsx-no-target-blank
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={String(option[2])}
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full">
|
||||
{option[0]}
|
||||
<div className="text-sm">{option[1]}</div>
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
@@ -159,11 +162,11 @@ export default function Navbar() {
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
SIGNED IN AS
|
||||
{t("nav:user.signed-in-as")}
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push('/settings/personal/' + router.query.id)
|
||||
router.push("/settings/personal/" + router.query.id)
|
||||
}
|
||||
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
@@ -173,11 +176,11 @@ export default function Navbar() {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-gray-300 px-2 pt-1 text-sm">
|
||||
{' '}
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="text-gray-400 px-2 pb-1 text-xs">
|
||||
{' '}
|
||||
{" "}
|
||||
{user?.email}
|
||||
</p>
|
||||
</div>
|
||||
@@ -190,11 +193,11 @@ export default function Navbar() {
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
CURRENT ORGANIZATION
|
||||
{t("nav:user.current-organization")}
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push('/settings/org/' + router.query.id)
|
||||
router.push("/settings/org/" + router.query.id)
|
||||
}
|
||||
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
@@ -217,7 +220,7 @@ export default function Navbar() {
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push('/settings/billing/' + router.query.id)
|
||||
router.push("/settings/billing/" + router.query.id)
|
||||
}
|
||||
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
||||
>
|
||||
@@ -225,7 +228,7 @@ export default function Navbar() {
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faCoins}
|
||||
/>
|
||||
<div className="text-sm">Usage & Billing</div>
|
||||
<div className="text-sm">{t("nav:user.usage-billing")}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@@ -235,7 +238,7 @@ export default function Navbar() {
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/settings/org/' + router.query.id + '?invite'
|
||||
"/settings/org/" + router.query.id + "?invite"
|
||||
)
|
||||
}
|
||||
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
||||
@@ -243,26 +246,26 @@ export default function Navbar() {
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="text-sm ml-1">Invite Members</div>
|
||||
<div className="text-sm ml-1">{t("nav:user.invite")}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
OTHER ORGANIZATIONS
|
||||
{t("nav:user.other-organizations")}
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||
{orgs
|
||||
.filter(
|
||||
(org: { _id: string }) =>
|
||||
org._id != localStorage.getItem('orgData.id')
|
||||
org._id != localStorage.getItem("orgData.id")
|
||||
)
|
||||
.map((org: { _id: string; name: string }) => (
|
||||
<div
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
localStorage.setItem('orgData.id', org._id);
|
||||
localStorage.setItem("orgData.id", org._id);
|
||||
router.reload();
|
||||
}}
|
||||
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
||||
@@ -287,8 +290,8 @@ export default function Navbar() {
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
active
|
||||
? 'bg-red font-semibold text-white'
|
||||
: 'text-gray-400'
|
||||
? "bg-red font-semibold text-white"
|
||||
: "text-gray-400"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
||||
@@ -296,7 +299,7 @@ export default function Navbar() {
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
Log Out
|
||||
{t("common:logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -16,12 +16,19 @@ export default class SecurityClient {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
if (this.#token == '') {
|
||||
this.setToken(await token());
|
||||
try {
|
||||
// TODO: This should be moved to a context to do it only once when app loads
|
||||
// this try catch saves route guard from a stuck state
|
||||
this.setToken(await token());
|
||||
} catch (error) {
|
||||
console.error("Unauthorized access");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#token) {
|
||||
req.headers.set('Authorization', 'Bearer ' + this.#token);
|
||||
return fetch(req);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ const attemptLogin = async (
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||
await login2(email, clientProof);
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
@@ -108,15 +109,16 @@ const attemptLogin = async (
|
||||
if (isSignUp) {
|
||||
await pushKeys({
|
||||
obj: {
|
||||
DATABASE_URL: [
|
||||
sDATABASE_URL: [
|
||||
'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||
'personal'
|
||||
'This is an example of secret referencing.'
|
||||
],
|
||||
DB_USERNAME: ['user1234', 'personal'],
|
||||
DB_PASSWORD: ['example_password', 'personal'],
|
||||
TWILIO_AUTH_TOKEN: ['example_twillion_token', 'shared'],
|
||||
WEBSITE_URL: ['http://localhost:3000', 'shared'],
|
||||
STRIPE_SECRET_KEY: ['sk_test_7348oyho4hfq398HIUOH78', 'shared']
|
||||
sDB_USERNAME: ['OVERRIDE_THIS', ''],
|
||||
sDB_PASSWORD: ['OVERRIDE_THIS', ''],
|
||||
pDB_USERNAME: ['user1234', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
|
||||
pDB_PASSWORD: ['example_password', 'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need.'],
|
||||
sTWILIO_AUTH_TOKEN: ['example_twillio_token', ''],
|
||||
sWEBSITE_URL: ['http://localhost:3000', ''],
|
||||
},
|
||||
workspaceId: projectToLogin,
|
||||
env: 'Development'
|
||||
|
||||
@@ -10,6 +10,14 @@ const {
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
env: keyof typeof envMapping;
|
||||
setFileState: any;
|
||||
@@ -34,12 +42,12 @@ const getSecretsForProject = async ({
|
||||
} catch (error) {
|
||||
console.log('ERROR: Not able to access the latest file');
|
||||
}
|
||||
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs
|
||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
|
||||
const tempFileState: SecretProps[] = [];
|
||||
if (file.key) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
@@ -64,10 +72,25 @@ const getSecretsForProject = async ({
|
||||
tag: secretPair.secretValue.tag,
|
||||
key
|
||||
});
|
||||
|
||||
let plainTextComment;
|
||||
if (secretPair.secretComment.ciphertext) {
|
||||
plainTextComment = decryptSymmetric({
|
||||
ciphertext: secretPair.secretComment.ciphertext,
|
||||
iv: secretPair.secretComment.iv,
|
||||
tag: secretPair.secretComment.tag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = "";
|
||||
}
|
||||
|
||||
tempFileState.push({
|
||||
id: secretPair._id,
|
||||
key: plainTextKey,
|
||||
value: plainTextValue,
|
||||
type: secretPair.type
|
||||
type: secretPair.type,
|
||||
comment: plainTextComment
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -76,22 +99,24 @@ const getSecretsForProject = async ({
|
||||
setData(
|
||||
tempFileState.map((line, index) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
id: line['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type']
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return tempFileState.map((line, index) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
id: line['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type']
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -47,9 +47,9 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
const secrets = Object.keys(obj).map((key) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: ciphertextKey,
|
||||
iv: ivKey,
|
||||
tag: tagKey,
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key.slice(1),
|
||||
key: randomBytes,
|
||||
@@ -57,25 +57,39 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: ciphertextValue,
|
||||
iv: ivValue,
|
||||
tag: tagValue,
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][0],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][1],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||
|
||||
return {
|
||||
ciphertextKey,
|
||||
ivKey,
|
||||
tagKey,
|
||||
hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
|
||||
type: visibility,
|
||||
};
|
||||
});
|
||||
|
||||
52
frontend/components/utilities/withTranslateProps.ts
Normal file
52
frontend/components/utilities/withTranslateProps.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { GetServerSideProps, GetStaticProps } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
const DefaultNamespaces = ["common", "nav"];
|
||||
|
||||
type GetTranslatedStaticProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetStaticProps<any>
|
||||
) => GetStaticProps;
|
||||
|
||||
type GetTranslatedServerSideProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetServerSideProps<any>
|
||||
) => GetServerSideProps;
|
||||
|
||||
export const getTranslatedStaticProps: GetTranslatedStaticProps =
|
||||
(namespaces, getStaticProps) =>
|
||||
async ({ locale, ...context }) => {
|
||||
let staticProps = { props: {} };
|
||||
if (typeof getStaticProps === "function") {
|
||||
staticProps = (await getStaticProps(context)) as any;
|
||||
}
|
||||
return {
|
||||
...staticProps,
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? "en", [
|
||||
...DefaultNamespaces,
|
||||
...namespaces,
|
||||
])),
|
||||
...staticProps.props,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getTranslatedServerSideProps: GetTranslatedServerSideProps =
|
||||
(namespaces, getServerSideProps) =>
|
||||
async ({ locale, ...context }) => {
|
||||
let staticProps = { props: {} };
|
||||
if (typeof getServerSideProps === "function") {
|
||||
staticProps = (await getServerSideProps(context)) as any;
|
||||
}
|
||||
return {
|
||||
...staticProps,
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? "en", [
|
||||
...DefaultNamespaces,
|
||||
...namespaces,
|
||||
])),
|
||||
...staticProps.props,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -16,3 +16,8 @@ export const publicPaths = [
|
||||
`/terms`,
|
||||
`/subprocessors`,
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
en: "English",
|
||||
ko: "한국어",
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user