parent
8d00c5cdb7
commit
bea0ff6e05
@ -0,0 +1,70 @@
|
|||||||
|
# Keys
|
||||||
|
# Required keys for platform encryption/decryption ops
|
||||||
|
PRIVATE_KEY=replace_with_nacl_sk
|
||||||
|
PUBLIC_KEY=replace_with_nacl_pk
|
||||||
|
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
# Required secrets to sign JWT tokens
|
||||||
|
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex
|
||||||
|
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex
|
||||||
|
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
|
# JWT lifetime
|
||||||
|
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
||||||
|
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
||||||
|
JWT_AUTH_LIFETIME=
|
||||||
|
JWT_REFRESH_LIFETIME=
|
||||||
|
JWT_SERVICE_SECRET=
|
||||||
|
JWT_SIGNUP_LIFETIME=
|
||||||
|
|
||||||
|
# Optional lifetimes for OTP expressed in seconds
|
||||||
|
EMAIL_TOKEN_LIFETIME=
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
||||||
|
# to the MongoDB container instance or Mongo Cloud
|
||||||
|
# Required
|
||||||
|
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||||
|
|
||||||
|
# Optional credentials for MongoDB container instance
|
||||||
|
MONGO_USERNAME=root
|
||||||
|
MONGO_PASSWORD=example
|
||||||
|
|
||||||
|
# Mongo-Express vars (needed for development only)
|
||||||
|
ME_CONFIG_MONGODB_ADMINUSERNAME=root
|
||||||
|
ME_CONFIG_MONGODB_ADMINPASSWORD=example
|
||||||
|
ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
|
||||||
|
|
||||||
|
# Website URL
|
||||||
|
# Required
|
||||||
|
NODE_ENV=development
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Mail/SMTP
|
||||||
|
# Required to send emails
|
||||||
|
# By default, SMTP_HOST is set to smtp.gmail.com
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_NAME=Team
|
||||||
|
SMTP_USERNAME=team@infisical.com
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
|
# Integration
|
||||||
|
# Optional only if integration is used
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU=
|
||||||
|
OAUTH_TOKEN_URL_HEROKU=
|
||||||
|
|
||||||
|
# Sentry (optional) for monitoring errors
|
||||||
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
# Infisical Cloud-specific configs
|
||||||
|
# Ignore - Not applicable for self-hosted version
|
||||||
|
POSTHOG_HOST=
|
||||||
|
POSTHOG_PROJECT_API_KEY=
|
||||||
|
STRIPE_SECRET_KEY=
|
||||||
|
STRIPE_PUBLISHABLE_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
STRIPE_PRODUCT_CARD_AUTH=
|
||||||
|
STRIPE_PRODUCT_PRO=
|
||||||
|
STRIPE_PRODUCT_STARTER=
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
@ -0,0 +1,40 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# run only against tags
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
# packages: write
|
||||||
|
# issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '>=1.19.3'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: cli/go.sum
|
||||||
|
# More assembly might be required: Docker logins, GPG, etc. It all depends
|
||||||
|
# on your needs.
|
||||||
|
- uses: goreleaser/goreleaser-action@v2
|
||||||
|
with:
|
||||||
|
# either 'goreleaser' (default) or 'goreleaser-pro':
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --rm-dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||||
|
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||||
|
# Your GoReleaser Pro key, if you are using the 'goreleaser-pro'
|
||||||
|
# distribution:
|
||||||
|
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
@ -1,9 +1,51 @@
|
|||||||
|
# backend
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
.env.dev
|
||||||
|
.env.prod
|
||||||
.env.infisical
|
.env.infisical
|
||||||
.DS_STORE
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
*.swn
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# frontend
|
||||||
|
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.vercel
|
||||||
|
.env.infisical
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||||
|
# Make sure to check the documentation at https://goreleaser.com
|
||||||
|
# before:
|
||||||
|
# hooks:
|
||||||
|
# # You may remove this if you don't use go modules.
|
||||||
|
# - cd cli && go mod tidy
|
||||||
|
# # you may remove this if you don't need go generate
|
||||||
|
# - cd cli && go generate ./...
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
binary: infisical
|
||||||
|
goos:
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
id: infisical
|
||||||
|
goarch:
|
||||||
|
- arm
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
goarm:
|
||||||
|
- 5
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
|
dir: ./cli
|
||||||
|
|
||||||
|
release:
|
||||||
|
replace_existing_draft: true
|
||||||
|
mode: 'replace'
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
publishers:
|
||||||
|
- name: fury.io
|
||||||
|
ids:
|
||||||
|
- infisical
|
||||||
|
dir: "{{ dir .ArtifactPath }}"
|
||||||
|
cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
|
||||||
|
|
||||||
|
# publishers:
|
||||||
|
# - name: fury.io
|
||||||
|
# ids:
|
||||||
|
# - infisical
|
||||||
|
# dir: "{{ dir .ArtifactPath }}"
|
||||||
|
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
|
||||||
|
brews:
|
||||||
|
- name: infisical
|
||||||
|
tap:
|
||||||
|
owner: Infisical
|
||||||
|
name: homebrew-get-cli
|
||||||
|
commit_author:
|
||||||
|
name: "Infisical"
|
||||||
|
email: ai@infisical.com
|
||||||
|
folder: Formula
|
||||||
|
homepage: "https://infisical.com"
|
||||||
|
description: "The official Infisical CLI"
|
||||||
|
nfpms:
|
||||||
|
- id: infisical
|
||||||
|
file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
package_name: infisical
|
||||||
|
builds:
|
||||||
|
- infisical
|
||||||
|
vendor: Infisical, Inc
|
||||||
|
homepage: https://infisical.com/
|
||||||
|
maintainer: Infisical, Inc
|
||||||
|
description: The offical Infisical CLI
|
||||||
|
license: Apache 2.0
|
||||||
|
formats:
|
||||||
|
- rpm
|
||||||
|
- deb
|
||||||
|
- apk
|
||||||
|
bindir: /usr/bin
|
||||||
|
scoop:
|
||||||
|
bucket:
|
||||||
|
owner: Infisical
|
||||||
|
name: scoop-infisical
|
||||||
|
commit_author:
|
||||||
|
name: "Infisical"
|
||||||
|
email: ai@infisical.com
|
||||||
|
homepage: "https://infisical.com"
|
||||||
|
description: "The official Infisical CLI"
|
||||||
|
license: Apache-2.0
|
||||||
|
# dockers:
|
||||||
|
# - dockerfile: goreleaser.dockerfile
|
||||||
|
# goos: linux
|
||||||
|
# goarch: amd64
|
||||||
|
# ids:
|
||||||
|
# - infisical
|
||||||
|
# image_templates:
|
||||||
|
# - "infisical/cli:{{ .Version }}"
|
||||||
|
# - "infisical/cli:{{ .Major }}.{{ .Minor }}"
|
||||||
|
# - "infisical/cli:{{ .Major }}"
|
||||||
|
# - "infisical/cli:latest"
|
||||||
|
# build_flag_templates:
|
||||||
|
# - "--label=org.label-schema.schema-version=1.0"
|
||||||
|
# - "--label=org.label-schema.version={{.Version}}"
|
||||||
|
# - "--label=org.label-schema.name={{.ProjectName}}"
|
||||||
|
# - "--platform=linux/amd64"
|
@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
team@infisical.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
@ -0,0 +1,5 @@
|
|||||||
|
# Contributing to Infisical
|
||||||
|
|
||||||
|
Thanks for taking the time to contribute!
|
||||||
|
|
||||||
|
Please refer to our Contributing Guide for instructions on how to contribute.
|
@ -0,0 +1,14 @@
|
|||||||
|
build:
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml build
|
||||||
|
|
||||||
|
push:
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml push
|
||||||
|
|
||||||
|
up-dev:
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
up-prod:
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker-compose down
|
@ -0,0 +1,9 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We always recommend using the latest version of Infisical to ensure you get all security updates.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security vulnerabilities or concerns to team@infisical.com.
|
@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.*
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
built
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-console": 2,
|
||||||
|
"prettier/prettier": 2
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
|
|
@ -0,0 +1,37 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
EMAIL_TOKEN_LIFETIME: string;
|
||||||
|
ENCRYPTION_KEY: string;
|
||||||
|
JWT_AUTH_LIFETIME: string;
|
||||||
|
JWT_AUTH_SECRET: string;
|
||||||
|
JWT_REFRESH_LIFETIME: string;
|
||||||
|
JWT_REFRESH_SECRET: string;
|
||||||
|
JWT_SERVICE_SECRET: string;
|
||||||
|
JWT_SIGNUP_LIFETIME: string;
|
||||||
|
JWT_SIGNUP_SECRET: string;
|
||||||
|
MONGO_URL: string;
|
||||||
|
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU: string;
|
||||||
|
OAUTH_TOKEN_URL_HEROKU: string;
|
||||||
|
POSTHOG_HOST: string;
|
||||||
|
POSTHOG_PROJECT_API_KEY: string;
|
||||||
|
PRIVATE_KEY: string;
|
||||||
|
PUBLIC_KEY: string;
|
||||||
|
SENTRY_DSN: string;
|
||||||
|
SMTP_HOST: string;
|
||||||
|
SMTP_NAME: string;
|
||||||
|
SMTP_PASSWORD: string;
|
||||||
|
SMTP_USERNAME: string;
|
||||||
|
STRIPE_PRODUCT_CARD_AUTH: string;
|
||||||
|
STRIPE_PRODUCT_PRO: string;
|
||||||
|
STRIPE_PRODUCT_STARTER: string;
|
||||||
|
STRIPE_PUBLISHABLE_KEY: string;
|
||||||
|
STRIPE_SECRET_KEY: string;
|
||||||
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
WEBSITE_URL: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 493 KiB |
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": ".ts,.js",
|
||||||
|
"ignore": [],
|
||||||
|
"exec": "ts-node ./src/index.ts"
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/node": "^7.14.0",
|
||||||
|
"@sentry/tracing": "^7.14.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"bigint-conversion": "^2.2.2",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"express-rate-limit": "^6.5.1",
|
||||||
|
"express-validator": "^6.14.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
|
"helmet": "^5.1.1",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"jsrp": "^0.2.4",
|
||||||
|
"mongoose": "^6.7.1",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
|
"posthog-node": "^2.1.0",
|
||||||
|
"query-string": "^7.1.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"stripe": "^10.7.0",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
},
|
||||||
|
"name": "infisical-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "npm run build && node build/index.js",
|
||||||
|
"dev": "nodemon",
|
||||||
|
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
|
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/Infisical/infisical-api.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Infisical/infisical-api/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@posthog/plugin-scaffold": "^1.3.4",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/express": "^4.17.14",
|
||||||
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
|
"@types/node": "^18.11.3",
|
||||||
|
"@types/nodemailer": "^6.4.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||||
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
|
"eslint": "^8.26.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"husky": "^8.0.1",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"nodemon": "^2.0.19",
|
||||||
|
"npm": "^8.19.3",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"ts-node": "^10.9.1"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
const PORT = process.env.PORT || 4000;
|
||||||
|
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; // investigate
|
||||||
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
||||||
|
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||||
|
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
||||||
|
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
||||||
|
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
|
||||||
|
const JWT_SERVICE_SECRET = process.env.JWT_SERVICE_SECRET!;
|
||||||
|
const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
||||||
|
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||||
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
|
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
||||||
|
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
||||||
|
const POSTHOG_HOST = process.env.POSTHOG_HOST!;
|
||||||
|
const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY!;
|
||||||
|
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
|
||||||
|
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
|
||||||
|
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
||||||
|
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com';
|
||||||
|
const SMTP_NAME = process.env.SMTP_NAME!;
|
||||||
|
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
|
||||||
|
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
||||||
|
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
|
||||||
|
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
|
||||||
|
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
||||||
|
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||||
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||||
|
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
|
const WEBSITE_URL = 'http://frontend:3000';
|
||||||
|
|
||||||
|
export {
|
||||||
|
PORT,
|
||||||
|
EMAIL_TOKEN_LIFETIME,
|
||||||
|
ENCRYPTION_KEY,
|
||||||
|
JWT_AUTH_LIFETIME,
|
||||||
|
JWT_AUTH_SECRET,
|
||||||
|
JWT_REFRESH_LIFETIME,
|
||||||
|
JWT_REFRESH_SECRET,
|
||||||
|
JWT_SERVICE_SECRET,
|
||||||
|
JWT_SIGNUP_LIFETIME,
|
||||||
|
JWT_SIGNUP_SECRET,
|
||||||
|
MONGO_URL,
|
||||||
|
NODE_ENV,
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU,
|
||||||
|
POSTHOG_HOST,
|
||||||
|
POSTHOG_PROJECT_API_KEY,
|
||||||
|
PRIVATE_KEY,
|
||||||
|
PUBLIC_KEY,
|
||||||
|
SENTRY_DSN,
|
||||||
|
SMTP_HOST,
|
||||||
|
SMTP_NAME,
|
||||||
|
SMTP_USERNAME,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
STRIPE_PRODUCT_CARD_AUTH,
|
||||||
|
STRIPE_PRODUCT_PRO,
|
||||||
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PUBLISHABLE_KEY,
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
WEBSITE_URL
|
||||||
|
};
|
@ -0,0 +1,224 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
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 {
|
||||||
|
NODE_ENV,
|
||||||
|
JWT_AUTH_LIFETIME,
|
||||||
|
JWT_AUTH_SECRET,
|
||||||
|
JWT_REFRESH_SECRET
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPublicKeys: any = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
clientPublicKey
|
||||||
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// generate server-side public key
|
||||||
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
clientPublicKeys[email] = {
|
||||||
|
clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to start authentication process'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||||
|
* private key
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email, clientProof } = req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: clientPublicKeys[email].serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(clientPublicKeys[email].clientPublicKey);
|
||||||
|
|
||||||
|
// compare server and client shared keys
|
||||||
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({ userId: user._id.toString() });
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/token',
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
|
});
|
||||||
|
|
||||||
|
// return (access) token in response
|
||||||
|
return res.status(200).send({
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const logout = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
await clearTokens({
|
||||||
|
userId: req.user._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// clear httpOnly cookie
|
||||||
|
res.cookie('jid', '', {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/token',
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: NODE_ENV === 'production' ? true : false
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to logout'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully logged out.'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return user is authenticated
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const checkAuth = async (req: Request, res: Response) =>
|
||||||
|
res.status(200).send({
|
||||||
|
message: 'Authenticated'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new token by redeeming refresh token
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getNewToken = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const refreshToken = req.cookies.jid;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('Failed to find token in request cookies');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||||
|
if (!user?.publicKey)
|
||||||
|
throw new Error('Failed to authenticate not fully set up account');
|
||||||
|
|
||||||
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: decodedToken.userId
|
||||||
|
},
|
||||||
|
expiresIn: JWT_AUTH_LIFETIME,
|
||||||
|
secret: JWT_AUTH_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Invalid request'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
import * as authController from './authController';
|
||||||
|
import * as integrationAuthController from './integrationAuthController';
|
||||||
|
import * as integrationController from './integrationController';
|
||||||
|
import * as keyController from './keyController';
|
||||||
|
import * as membershipController from './membershipController';
|
||||||
|
import * as membershipOrgController from './membershipOrgController';
|
||||||
|
import * as organizationController from './organizationController';
|
||||||
|
import * as passwordController from './passwordController';
|
||||||
|
import * as secretController from './secretController';
|
||||||
|
import * as serviceTokenController from './serviceTokenController';
|
||||||
|
import * as signupController from './signupController';
|
||||||
|
import * as stripeController from './stripeController';
|
||||||
|
import * as userActionController from './userActionController';
|
||||||
|
import * as userController from './userController';
|
||||||
|
import * as workspaceController from './workspaceController';
|
||||||
|
|
||||||
|
export {
|
||||||
|
authController,
|
||||||
|
integrationAuthController,
|
||||||
|
integrationController,
|
||||||
|
keyController,
|
||||||
|
membershipController,
|
||||||
|
membershipOrgController,
|
||||||
|
organizationController,
|
||||||
|
passwordController,
|
||||||
|
secretController,
|
||||||
|
serviceTokenController,
|
||||||
|
signupController,
|
||||||
|
stripeController,
|
||||||
|
userActionController,
|
||||||
|
userController,
|
||||||
|
workspaceController
|
||||||
|
};
|
@ -0,0 +1,153 @@
|
|||||||
|
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 { processOAuthTokenRes } from '../helpers/integrationAuth';
|
||||||
|
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
||||||
|
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
|
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const integrationAuthOauthExchange = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let clientSecret;
|
||||||
|
|
||||||
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
|
if (!INTEGRATION_SET.has(integration))
|
||||||
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
|
// use correct client secret
|
||||||
|
switch (integration) {
|
||||||
|
case 'heroku':
|
||||||
|
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: unfinished - make compatible with other integration types
|
||||||
|
const res = await axios.post(
|
||||||
|
OAUTH_TOKEN_URL_HEROKU!,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_secret: clientSecret
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
const integrationAuth = await processOAuthTokenRes({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
res
|
||||||
|
});
|
||||||
|
|
||||||
|
// create or replace integration
|
||||||
|
const integrationObj = await Integration.findOneAndUpdate(
|
||||||
|
{ workspace: workspaceId, integration },
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: ENV_DEV,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
integration,
|
||||||
|
integrationAuth: integrationAuth._id
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get OAuth2 token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully enabled integration authorization'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of applications allowed for integration with id [integrationAuthId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
|
// TODO: unfinished - make compatible with other integration types
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = await axios.get('https://api.heroku.com/apps', {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: 'Bearer ' + req.accessToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apps = res.data.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apps
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete integration authorization with id [integrationAuthId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
|
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
||||||
|
try {
|
||||||
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
|
// TODO: disable application via Heroku API; figure out what authorization id is
|
||||||
|
|
||||||
|
const integrations = JSON.parse(
|
||||||
|
readFileSync('./src/json/integrations.json').toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
let authorizationId;
|
||||||
|
switch (req.integrationAuth.integration) {
|
||||||
|
case 'heroku':
|
||||||
|
authorizationId = integrations.heroku.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not sure what authorizationId is?
|
||||||
|
// // revoke authorization
|
||||||
|
// const res2 = await axios.delete(
|
||||||
|
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// 'Accept': 'application/vnd.heroku+json; version=3',
|
||||||
|
// 'Authorization': 'Bearer ' + req.accessToken
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
|
_id: integrationAuthId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletedIntegrationAuth) {
|
||||||
|
await Integration.deleteMany({
|
||||||
|
integrationAuth: deletedIntegrationAuth._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete integration authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,158 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Integration } from '../models';
|
||||||
|
import { decryptAsymmetric } from '../utils/crypto';
|
||||||
|
import { decryptSecrets } from '../helpers/secret';
|
||||||
|
import { PRIVATE_KEY } from '../config';
|
||||||
|
|
||||||
|
interface Key {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of all available integrations on Infisical
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getIntegrations = async (req: Request, res: Response) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
integrations = JSON.parse(
|
||||||
|
readFileSync('./src/json/integrations.json').toString()
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get integrations'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integrations
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync secrets [secrets] to integration with id [integrationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const syncIntegration = async (req: Request, res: Response) => {
|
||||||
|
// TODO: unfinished - make more versatile to accomodate for other integrations
|
||||||
|
try {
|
||||||
|
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
||||||
|
const symmetricKey = decryptAsymmetric({
|
||||||
|
ciphertext: key.encryptedKey,
|
||||||
|
nonce: key.nonce,
|
||||||
|
publicKey: req.user.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
// decrypt secrets with symmetric key
|
||||||
|
const content = decryptSecrets({
|
||||||
|
secrets,
|
||||||
|
key: symmetricKey,
|
||||||
|
format: 'object'
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: make integration work for other integrations as well
|
||||||
|
const res = await axios.patch(
|
||||||
|
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
||||||
|
content,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: 'Bearer ' + req.accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to sync secrets with integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully synced secrets with integration'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change environment or name of integration with id [integrationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const modifyIntegration = async (req: Request, res: Response) => {
|
||||||
|
let integration;
|
||||||
|
try {
|
||||||
|
const { update } = req.body;
|
||||||
|
|
||||||
|
integration = await Integration.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: req.integration._id
|
||||||
|
},
|
||||||
|
update,
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to modify integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integration
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete integration with id [integrationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
|
let deletedIntegration;
|
||||||
|
try {
|
||||||
|
const { integrationId } = req.params;
|
||||||
|
|
||||||
|
deletedIntegration = await Integration.findOneAndDelete({
|
||||||
|
_id: integrationId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
deletedIntegration
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,109 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Key } from '../models';
|
||||||
|
import { findMembership } from '../helpers/membership';
|
||||||
|
import { PUBLIC_KEY } from '../config';
|
||||||
|
import { GRANTED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||||
|
* id [key.userId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const uploadKey = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { key } = req.body;
|
||||||
|
|
||||||
|
// validate membership of sender
|
||||||
|
const senderMembership = await findMembership({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!senderMembership) {
|
||||||
|
throw new Error('Failed sender membership validation for workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate membership of receiver
|
||||||
|
const receiverMembership = await findMembership({
|
||||||
|
user: key.userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!receiverMembership) {
|
||||||
|
throw new Error('Failed receiver membership validation for workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverMembership.status = GRANTED;
|
||||||
|
await receiverMembership.save();
|
||||||
|
|
||||||
|
await new Key({
|
||||||
|
encryptedKey: key.encryptedKey,
|
||||||
|
nonce: key.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
receiver: key.userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to upload key to workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully uploaded key to workspace'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return latest (encrypted) copy of workspace key for user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getLatestKey = async (req: Request, res: Response) => {
|
||||||
|
let latestKey;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// get latest key
|
||||||
|
latestKey = await Key.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
receiver: req.user._id
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(1)
|
||||||
|
.populate('sender', '+publicKey');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get latest key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resObj: any = {};
|
||||||
|
|
||||||
|
if (latestKey.length > 0) {
|
||||||
|
resObj['latestKey'] = latestKey[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(resObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public key of Infisical
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
publicKey: PUBLIC_KEY
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,236 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Membership, MembershipOrg, User, Key } from '../models';
|
||||||
|
import {
|
||||||
|
findMembership,
|
||||||
|
deleteMembership as deleteMember
|
||||||
|
} from '../helpers/membership';
|
||||||
|
import { sendMail } from '../helpers/nodemailer';
|
||||||
|
import { WEBSITE_URL } from '../config';
|
||||||
|
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that user is a member of workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const validateMembership = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// validate membership
|
||||||
|
const membership = await findMembership({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error('Failed to validate membership');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed workspace connection check'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Workspace membership confirmed'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete membership with id [membershipId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteMembership = async (req: Request, res: Response) => {
|
||||||
|
let deletedMembership;
|
||||||
|
try {
|
||||||
|
const { membershipId } = req.params;
|
||||||
|
|
||||||
|
// check if membership to delete exists
|
||||||
|
const membershipToDelete = await Membership.findOne({
|
||||||
|
_id: membershipId
|
||||||
|
}).populate('user');
|
||||||
|
|
||||||
|
if (!membershipToDelete) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to delete workspace membership that doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user is a member and admin of the workspace
|
||||||
|
// whose membership we wish to delete
|
||||||
|
const membership = await Membership.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: membershipToDelete.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error('Failed to validate workspace membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membership.role !== ADMIN) {
|
||||||
|
// user is not an admin member of the workspace
|
||||||
|
throw new Error('Insufficient role for deleting workspace membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete workspace membership
|
||||||
|
deletedMembership = await deleteMember({
|
||||||
|
membershipId: membershipToDelete._id.toString()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
deletedMembership
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change and return workspace membership role
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||||
|
let membershipToChangeRole;
|
||||||
|
try {
|
||||||
|
const { membershipId } = req.params;
|
||||||
|
const { role } = req.body;
|
||||||
|
|
||||||
|
if (![ADMIN, MEMBER].includes(role)) {
|
||||||
|
throw new Error('Failed to validate role');
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate target membership
|
||||||
|
membershipToChangeRole = await findMembership({
|
||||||
|
_id: membershipId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipToChangeRole) {
|
||||||
|
throw new Error('Failed to find membership to change role');
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user is a member and admin of target membership's
|
||||||
|
// workspace
|
||||||
|
const membership = await findMembership({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: membershipToChangeRole.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error('Failed to validate membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membership.role !== ADMIN) {
|
||||||
|
// user is not an admin member of the workspace
|
||||||
|
throw new Error('Insufficient role for changing member roles');
|
||||||
|
}
|
||||||
|
|
||||||
|
membershipToChangeRole.role = role;
|
||||||
|
await membershipToChangeRole.save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to change membership role'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
membership: membershipToChangeRole
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add user with email [email] to workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||||
|
let invitee, latestKey;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { email }: { email: string } = req.body;
|
||||||
|
|
||||||
|
invitee = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!invitee || !invitee?.publicKey)
|
||||||
|
throw new Error('Failed to validate invitee');
|
||||||
|
|
||||||
|
// validate invitee's workspace membership - ensure member isn't
|
||||||
|
// already a member of the workspace
|
||||||
|
const inviteeMembership = await Membership.findOne({
|
||||||
|
user: invitee._id,
|
||||||
|
workspace: workspaceId,
|
||||||
|
status: GRANTED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inviteeMembership)
|
||||||
|
throw new Error('Failed to add existing member of workspace');
|
||||||
|
|
||||||
|
// validate invitee's organization membership - ensure that only
|
||||||
|
// (accepted) organization members can be added to the workspace
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: invitee._id,
|
||||||
|
organization: req.membership.workspace.organization,
|
||||||
|
status: ACCEPTED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg)
|
||||||
|
throw new Error("Failed to validate invitee's organization membership");
|
||||||
|
|
||||||
|
// get latest key
|
||||||
|
latestKey = await Key.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
receiver: req.user._id
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.populate('sender', '+publicKey');
|
||||||
|
|
||||||
|
// create new workspace membership
|
||||||
|
const m = await new Membership({
|
||||||
|
user: invitee._id,
|
||||||
|
workspace: workspaceId,
|
||||||
|
role: MEMBER,
|
||||||
|
status: GRANTED
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
template: 'workspaceInvitation.handlebars',
|
||||||
|
subjectLine: 'Infisical workspace invitation',
|
||||||
|
recipients: [invitee.email],
|
||||||
|
substitutions: {
|
||||||
|
inviterFirstName: req.user.firstName,
|
||||||
|
inviterEmail: req.user.email,
|
||||||
|
workspaceName: req.membership.workspace.name,
|
||||||
|
callback_url: WEBSITE_URL + '/login'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to invite user to workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
invitee,
|
||||||
|
latestKey
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,269 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { WEBSITE_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
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
||||||
|
let membershipOrgToDelete;
|
||||||
|
try {
|
||||||
|
const { membershipOrgId } = req.params;
|
||||||
|
|
||||||
|
// check if organization membership to delete exists
|
||||||
|
membershipOrgToDelete = await MembershipOrg.findOne({
|
||||||
|
_id: membershipOrgId
|
||||||
|
}).populate('user');
|
||||||
|
|
||||||
|
if (!membershipOrgToDelete) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to delete organization membership that doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user is a member and admin of the organization
|
||||||
|
// whose membership we wish to delete
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
organization: membershipOrgToDelete.organization
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg) {
|
||||||
|
throw new Error('Failed to validate organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
||||||
|
// user is not an admin member of the organization
|
||||||
|
throw new Error('Insufficient role for deleting organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete organization membership
|
||||||
|
const deletedMembershipOrg = await deleteMemberFromOrg({
|
||||||
|
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSubscriptionOrgQuantity({
|
||||||
|
organizationId: membershipOrg.organization.toString()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete organization membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return membershipOrgToDelete;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change and return organization membership role
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||||
|
// change role for (target) organization membership with id
|
||||||
|
// [membershipOrgId]
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
membershipOrg: membershipToChangeRole
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization invitation step 1: Send email invitation to user with email [email]
|
||||||
|
* for organization with id [organizationId] containing magic link
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||||
|
let invitee, inviteeMembershipOrg;
|
||||||
|
try {
|
||||||
|
const { organizationId, inviteeEmail } = req.body;
|
||||||
|
|
||||||
|
// validate membership
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg) {
|
||||||
|
throw new Error('Failed to validate organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
invitee = await User.findOne({
|
||||||
|
email: inviteeEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invitee) {
|
||||||
|
// case: invitee is an existing user
|
||||||
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: invitee._id,
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to invite an existing member of the organization'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inviteeMembershipOrg) {
|
||||||
|
await new MembershipOrg({
|
||||||
|
user: invitee,
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
organization: organizationId,
|
||||||
|
role: MEMBER,
|
||||||
|
status: invitee?.publicKey ? ACCEPTED : INVITED
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// check if invitee has been invited before
|
||||||
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inviteeMembershipOrg) {
|
||||||
|
// case: invitee has never been invited before
|
||||||
|
|
||||||
|
await new MembershipOrg({
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
organization: organizationId,
|
||||||
|
role: MEMBER,
|
||||||
|
status: INVITED
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await Organization.findOne({ _id: organizationId });
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
const token = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
|
await Token.findOneAndUpdate(
|
||||||
|
{ email: inviteeEmail },
|
||||||
|
{
|
||||||
|
email: inviteeEmail,
|
||||||
|
token,
|
||||||
|
createdAt: new Date()
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
template: 'organizationInvitation.handlebars',
|
||||||
|
subjectLine: 'Infisical organization invitation',
|
||||||
|
recipients: [inviteeEmail],
|
||||||
|
substitutions: {
|
||||||
|
inviterFirstName: req.user.firstName,
|
||||||
|
inviterEmail: req.user.email,
|
||||||
|
organizationName: organization.name,
|
||||||
|
email: inviteeEmail,
|
||||||
|
token,
|
||||||
|
callback_url: WEBSITE_URL + '/signupinvite'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateSubscriptionOrgQuantity({ organizationId });
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to send organization invite'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: `Sent an invite link to ${req.body.inviteeEmail}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
||||||
|
* magic link and issue a temporary signup token for user to complete setting up their account
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||||
|
let user, token;
|
||||||
|
try {
|
||||||
|
const { email, code } = req.body;
|
||||||
|
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
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,
|
||||||
|
status: INVITED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg)
|
||||||
|
throw new Error('Failed to find any invitations for email');
|
||||||
|
|
||||||
|
await checkEmailVerification({
|
||||||
|
email,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// initialize user account
|
||||||
|
user = await new User({
|
||||||
|
email
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate temporary signup token
|
||||||
|
token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||||
|
secret: JWT_SIGNUP_SECRET
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed email magic link confirmation'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully verified email',
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,399 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PRODUCT_PRO,
|
||||||
|
STRIPE_PRODUCT_CARD_AUTH,
|
||||||
|
WEBSITE_URL
|
||||||
|
} from '../config';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2022-08-01'
|
||||||
|
});
|
||||||
|
import {
|
||||||
|
Membership,
|
||||||
|
MembershipOrg,
|
||||||
|
Organization,
|
||||||
|
Workspace,
|
||||||
|
IncidentContactOrg
|
||||||
|
} 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,
|
||||||
|
pro: STRIPE_PRODUCT_PRO,
|
||||||
|
cardAuth: STRIPE_PRODUCT_CARD_AUTH
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organizations that user is part of
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganizations = async (req: Request, res: Response) => {
|
||||||
|
let organizations;
|
||||||
|
try {
|
||||||
|
organizations = (
|
||||||
|
await MembershipOrg.find({
|
||||||
|
user: req.user._id
|
||||||
|
}).populate('organization')
|
||||||
|
).map((m) => m.organization);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get organizations'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
organizations
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new organization named [organizationName]
|
||||||
|
* and add user as owner
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createOrganization = async (req: Request, res: Response) => {
|
||||||
|
let organization;
|
||||||
|
try {
|
||||||
|
const { organizationName } = req.body;
|
||||||
|
|
||||||
|
if (organizationName.length < 1) {
|
||||||
|
throw new Error('Organization names must be at least 1-character long');
|
||||||
|
}
|
||||||
|
|
||||||
|
// create organization and add user as member
|
||||||
|
organization = await create({
|
||||||
|
email: req.user.email,
|
||||||
|
name: organizationName
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMembershipsOrg({
|
||||||
|
userIds: [req.user._id.toString()],
|
||||||
|
organizationId: organization._id.toString(),
|
||||||
|
roles: [OWNER],
|
||||||
|
statuses: [ACCEPTED]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to create organization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
organization
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganization = async (req: Request, res: Response) => {
|
||||||
|
let organization;
|
||||||
|
try {
|
||||||
|
organization = req.membershipOrg.organization;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to find organization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
organization
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization memberships for organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||||
|
let users;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
|
users = await MembershipOrg.find({
|
||||||
|
organization: organizationId
|
||||||
|
}).populate('user', '+publicKey');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get organization members'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
users
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return workspaces that user is part of in organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganizationWorkspaces = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let workspaces;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
|
const workspacesSet = new Set(
|
||||||
|
(
|
||||||
|
await Workspace.find(
|
||||||
|
{
|
||||||
|
organization: organizationId
|
||||||
|
},
|
||||||
|
'_id'
|
||||||
|
)
|
||||||
|
).map((w) => w._id.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaces = (
|
||||||
|
await Membership.find({
|
||||||
|
user: req.user._id
|
||||||
|
}).populate('workspace')
|
||||||
|
)
|
||||||
|
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||||
|
.map((m) => m.workspace);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get my workspaces'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
workspaces
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change name of organization with id [organizationId] to [name]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||||
|
let organization;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
organization = await Organization.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: organizationId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to change organization name'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully changed organization name',
|
||||||
|
organization
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return incident contacts of organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganizationIncidentContacts = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let incidentContactsOrg;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
|
incidentContactsOrg = await IncidentContactOrg.find({
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get organization incident contacts'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
incidentContactsOrg
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add and return new incident contact with email [email] for organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const addOrganizationIncidentContact = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let incidentContactOrg;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||||
|
{ email, organization: organizationId },
|
||||||
|
{ email, organization: organizationId },
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to add incident contact for organization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
incidentContactOrg
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete incident contact with email [email] for organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteOrganizationIncidentContact = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let incidentContactOrg;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||||
|
email,
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete organization incident contact'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted organization incident contact',
|
||||||
|
incidentContactOrg
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect user to (stripe) billing portal or add card page depending on
|
||||||
|
* if there is a card on file
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createOrganizationPortalSession = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let session;
|
||||||
|
try {
|
||||||
|
// check if there is a payment method on file
|
||||||
|
const paymentMethods = await stripe.paymentMethods.list({
|
||||||
|
customer: req.membershipOrg.organization.customerId,
|
||||||
|
type: 'card'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paymentMethods.data.length < 1) {
|
||||||
|
// case: no payment method on file
|
||||||
|
productToPriceMap['cardAuth'];
|
||||||
|
session = await stripe.checkout.sessions.create({
|
||||||
|
customer: req.membershipOrg.organization.customerId,
|
||||||
|
mode: 'setup',
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
success_url: WEBSITE_URL + '/dashboard',
|
||||||
|
cancel_url: WEBSITE_URL + '/dashboard'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: req.membershipOrg.organization.customerId,
|
||||||
|
return_url: WEBSITE_URL + '/dashboard'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({ url: session.url });
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to redirect to organization billing portal'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization subscriptions
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getOrganizationSubscriptions = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let subscriptions;
|
||||||
|
try {
|
||||||
|
subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: req.membershipOrg.organization.customerId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get organization subscriptions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
subscriptions
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,189 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
const jsrp = require('jsrp');
|
||||||
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
|
import { User, BackupPrivateKey } from '../models';
|
||||||
|
|
||||||
|
const clientPublicKeys: any = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const srp1 = async (req: Request, res: Response) => {
|
||||||
|
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||||
|
try {
|
||||||
|
const { clientPublicKey } = req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email: req.user.email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// generate server-side public key
|
||||||
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
clientPublicKeys[req.user.email] = {
|
||||||
|
clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to start change password process'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change account SRP authentication information for user
|
||||||
|
* Requires verifying [clientProof] as part of step 2 of SRP protocol
|
||||||
|
* as initiated in POST /srp1
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changePassword = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||||
|
req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email: req.user.email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: clientPublicKeys[req.user.email].serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(
|
||||||
|
clientPublicKeys[req.user.email].clientPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// compare server and client shared keys
|
||||||
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
// change password
|
||||||
|
|
||||||
|
await User.findByIdAndUpdate(
|
||||||
|
req.user._id.toString(),
|
||||||
|
{
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully changed password'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to change password. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to change password. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or change backup private key for user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||||
|
// create/change backup private key
|
||||||
|
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||||
|
// as initiated in /srp1
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||||
|
req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email: req.user.email
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: clientPublicKeys[req.user.email].serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(
|
||||||
|
clientPublicKeys[req.user.email].clientPublicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// compare server and client shared keys
|
||||||
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
// create new or replace backup private key
|
||||||
|
|
||||||
|
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||||
|
{ user: req.user._id },
|
||||||
|
{
|
||||||
|
user: req.user._id,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
).select('+user, encryptedPrivateKey');
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully updated backup private key',
|
||||||
|
backupPrivateKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update backup private key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update backup private key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,226 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Key } from '../models';
|
||||||
|
import {
|
||||||
|
pushSecrets as push,
|
||||||
|
pullSecrets as pull,
|
||||||
|
reformatPullSecrets
|
||||||
|
} from '../helpers/secret';
|
||||||
|
import { pushKeys } from '../helpers/key';
|
||||||
|
import { PostHog } from 'posthog-node';
|
||||||
|
import { ENV_SET } from '../variables';
|
||||||
|
import { NODE_ENV, POSTHOG_PROJECT_API_KEY, POSTHOG_HOST } from '../config';
|
||||||
|
|
||||||
|
let client: any;
|
||||||
|
if (NODE_ENV === 'production' && POSTHOG_PROJECT_API_KEY && POSTHOG_HOST) {
|
||||||
|
client = new PostHog(POSTHOG_PROJECT_API_KEY, {
|
||||||
|
host: POSTHOG_HOST
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
* for environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pushSecrets = async (req: Request, res: Response) => {
|
||||||
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
|
||||||
|
try {
|
||||||
|
let { secrets }: { secrets: PushSecret[] } = 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: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
||||||
|
);
|
||||||
|
|
||||||
|
await push({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
|
||||||
|
await pushKeys({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId,
|
||||||
|
keys
|
||||||
|
});
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
// capture secrets pushed event in production
|
||||||
|
client.capture({
|
||||||
|
distinctId: req.user.email,
|
||||||
|
event: 'secrets pushed',
|
||||||
|
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 upload workspace secrets'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully uploaded workspace secrets'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||||
|
* for environment [environment] and (encrypted) workspace key
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
|
let secrets;
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const environment: string = req.query.environment as string;
|
||||||
|
const channel: string = req.query.channel as string;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// validate environment
|
||||||
|
if (!ENV_SET.has(environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = await pull({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
|
key = await Key.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
receiver: req.user._id
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.populate('sender', '+publicKey');
|
||||||
|
|
||||||
|
if (channel !== 'cli') {
|
||||||
|
secrets = reformatPullSecrets({ secrets });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
// capture secrets pushed event in production
|
||||||
|
client.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,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||||
|
* for environment [environment] and (encrypted) workspace key
|
||||||
|
* via service token
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||||
|
// get (encrypted) secrets from workspace with id [workspaceId]
|
||||||
|
// service token route
|
||||||
|
|
||||||
|
let secrets;
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const environment: string = req.query.environment as string;
|
||||||
|
const channel: string = req.query.channel as string;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// validate environment
|
||||||
|
if (!ENV_SET.has(environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = await pull({
|
||||||
|
userId: req.serviceToken.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
|
key = {
|
||||||
|
encryptedKey: req.serviceToken.encryptedKey,
|
||||||
|
nonce: req.serviceToken.nonce,
|
||||||
|
sender: {
|
||||||
|
publicKey: req.serviceToken.publicKey
|
||||||
|
},
|
||||||
|
receiver: req.serviceToken.user,
|
||||||
|
workspace: req.serviceToken.workspace
|
||||||
|
};
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
// capture secrets pushed event in production
|
||||||
|
client.capture({
|
||||||
|
distinctId: req.serviceToken.user.email,
|
||||||
|
event: 'secrets pulled',
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.serviceToken.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to pull workspace secrets'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: reformatPullSecrets({ secrets }),
|
||||||
|
key
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return service token on request
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getServiceToken = async (req: Request, res: Response) => {
|
||||||
|
// get service token
|
||||||
|
return res.status(200).send({
|
||||||
|
serviceToken: req.serviceToken
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a new service token
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createServiceToken = async (req: Request, res: Response) => {
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
expiresIn,
|
||||||
|
publicKey,
|
||||||
|
encryptedKey,
|
||||||
|
nonce
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// validate environment
|
||||||
|
if (!ENV_SET.has(environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute access token expiration date
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
const serviceToken = await new ServiceToken({
|
||||||
|
name,
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
expiresAt,
|
||||||
|
publicKey,
|
||||||
|
encryptedKey,
|
||||||
|
nonce
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
token = createToken({
|
||||||
|
payload: {
|
||||||
|
serviceTokenId: serviceToken._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: expiresIn,
|
||||||
|
secret: JWT_SERVICE_SECRET
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to create service token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,287 @@
|
|||||||
|
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 {
|
||||||
|
sendEmailVerification,
|
||||||
|
checkEmailVerification,
|
||||||
|
initializeDefaultOrg
|
||||||
|
} from '../helpers/signup';
|
||||||
|
import { issueTokens, createToken } from '../helpers/auth';
|
||||||
|
import { INVITED, ACCEPTED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||||
|
* to that email
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||||
|
let email: string;
|
||||||
|
try {
|
||||||
|
email = req.body.email;
|
||||||
|
|
||||||
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
|
if (user && user?.publicKey) {
|
||||||
|
// case: user has already completed account
|
||||||
|
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to send email verification code for complete account'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// send send verification email
|
||||||
|
await sendEmailVerification({ email });
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to send email verification code'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: `Sent an email verification code to ${email}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signup step 2: Verify that code [code] was sent to email [email] and issue
|
||||||
|
* a temporary signup token for user to complete setting up their account
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||||
|
let user, token;
|
||||||
|
try {
|
||||||
|
const { email, code } = req.body;
|
||||||
|
|
||||||
|
// initialize user account
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
if (user && user?.publicKey) {
|
||||||
|
// case: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed email verification for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify email
|
||||||
|
await checkEmailVerification({
|
||||||
|
email,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await new User({
|
||||||
|
email
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate temporary signup token
|
||||||
|
token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: JWT_SIGNUP_LIFETIME,
|
||||||
|
secret: JWT_SIGNUP_SECRET
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed email verification'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfuly verified email',
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
|
* signup flow
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
|
let user, token, refreshToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
organizationName
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// get user
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user || (user && user?.publicKey)) {
|
||||||
|
// case 1: user doesn't exist.
|
||||||
|
// case 2: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to complete account for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete setting up user's account
|
||||||
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||||
|
|
||||||
|
// initialize default organization and workspace
|
||||||
|
await initializeDefaultOrg({
|
||||||
|
organizationName,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userId: user._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
token = tokens.token;
|
||||||
|
refreshToken = tokens.refreshToken;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to complete account setup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully set up account',
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
|
* invite flow
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||||
|
let user, token, refreshToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// get user
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user || (user && user?.publicKey)) {
|
||||||
|
// case 1: user doesn't exist.
|
||||||
|
// case 2: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to complete account for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||||
|
|
||||||
|
// complete setting up user's account
|
||||||
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to complete account for non-existent user');
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueTokens({
|
||||||
|
userId: user._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
token = tokens.token;
|
||||||
|
refreshToken = tokens.refreshToken;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to complete account setup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully set up account',
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2022-08-01'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle service provisioning/un-provisioning via Stripe
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
|
let event;
|
||||||
|
try {
|
||||||
|
// check request for valid stripe signature
|
||||||
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
|
event = stripe.webhooks.constructEvent(
|
||||||
|
req.body,
|
||||||
|
sig,
|
||||||
|
STRIPE_WEBHOOK_SECRET // ?
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to process webhook'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case '':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ received: true });
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { UserAction } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add user action [action]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const addUserAction = async (req: Request, res: Response) => {
|
||||||
|
// add/record new action [action] for user with id [req.user._id]
|
||||||
|
|
||||||
|
let userAction;
|
||||||
|
try {
|
||||||
|
const { action } = req.body;
|
||||||
|
|
||||||
|
userAction = await UserAction.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
user: req.user._id,
|
||||||
|
action
|
||||||
|
},
|
||||||
|
{ user: req.user._id, action },
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to record user action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully recorded user action',
|
||||||
|
userAction
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return user action [action] for user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getUserAction = async (req: Request, res: Response) => {
|
||||||
|
// get user action [action] for user with id [req.user._id]
|
||||||
|
let userAction;
|
||||||
|
try {
|
||||||
|
const action: string = req.query.action as string;
|
||||||
|
|
||||||
|
userAction = await UserAction.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
action
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get user action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
userAction
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return user on request
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getUser = async (req: Request, res: Response) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
user: req.user
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,337 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
Membership,
|
||||||
|
MembershipOrg,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
IUser,
|
||||||
|
ServiceToken
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
createWorkspace as create,
|
||||||
|
deleteWorkspace as deleteWork
|
||||||
|
} from '../helpers/workspace';
|
||||||
|
import { addMemberships } from '../helpers/membership';
|
||||||
|
import { ADMIN, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public keys of members of workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||||
|
let publicKeys;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
publicKeys = (
|
||||||
|
await Membership.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate<{ user: IUser }>('user', 'publicKey')
|
||||||
|
)
|
||||||
|
.filter((m) => m.status === COMPLETED || m.status === GRANTED)
|
||||||
|
.map((member) => {
|
||||||
|
return {
|
||||||
|
publicKey: member.user.publicKey,
|
||||||
|
userId: member.user._id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace member public keys'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
publicKeys
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return memberships for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
|
let users;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
users = await Membership.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate('user', '+publicKey');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace members'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
users
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return workspaces that user is part of
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||||
|
let workspaces;
|
||||||
|
try {
|
||||||
|
workspaces = (
|
||||||
|
await Membership.find({
|
||||||
|
user: req.user._id
|
||||||
|
}).populate('workspace')
|
||||||
|
).map((m) => m.workspace);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspaces'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
workspaces
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspace = async (req: Request, res: Response) => {
|
||||||
|
let workspace;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
workspace = await Workspace.findOne({
|
||||||
|
_id: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new workspace named [workspaceName] under organization with id
|
||||||
|
* [organizationId] and add user as admin
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createWorkspace = async (req: Request, res: Response) => {
|
||||||
|
let workspace;
|
||||||
|
try {
|
||||||
|
const { workspaceName, organizationId } = req.body;
|
||||||
|
|
||||||
|
// validate organization membership
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membershipOrg) {
|
||||||
|
throw new Error('Failed to validate organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceName.length < 1) {
|
||||||
|
throw new Error('Workspace names must be at least 1-character long');
|
||||||
|
}
|
||||||
|
|
||||||
|
// create workspace and add user as member
|
||||||
|
workspace = await create({
|
||||||
|
name: workspaceName,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMemberships({
|
||||||
|
userIds: [req.user._id],
|
||||||
|
workspaceId: workspace._id.toString(),
|
||||||
|
roles: [ADMIN],
|
||||||
|
statuses: [GRANTED]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to create workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// delete workspace
|
||||||
|
await deleteWork({
|
||||||
|
id: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted workspace'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change name of workspace with id [workspaceId] to [name]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||||
|
let workspace;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
workspace = await Workspace.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: workspaceId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to change workspace name'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully changed workspace name',
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return integrations for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
integrations = await Integration.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace integrations'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integrations
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return (integration) authorizations for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceIntegrationAuthorizations = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let authorizations;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
authorizations = await IntegrationAuth.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace integration authorizations'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
authorizations
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceServiceTokens = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let serviceTokens;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
serviceTokens = await ServiceToken.find({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace service tokens'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serviceTokens
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
The Infisical Enterprise license (the “Enterprise License”)
|
||||||
|
Copyright (c) 2022 Infisical Inc
|
||||||
|
|
||||||
|
With regard to the Infisical Software:
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, the Infisical Subscription Terms of Service, available
|
||||||
|
at https://infisical.com/terms (the “Enterprise Terms”), or other
|
||||||
|
agreement governing the use of the Software, as agreed by you and Infisical,
|
||||||
|
and otherwise have a valid Infisical Enterprise License for the
|
||||||
|
correct number of user seats. Subject to the foregoing sentence, you are free to
|
||||||
|
modify this Software and publish patches to the Software. You agree that Infisical
|
||||||
|
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||||
|
to all such modifications and/or patches, and all such modifications and/or
|
||||||
|
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||||
|
exploited with a valid Infiscial Enterprise subscription for the correct
|
||||||
|
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
||||||
|
the Software for development and testing purposes, without requiring a
|
||||||
|
subscription. You agree that Infisical and/or its licensors (as applicable) retain
|
||||||
|
all right, title and interest in and to all such modifications. You are not
|
||||||
|
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||||
|
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Infisical Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
@ -0,0 +1,5 @@
|
|||||||
|
import * as stripeController from './stripeController';
|
||||||
|
|
||||||
|
export {
|
||||||
|
stripeController
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../config';
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2022-08-01'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle service provisioning/un-provisioning via Stripe
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
|
let event;
|
||||||
|
try {
|
||||||
|
// check request for valid stripe signature
|
||||||
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
|
event = stripe.webhooks.constructEvent(
|
||||||
|
req.body,
|
||||||
|
sig,
|
||||||
|
STRIPE_WEBHOOK_SECRET // ?
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
error: 'Failed to process webhook'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case '':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ received: true });
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.licenseKey - Infisical license key
|
||||||
|
*/
|
||||||
|
const checkLicenseKey = ({
|
||||||
|
licenseKey
|
||||||
|
}: {
|
||||||
|
licenseKey: string
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
checkLicenseKey
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if organization hosting meets license requirements to
|
||||||
|
* access a license-specific route.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedTiers
|
||||||
|
*/
|
||||||
|
const requireLicenseAuth = ({
|
||||||
|
acceptedTiers
|
||||||
|
}: {
|
||||||
|
acceptedTiers: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireLicenseAuth;
|
@ -0,0 +1,7 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { stripeController } from '../controllers';
|
||||||
|
|
||||||
|
router.post('/webhook', stripeController.handleWebhook);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,102 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
User
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
JWT_AUTH_LIFETIME,
|
||||||
|
JWT_AUTH_SECRET,
|
||||||
|
JWT_REFRESH_LIFETIME,
|
||||||
|
JWT_REFRESH_SECRET
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user who we are issuing tokens for
|
||||||
|
* @return {Object} obj
|
||||||
|
* @return {String} obj.token - issued JWT token
|
||||||
|
* @return {String} obj.refreshToken - issued refresh token
|
||||||
|
*/
|
||||||
|
const issueTokens = async ({ userId }: { userId: string }) => {
|
||||||
|
let token: string;
|
||||||
|
let refreshToken: string;
|
||||||
|
try {
|
||||||
|
// issue tokens
|
||||||
|
token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
expiresIn: JWT_AUTH_LIFETIME,
|
||||||
|
secret: JWT_AUTH_SECRET
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshToken = createToken({
|
||||||
|
payload: {
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
expiresIn: JWT_REFRESH_LIFETIME,
|
||||||
|
secret: JWT_REFRESH_SECRET
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to issue tokens');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
refreshToken
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove JWT and refresh tokens for user with id [userId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user whose tokens are cleared.
|
||||||
|
*/
|
||||||
|
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// increment refreshVersion on user by 1
|
||||||
|
User.findOneAndUpdate({
|
||||||
|
_id: userId
|
||||||
|
}, {
|
||||||
|
$inc: {
|
||||||
|
refreshVersion: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new (JWT) token for user with id [userId] that expires in [expiresIn]; can be used to, for instance, generate
|
||||||
|
* bearer/auth, refresh, and temporary signup tokens
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.payload - payload of (JWT) token
|
||||||
|
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
|
||||||
|
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
||||||
|
*/
|
||||||
|
const createToken = ({
|
||||||
|
payload,
|
||||||
|
expiresIn,
|
||||||
|
secret
|
||||||
|
}: {
|
||||||
|
payload: any;
|
||||||
|
expiresIn: string | number;
|
||||||
|
secret: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
return jwt.sign(payload, secret, {
|
||||||
|
expiresIn
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create a token');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createToken, issueTokens, clearTokens };
|
@ -0,0 +1,174 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { IntegrationAuth } from '../models';
|
||||||
|
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
||||||
|
import { IIntegrationAuth } from '../models';
|
||||||
|
import {
|
||||||
|
ENCRYPTION_KEY,
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
||||||
|
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
||||||
|
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration (e.g. heroku)
|
||||||
|
* @param {Object} obj.res - response from OAuth2 authorization server
|
||||||
|
*/
|
||||||
|
const processOAuthTokenRes = async ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
res
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
res: any;
|
||||||
|
}): Promise<IIntegrationAuth> => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
// encrypt refresh + access tokens
|
||||||
|
const {
|
||||||
|
ciphertext: refreshCiphertext,
|
||||||
|
iv: refreshIV,
|
||||||
|
tag: refreshTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: res.data.refresh_token,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: accessCiphertext,
|
||||||
|
iv: accessIV,
|
||||||
|
tag: accessTag
|
||||||
|
} = encryptSymmetric({
|
||||||
|
plaintext: res.data.access_token,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
// compute access token expiration date
|
||||||
|
const accessExpiresAt = new Date();
|
||||||
|
accessExpiresAt.setSeconds(
|
||||||
|
accessExpiresAt.getSeconds() + res.data.expires_in
|
||||||
|
);
|
||||||
|
|
||||||
|
// create or replace integration authorization with encrypted tokens
|
||||||
|
// and access token expiration date
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||||
|
{ workspace: workspaceId, integration },
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration,
|
||||||
|
refreshCiphertext,
|
||||||
|
refreshIV,
|
||||||
|
refreshTag,
|
||||||
|
accessCiphertext,
|
||||||
|
accessIV,
|
||||||
|
accessTag,
|
||||||
|
accessExpiresAt
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error(
|
||||||
|
'Failed to process OAuth2 authorization server token response'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
||||||
|
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
||||||
|
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
||||||
|
* @returns {String} access token - new access token
|
||||||
|
*/
|
||||||
|
const getOAuthAccessToken = async ({
|
||||||
|
integrationAuth
|
||||||
|
}: {
|
||||||
|
integrationAuth: IIntegrationAuth;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
refreshCiphertext,
|
||||||
|
refreshIV,
|
||||||
|
refreshTag,
|
||||||
|
accessCiphertext,
|
||||||
|
accessIV,
|
||||||
|
accessTag,
|
||||||
|
accessExpiresAt
|
||||||
|
} = integrationAuth;
|
||||||
|
|
||||||
|
if (
|
||||||
|
refreshCiphertext &&
|
||||||
|
refreshIV &&
|
||||||
|
refreshTag &&
|
||||||
|
accessCiphertext &&
|
||||||
|
accessIV &&
|
||||||
|
accessTag &&
|
||||||
|
accessExpiresAt
|
||||||
|
) {
|
||||||
|
if (accessExpiresAt < new Date()) {
|
||||||
|
// case: access token expired
|
||||||
|
// TODO: fetch another access token
|
||||||
|
|
||||||
|
let clientSecret;
|
||||||
|
switch (integrationAuth.integration) {
|
||||||
|
case 'heroku':
|
||||||
|
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||||
|
}
|
||||||
|
|
||||||
|
// record new access token and refresh token
|
||||||
|
// encrypt refresh + access tokens
|
||||||
|
const refreshToken = decryptSymmetric({
|
||||||
|
ciphertext: refreshCiphertext,
|
||||||
|
iv: refreshIV,
|
||||||
|
tag: refreshTag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: make route compatible with other integration types
|
||||||
|
const res = await axios.post(
|
||||||
|
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_secret: clientSecret
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessToken = res.data.access_token;
|
||||||
|
|
||||||
|
await processOAuthTokenRes({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
integration: integrationAuth.integration,
|
||||||
|
res
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// case: access token still works
|
||||||
|
accessToken = decryptSymmetric({
|
||||||
|
ciphertext: accessCiphertext,
|
||||||
|
iv: accessIV,
|
||||||
|
tag: accessTag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get OAuth2 access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { processOAuthTokenRes, getOAuthAccessToken };
|
@ -0,0 +1,62 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Key, IKey } from '../models';
|
||||||
|
|
||||||
|
interface Key {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push (access) [keys] for workspace with id [workspaceId] with
|
||||||
|
* user with id [userId] as the sender
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of sender user
|
||||||
|
* @param {String} obj.workspaceId - id of workspace that keys belong to
|
||||||
|
* @param {Object[]} obj.keys - (access) keys to push
|
||||||
|
* @param {String} obj.keys.encryptedKey - encrypted key under receiver's public key
|
||||||
|
* @param {String} obj.keys.nonce - nonce for encryption
|
||||||
|
* @param {String} obj.keys.userId - id of receiver user
|
||||||
|
*/
|
||||||
|
const pushKeys = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
keys
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
keys: Key[];
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// filter out already-inserted keys
|
||||||
|
const keysSet = new Set(
|
||||||
|
(
|
||||||
|
await Key.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId
|
||||||
|
},
|
||||||
|
'receiver'
|
||||||
|
)
|
||||||
|
).map((k: IKey) => k.receiver.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
keys = keys.filter((key) => !keysSet.has(key.userId));
|
||||||
|
|
||||||
|
// add new shared keys only
|
||||||
|
await Key.insertMany(
|
||||||
|
keys.map((k) => ({
|
||||||
|
encryptedKey: k.encryptedKey,
|
||||||
|
nonce: k.nonce,
|
||||||
|
sender: userId,
|
||||||
|
receiver: k.userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to push access keys');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { pushKeys };
|
@ -0,0 +1,100 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
|
* @param {Object} queryObj - query object
|
||||||
|
* @return {Object} membership - membership
|
||||||
|
*/
|
||||||
|
const findMembership = async (queryObj: any) => {
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
membership = await Membership.findOne(queryObj);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to find membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add memberships for users with ids [userIds] to workspace with
|
||||||
|
* id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.userIds - id of users.
|
||||||
|
* @param {String} obj.workspaceId - id of workspace.
|
||||||
|
* @param {String[]} obj.roles - roles of users.
|
||||||
|
* @param {String[]} obj.statuses - statuses of users.
|
||||||
|
*/
|
||||||
|
const addMemberships = async ({
|
||||||
|
userIds,
|
||||||
|
workspaceId,
|
||||||
|
roles,
|
||||||
|
statuses
|
||||||
|
}: {
|
||||||
|
userIds: string[];
|
||||||
|
workspaceId: string;
|
||||||
|
roles: string[];
|
||||||
|
statuses: string[];
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const operations = userIds.map((userId, idx) => {
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
role: roles[idx],
|
||||||
|
status: statuses[idx]
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
role: roles[idx],
|
||||||
|
status: statuses[idx]
|
||||||
|
},
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await Membership.bulkWrite(operations as any);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to add users to workspace');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete membership with id [membershipId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.membershipId - id of membership to delete
|
||||||
|
*/
|
||||||
|
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||||
|
let deletedMembership;
|
||||||
|
try {
|
||||||
|
deletedMembership = await Membership.findOneAndDelete({
|
||||||
|
_id: membershipId
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete keys associated with the membership
|
||||||
|
if (deletedMembership?.user) {
|
||||||
|
// case: membership had a registered user
|
||||||
|
await Key.deleteMany({
|
||||||
|
receiver: deletedMembership.user,
|
||||||
|
workspace: deletedMembership.workspace
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to delete membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedMembership;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addMemberships, findMembership, deleteMembership };
|
@ -0,0 +1,120 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organization membership matching criteria specified in
|
||||||
|
* query [queryObj]
|
||||||
|
* @param {Object} queryObj - query object
|
||||||
|
* @return {Object} membershipOrg - membership
|
||||||
|
*/
|
||||||
|
const findMembershipOrg = (queryObj: any) => {
|
||||||
|
let membershipOrg;
|
||||||
|
try {
|
||||||
|
membershipOrg = MembershipOrg.findOne(queryObj);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to find organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add organization memberships for users with ids [userIds] to organization with
|
||||||
|
* id [organizationId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.userIds - id of users.
|
||||||
|
* @param {String} obj.organizationId - id of organization.
|
||||||
|
* @param {String[]} obj.roles - roles of users.
|
||||||
|
*/
|
||||||
|
const addMembershipsOrg = async ({
|
||||||
|
userIds,
|
||||||
|
organizationId,
|
||||||
|
roles,
|
||||||
|
statuses
|
||||||
|
}: {
|
||||||
|
userIds: string[];
|
||||||
|
organizationId: string;
|
||||||
|
roles: string[];
|
||||||
|
statuses: string[];
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const operations = userIds.map((userId, idx) => {
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
user: userId,
|
||||||
|
organization: organizationId,
|
||||||
|
role: roles[idx],
|
||||||
|
status: statuses[idx]
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
user: userId,
|
||||||
|
organization: organizationId,
|
||||||
|
role: roles[idx],
|
||||||
|
status: statuses[idx]
|
||||||
|
},
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await MembershipOrg.bulkWrite(operations as any);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to add users to organization');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete organization membership with id [membershipOrgId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
||||||
|
*/
|
||||||
|
const deleteMembershipOrg = async ({
|
||||||
|
membershipOrgId
|
||||||
|
}: {
|
||||||
|
membershipOrgId: string;
|
||||||
|
}) => {
|
||||||
|
let deletedMembershipOrg;
|
||||||
|
try {
|
||||||
|
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||||
|
_id: membershipOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete keys associated with organization membership
|
||||||
|
if (deletedMembershipOrg?.user) {
|
||||||
|
// case: organization membership had a registered user
|
||||||
|
|
||||||
|
const workspaces = (
|
||||||
|
await Workspace.find({
|
||||||
|
organization: deletedMembershipOrg.organization
|
||||||
|
})
|
||||||
|
).map((w) => w._id.toString());
|
||||||
|
|
||||||
|
await Membership.deleteMany({
|
||||||
|
user: deletedMembershipOrg.user,
|
||||||
|
workspace: {
|
||||||
|
$in: workspaces
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Key.deleteMany({
|
||||||
|
receiver: deletedMembershipOrg.user,
|
||||||
|
workspace: {
|
||||||
|
$in: workspaces
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to delete organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedMembershipOrg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { findMembershipOrg, addMembershipsOrg, deleteMembershipOrg };
|
@ -0,0 +1,58 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { SMTP_HOST, SMTP_NAME, SMTP_USERNAME, SMTP_PASSWORD } from '../config';
|
||||||
|
|
||||||
|
// create nodemailer transporter
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: SMTP_HOST,
|
||||||
|
port: 587,
|
||||||
|
auth: {
|
||||||
|
user: SMTP_USERNAME,
|
||||||
|
pass: SMTP_PASSWORD
|
||||||
|
}
|
||||||
|
});
|
||||||
|
transporter
|
||||||
|
.verify()
|
||||||
|
.then(() => console.log('SMTP - Successfully connected'))
|
||||||
|
.catch((err) => console.log('SMTP - Failed to connect'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.template - email template to use from /templates folder (e.g. testEmail.handlebars)
|
||||||
|
* @param {String[]} obj.subjectLine - email subject line
|
||||||
|
* @param {String[]} obj.recipients - email addresses of people to send email to
|
||||||
|
* @param {Object} obj.substitutions - object containing template substitutions
|
||||||
|
*/
|
||||||
|
const sendMail = async ({
|
||||||
|
template,
|
||||||
|
subjectLine,
|
||||||
|
recipients,
|
||||||
|
substitutions
|
||||||
|
}: {
|
||||||
|
template: string;
|
||||||
|
subjectLine: string;
|
||||||
|
recipients: string[];
|
||||||
|
substitutions: any;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const html = fs.readFileSync(
|
||||||
|
path.resolve(__dirname, '../templates/' + template),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const temp = handlebars.compile(html);
|
||||||
|
const htmlToSend = temp(substitutions);
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
|
||||||
|
to: recipients.join(', '),
|
||||||
|
subject: subjectLine,
|
||||||
|
html: htmlToSend
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sendMail };
|
@ -0,0 +1,168 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import {
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PRODUCT_PRO
|
||||||
|
} from '../config';
|
||||||
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2022-08-01'
|
||||||
|
});
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { ACCEPTED } from '../variables';
|
||||||
|
import { Organization, MembershipOrg } from '../models';
|
||||||
|
|
||||||
|
const productToPriceMap = {
|
||||||
|
starter: STRIPE_PRODUCT_STARTER,
|
||||||
|
pro: STRIPE_PRODUCT_PRO
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an organization with name [name]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of organization to create.
|
||||||
|
* @param {String} obj.email - POC email that will receive invoice info
|
||||||
|
* @param {Object} organization - new organization
|
||||||
|
*/
|
||||||
|
const createOrganization = async ({
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}) => {
|
||||||
|
let organization;
|
||||||
|
try {
|
||||||
|
// register stripe account
|
||||||
|
|
||||||
|
if (STRIPE_SECRET_KEY) {
|
||||||
|
const customer = await stripe.customers.create({
|
||||||
|
email,
|
||||||
|
description: name
|
||||||
|
});
|
||||||
|
|
||||||
|
organization = await new Organization({
|
||||||
|
name,
|
||||||
|
customerId: customer.id
|
||||||
|
}).save();
|
||||||
|
} else {
|
||||||
|
organization = await new Organization({
|
||||||
|
name
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
await initSubscriptionOrg({ organizationId: organization._id });
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create organization');
|
||||||
|
}
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize free-tier subscription for new organization
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.organizationId - id of associated organization for subscription
|
||||||
|
* @return {Object} obj
|
||||||
|
* @return {Object} obj.stripeSubscription - new stripe subscription
|
||||||
|
* @return {Subscription} obj.subscription - new subscription
|
||||||
|
*/
|
||||||
|
const initSubscriptionOrg = async ({
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
organizationId: Types.ObjectId;
|
||||||
|
}) => {
|
||||||
|
let stripeSubscription;
|
||||||
|
let subscription;
|
||||||
|
try {
|
||||||
|
// find organization
|
||||||
|
const organization = await Organization.findOne({
|
||||||
|
_id: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
if (organization.customerId) {
|
||||||
|
// initialize starter subscription with quantity of 0
|
||||||
|
stripeSubscription = await stripe.subscriptions.create({
|
||||||
|
customer: organization.customerId,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
price: productToPriceMap['starter'],
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
payment_behavior: 'default_incomplete',
|
||||||
|
proration_behavior: 'none',
|
||||||
|
expand: ['latest_invoice.payment_intent']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to initialize free organization subscription');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to initialize free organization subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stripeSubscription,
|
||||||
|
subscription
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update organization subscription quantity to reflect number of members in
|
||||||
|
* the organization.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Number} obj.organizationId - id of subscription's organization
|
||||||
|
*/
|
||||||
|
const updateSubscriptionOrgQuantity = async ({
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
}) => {
|
||||||
|
let stripeSubscription;
|
||||||
|
try {
|
||||||
|
// find organization
|
||||||
|
const organization = await Organization.findOne({
|
||||||
|
_id: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organization && organization.customerId) {
|
||||||
|
const quantity = await MembershipOrg.countDocuments({
|
||||||
|
organization: organizationId,
|
||||||
|
status: ACCEPTED
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscription = (
|
||||||
|
await stripe.subscriptions.list({
|
||||||
|
customer: organization.customerId
|
||||||
|
})
|
||||||
|
).data[0];
|
||||||
|
|
||||||
|
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: subscription.items.data[0].id,
|
||||||
|
price: subscription.items.data[0].price.id,
|
||||||
|
quantity
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripeSubscription;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
createOrganization,
|
||||||
|
initSubscriptionOrg,
|
||||||
|
updateSubscriptionOrgQuantity
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
|
|
||||||
|
// 300 requests per 15 minutes
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 300,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5 requests per hour
|
||||||
|
const signupLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10 requests per hour
|
||||||
|
const loginLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
max: 10,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5 requests per hour
|
||||||
|
const passwordLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
|
@ -0,0 +1,303 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Secret,
|
||||||
|
ISecret
|
||||||
|
} from '../models';
|
||||||
|
import { decryptSymmetric } from '../utils/crypto';
|
||||||
|
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Update {
|
||||||
|
[index: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 pushSecrets = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: PushSecret[];
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// construct useful data structures
|
||||||
|
const oldSecrets = await pullSecrets({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
|
||||||
|
return { ...accumulator, [s.secretKeyHash]: s };
|
||||||
|
}, {});
|
||||||
|
const newSecretsObj = secrets.reduce((accumulator, s) => {
|
||||||
|
return { ...accumulator, [s.hashKey]: s };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// handle deleting secrets
|
||||||
|
const toDelete = oldSecrets.filter(
|
||||||
|
(s: ISecret) => !(s.secretKeyHash in newSecretsObj)
|
||||||
|
);
|
||||||
|
if (toDelete.length > 0) {
|
||||||
|
await Secret.deleteMany({
|
||||||
|
_id: { $in: toDelete.map((s) => s._id) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle modifying secrets where type or value changed
|
||||||
|
const operations = secrets
|
||||||
|
.filter((s) => {
|
||||||
|
if (s.hashKey in oldSecretsObj) {
|
||||||
|
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) {
|
||||||
|
// case: filter secrets where value changed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.type !== oldSecretsObj[s.hashKey].type) {
|
||||||
|
// case: filter secrets where type changed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((s) => {
|
||||||
|
const update: Update = {
|
||||||
|
type: s.type,
|
||||||
|
secretValueCiphertext: s.ciphertextValue,
|
||||||
|
secretValueIV: s.ivValue,
|
||||||
|
secretValueTag: s.tagValue,
|
||||||
|
secretValueHash: s.hashValue
|
||||||
|
};
|
||||||
|
|
||||||
|
if (s.type === SECRET_PERSONAL) {
|
||||||
|
// attach user assocaited with the personal secret
|
||||||
|
update['user'] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
workspace: workspaceId,
|
||||||
|
_id: oldSecretsObj[s.hashKey]._id
|
||||||
|
},
|
||||||
|
update
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const a = await Secret.bulkWrite(operations as any);
|
||||||
|
|
||||||
|
// handle adding new secrets
|
||||||
|
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj));
|
||||||
|
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
// add secrets
|
||||||
|
await Secret.insertMany(
|
||||||
|
toAdd.map((s, idx) => {
|
||||||
|
let obj: any = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
type: toAdd[idx].type,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext: s.ciphertextKey,
|
||||||
|
secretKeyIV: s.ivKey,
|
||||||
|
secretKeyTag: s.tagKey,
|
||||||
|
secretKeyHash: s.hashKey,
|
||||||
|
secretValueCiphertext: s.ciphertextValue,
|
||||||
|
secretValueIV: s.ivValue,
|
||||||
|
secretValueTag: s.tagValue,
|
||||||
|
secretValueHash: s.hashValue
|
||||||
|
};
|
||||||
|
|
||||||
|
if (toAdd[idx].type === 'personal') {
|
||||||
|
obj['user' as keyof typeof obj] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to push shared and personal secrets');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull secrets for user with id [userId] for workspace
|
||||||
|
* with id [workspaceId] with environment [environment]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId -id of user to pull secrets for
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const pullSecrets = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}): Promise<ISecret[]> => {
|
||||||
|
let secrets: any; // TODO: FIX any
|
||||||
|
try {
|
||||||
|
// get shared workspace secrets
|
||||||
|
const sharedSecrets = await Secret.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
// get personal workspace secrets
|
||||||
|
const personalSecrets = await Secret.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
type: SECRET_PERSONAL,
|
||||||
|
user: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
// concat shared and personal workspace secrets
|
||||||
|
secrets = personalSecrets.concat(sharedSecrets);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to pull shared and personal secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reformat output of pullSecrets() to be compatible with how existing
|
||||||
|
* clients handle secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.secrets
|
||||||
|
*/
|
||||||
|
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||||
|
let reformatedSecrets;
|
||||||
|
try {
|
||||||
|
reformatedSecrets = secrets.map((s) => ({
|
||||||
|
_id: s._id,
|
||||||
|
workspace: s.workspace,
|
||||||
|
type: s.type,
|
||||||
|
environment: s.environment,
|
||||||
|
secretKey: {
|
||||||
|
workspace: s.workspace,
|
||||||
|
ciphertext: s.secretKeyCiphertext,
|
||||||
|
iv: s.secretKeyIV,
|
||||||
|
tag: s.secretKeyTag,
|
||||||
|
hash: s.secretKeyHash
|
||||||
|
},
|
||||||
|
secretValue: {
|
||||||
|
workspace: s.workspace,
|
||||||
|
ciphertext: s.secretValueCiphertext,
|
||||||
|
iv: s.secretValueIV,
|
||||||
|
tag: s.secretValueTag,
|
||||||
|
hash: s.secretValueHash
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to reformat pulled secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
pullSecrets,
|
||||||
|
reformatPullSecrets,
|
||||||
|
decryptSecrets
|
||||||
|
};
|
@ -0,0 +1,124 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { Token, IToken, IUser } from '../models';
|
||||||
|
import { createOrganization } from './organization';
|
||||||
|
import { addMembershipsOrg } from './membershipOrg';
|
||||||
|
import { createWorkspace } from './workspace';
|
||||||
|
import { addMemberships } from './membership';
|
||||||
|
import { OWNER, ADMIN, ACCEPTED, GRANTED } from '../variables';
|
||||||
|
import { sendMail } from '../helpers/nodemailer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send magic link to verify email to [email]
|
||||||
|
* for user and workspace.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - email
|
||||||
|
* @returns {Boolean} success - whether or not operation was successful
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const sendEmailVerification = async ({ email }: { email: string }) => {
|
||||||
|
try {
|
||||||
|
const token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
|
||||||
|
|
||||||
|
await Token.findOneAndUpdate(
|
||||||
|
{ email },
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
createdAt: new Date()
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// send mail
|
||||||
|
await sendMail({
|
||||||
|
template: 'emailVerification.handlebars',
|
||||||
|
subjectLine: 'Infisical workspace invitation',
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
code: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error(
|
||||||
|
"Ouch. We weren't able to send your email verification code"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate [code] sent to [email]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - emai
|
||||||
|
* @param {String} obj.code - code that was sent to [email]
|
||||||
|
*/
|
||||||
|
const checkEmailVerification = async ({
|
||||||
|
email,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const token = await Token.findOneAndDelete({
|
||||||
|
email,
|
||||||
|
token: code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) throw new Error('Failed to find email verification token');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error("Oops. We weren't able to verify");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize default organization named [organizationName] with workspace
|
||||||
|
* for user [user]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.organizationName - name of organization to initialize
|
||||||
|
* @param {IUser} obj.user - user who we are initializing for
|
||||||
|
*/
|
||||||
|
const initializeDefaultOrg = async ({
|
||||||
|
organizationName,
|
||||||
|
user
|
||||||
|
}: {
|
||||||
|
organizationName: string;
|
||||||
|
user: IUser;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// create organization with user as owner and initialize a free
|
||||||
|
// subscription
|
||||||
|
const organization = await createOrganization({
|
||||||
|
email: user.email,
|
||||||
|
name: organizationName
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMembershipsOrg({
|
||||||
|
userIds: [user._id.toString()],
|
||||||
|
organizationId: organization._id.toString(),
|
||||||
|
roles: [OWNER],
|
||||||
|
statuses: [ACCEPTED]
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize a default workspace inside the new organization
|
||||||
|
const workspace = await createWorkspace({
|
||||||
|
name: `${user.firstName}'s Project`,
|
||||||
|
organizationId: organization._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMemberships({
|
||||||
|
userIds: [user._id.toString()],
|
||||||
|
workspaceId: workspace._id.toString(),
|
||||||
|
roles: [ADMIN],
|
||||||
|
statuses: [GRANTED]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Failed to initialize default organization and workspace');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
|
@ -0,0 +1,88 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { User, IUser } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a user under email [email]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.email - email of user to initialize
|
||||||
|
* @returns {Object} user - the initialized user
|
||||||
|
*/
|
||||||
|
const setupAccount = async ({ email }: { email: string }) => {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await new User({
|
||||||
|
email
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to set up account');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish setting up user
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to finish setting up
|
||||||
|
* @param {String} obj.firstName - first name of user
|
||||||
|
* @param {String} obj.lastName - last name of user
|
||||||
|
* @param {String} obj.publicKey - publickey of user
|
||||||
|
* @param {String} obj.encryptedPrivateKey - (encrypted) private key of user
|
||||||
|
* @param {String} obj.iv - iv for (encrypted) private key of user
|
||||||
|
* @param {String} obj.tag - tag for (encrypted) private key of user
|
||||||
|
* @param {String} obj.salt - salt for auth SRP
|
||||||
|
* @param {String} obj.verifier - verifier for auth SRP
|
||||||
|
* @returns {Object} user - the completed user
|
||||||
|
*/
|
||||||
|
const completeAccount = async ({
|
||||||
|
userId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
}) => {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
new: true
|
||||||
|
};
|
||||||
|
user = await User.findByIdAndUpdate(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to complete account set up');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { setupAccount, completeAccount };
|
@ -0,0 +1,62 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
Membership,
|
||||||
|
Key,
|
||||||
|
Secret
|
||||||
|
} from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
|
* @param {String} name - name of workspace to create.
|
||||||
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
|
* @param {Object} workspace - new workspace
|
||||||
|
*/
|
||||||
|
const createWorkspace = async ({
|
||||||
|
name,
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
}) => {
|
||||||
|
let workspace;
|
||||||
|
try {
|
||||||
|
workspace = await new Workspace({
|
||||||
|
name,
|
||||||
|
organization: organizationId
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace and all associated materials including memberships,
|
||||||
|
* secrets, keys, etc.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.id - id of workspace to delete
|
||||||
|
*/
|
||||||
|
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
|
try {
|
||||||
|
await Workspace.deleteOne({ _id: id });
|
||||||
|
await Membership.deleteMany({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
|
await Secret.deleteMany({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
|
await Key.deleteMany({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to delete workspace');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createWorkspace, deleteWorkspace };
|
@ -0,0 +1,90 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import cors from 'cors';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, WEBSITE_URL } from './config';
|
||||||
|
import { apiLimiter } from './helpers/rateLimiter';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: SENTRY_DSN,
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
debug: NODE_ENV === 'production' ? false : true,
|
||||||
|
environment: NODE_ENV
|
||||||
|
});
|
||||||
|
|
||||||
|
import {
|
||||||
|
signup as signupRouter,
|
||||||
|
auth as authRouter,
|
||||||
|
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
|
||||||
|
} from './routes';
|
||||||
|
|
||||||
|
const connectWithRetry = () => {
|
||||||
|
mongoose.connect(MONGO_URL)
|
||||||
|
.then(() => console.log('Successfully connected to DB'))
|
||||||
|
.catch((e) => {
|
||||||
|
console.log('Failed to connect to DB ', e);
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(e);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWithRetry();
|
||||||
|
|
||||||
|
app.enable('trust proxy');
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(cors({
|
||||||
|
credentials: true,
|
||||||
|
origin: WEBSITE_URL
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (NODE_ENV === 'production') {
|
||||||
|
// enable app-wide rate-limiting + helmet security
|
||||||
|
// in production
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
app.use(apiLimiter);
|
||||||
|
app.use(helmet());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// routers
|
||||||
|
app.use('/api/v1/signup', signupRouter);
|
||||||
|
app.use('/api/v1/auth', authRouter);
|
||||||
|
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.listen(PORT, () => {
|
||||||
|
console.log('Listening on PORT ' + PORT);
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"heroku": {
|
||||||
|
"name": "Heroku",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "bc132901-935a-4590-b010-f1857efc380d",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"netlify": {
|
||||||
|
"name": "Netlify",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"digitalocean": {
|
||||||
|
"name": "Digital Ocean",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"gcp": {
|
||||||
|
"name": "Google Cloud Platform",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"aws": {
|
||||||
|
"name": "Amazon Web Services",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"azure": {
|
||||||
|
"name": "Microsoft Azure",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"travisci": {
|
||||||
|
"name": "Travis CI",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
},
|
||||||
|
"circleci": {
|
||||||
|
"name": "Circle CI",
|
||||||
|
"type": "oauth2",
|
||||||
|
"clientId": "",
|
||||||
|
"docsLink": ""
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import requireAuth from './requireAuth';
|
||||||
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
|
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||||
|
import requireIntegrationAuth from './requireIntegrationAuth';
|
||||||
|
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
|
||||||
|
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||||
|
import validateRequest from './validateRequest';
|
||||||
|
|
||||||
|
export {
|
||||||
|
requireAuth,
|
||||||
|
requireSignupAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireOrganizationAuth,
|
||||||
|
requireIntegrationAuth,
|
||||||
|
requireIntegrationAuthorizationAuth,
|
||||||
|
requireServiceTokenAuth,
|
||||||
|
validateRequest
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { User } from '../models';
|
||||||
|
import { JWT_AUTH_SECRET } from '../config';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// JWT authentication middleware
|
||||||
|
try {
|
||||||
|
if (!req.headers?.authorization)
|
||||||
|
throw new Error('Failed to locate authorization header');
|
||||||
|
|
||||||
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(token, JWT_AUTH_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||||
|
if (!user?.publicKey)
|
||||||
|
throw new Error('Failed to authenticate not fully set up account');
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed to authenticate user. Try logging in'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireAuth;
|
@ -0,0 +1,76 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Integration, IntegrationAuth, Membership } from '../models';
|
||||||
|
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
|
* with the integration on request params.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||||
|
*/
|
||||||
|
const requireIntegrationAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// integration authorization middleware
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { integrationId } = req.params;
|
||||||
|
|
||||||
|
// validate integration accessibility
|
||||||
|
const integration = await Integration.findOne({
|
||||||
|
_id: integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!integration) {
|
||||||
|
throw new Error('Failed to find integration');
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await Membership.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: integration.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error('Failed to find integration workspace membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate workspace membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate workspace membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
|
_id: integration.integrationAuth
|
||||||
|
}).select(
|
||||||
|
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integrationAuth) {
|
||||||
|
throw new Error('Failed to find integration authorization');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.integration = integration;
|
||||||
|
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed integration authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireIntegrationAuth;
|
@ -0,0 +1,72 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { IntegrationAuth, Membership } from '../models';
|
||||||
|
import { decryptSymmetric } from '../utils/crypto';
|
||||||
|
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
|
* with the integration authorization on request params.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||||
|
* @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request
|
||||||
|
*/
|
||||||
|
const requireIntegrationAuthorizationAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// (authorization) integration authorization middleware
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}).select(
|
||||||
|
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!integrationAuth) {
|
||||||
|
throw new Error('Failed to find integration authorization');
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await Membership.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: integrationAuth.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to find integration authorization workspace membership'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate workspace membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate workspace membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.integrationAuth = integrationAuth;
|
||||||
|
|
||||||
|
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
||||||
|
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed (authorization) integration authorizationt'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireIntegrationAuthorizationAuth;
|
@ -0,0 +1,54 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { IOrganization, MembershipOrg } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if user on request is a member with proper roles for organization
|
||||||
|
* on request params.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedRoles - accepted organization roles
|
||||||
|
* @param {String[]} obj.acceptedStatuses - accepted organization statuses
|
||||||
|
*/
|
||||||
|
const requireOrganizationAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// organization authorization middleware
|
||||||
|
|
||||||
|
try {
|
||||||
|
// validate organization membership
|
||||||
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
organization: req.params.organizationId
|
||||||
|
}).populate<{ organization: IOrganization }>('organization');
|
||||||
|
|
||||||
|
if (!membershipOrg) {
|
||||||
|
throw new Error('Failed to find organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||||
|
throw new Error('Failed to validate organization membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||||
|
throw new Error('Failed to validate organization membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.membershipOrg = membershipOrg;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed organization authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireOrganizationAuth;
|
@ -0,0 +1,56 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { ServiceToken } from '../models';
|
||||||
|
import { JWT_SERVICE_SECRET } from '../config';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if JWT (service) token on request is valid (e.g. not expired),
|
||||||
|
* and if there is an associated service token
|
||||||
|
* @param req - express request object
|
||||||
|
* @param res - express response object
|
||||||
|
* @param next - express next function
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const requireServiceTokenAuth = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
// JWT service token middleware
|
||||||
|
try {
|
||||||
|
if (!req.headers?.authorization)
|
||||||
|
throw new Error('Failed to locate authorization header');
|
||||||
|
|
||||||
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(token, JWT_SERVICE_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
const serviceToken = await ServiceToken.findOne({
|
||||||
|
_id: decodedToken.serviceTokenId
|
||||||
|
})
|
||||||
|
.populate('user', '+publicKey')
|
||||||
|
.select('+encryptedKey +publicKey +nonce');
|
||||||
|
|
||||||
|
if (!serviceToken) throw new Error('Failed to find service token');
|
||||||
|
|
||||||
|
req.serviceToken = serviceToken;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed to authenticate service token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireServiceTokenAuth;
|
@ -0,0 +1,52 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { User } from '../models';
|
||||||
|
import { JWT_SIGNUP_SECRET } from '../config';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if JWT temporary token on request is valid (e.g. not expired)
|
||||||
|
* and if there is an associated user.
|
||||||
|
*/
|
||||||
|
const requireSignupAuth = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
// JWT (temporary) authentication middleware for complete signup
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!req.headers?.authorization)
|
||||||
|
throw new Error('Failed to locate authorization header');
|
||||||
|
|
||||||
|
const token = req.headers.authorization.split(' ')[1];
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(token, JWT_SIGNUP_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to temporarily authenticate unfound user');
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error:
|
||||||
|
'Failed to temporarily authenticate user for complete account. Try logging in'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireSignupAuth;
|
@ -0,0 +1,60 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Membership, IWorkspace } from '../models';
|
||||||
|
|
||||||
|
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.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||||
|
*/
|
||||||
|
const requireWorkspaceAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
location = 'params'
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
location?: req;
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// workspace authorization middleware
|
||||||
|
|
||||||
|
try {
|
||||||
|
// validate workspace membership
|
||||||
|
|
||||||
|
const membership = await Membership.findOne({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: req[location].workspaceId
|
||||||
|
}).populate<{ workspace: IWorkspace }>('workspace');
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new Error('Failed to find workspace membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate workspace membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate workspace membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.membership = membership;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed workspace authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireWorkspaceAuth;
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate intended inputs on [req] via express-validator
|
||||||
|
* @param req - express request object
|
||||||
|
* @param res - express response object
|
||||||
|
* @param next - express next function
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const validate = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// express validator middleware
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: "Looks like you're unauthenticated . Try logging in"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default validate;
|
@ -0,0 +1,56 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBackupPrivateKey {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupPrivateKeySchema = new Schema<IBackupPrivateKey>(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
iv: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
salt: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
verifier: {
|
||||||
|
type: String,
|
||||||
|
select: false,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BackupPrivateKey = model<IBackupPrivateKey>(
|
||||||
|
'BackupPrivateKey',
|
||||||
|
backupPrivateKeySchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BackupPrivateKey;
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IIncidentContactOrg {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
email: string;
|
||||||
|
organization: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incidentContactOrgSchema = new Schema<IIncidentContactOrg>(
|
||||||
|
{
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Organization',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IncidentContactOrg = model<IIncidentContactOrg>(
|
||||||
|
'IncidentContactOrg',
|
||||||
|
incidentContactOrgSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default IncidentContactOrg;
|
@ -0,0 +1,45 @@
|
|||||||
|
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||||
|
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||||
|
import Integration, { IIntegration } from './integration';
|
||||||
|
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||||
|
import Key, { IKey } from './key';
|
||||||
|
import Membership, { IMembership } from './membership';
|
||||||
|
import MembershipOrg, { IMembershipOrg } from './membershipOrg';
|
||||||
|
import Organization, { IOrganization } from './organization';
|
||||||
|
import Secret, { ISecret } from './secret';
|
||||||
|
import ServiceToken, { IServiceToken } from './serviceToken';
|
||||||
|
import Token, { IToken } from './token';
|
||||||
|
import User, { IUser } from './user';
|
||||||
|
import UserAction, { IUserAction } from './userAction';
|
||||||
|
import Workspace, { IWorkspace } from './workspace';
|
||||||
|
|
||||||
|
export {
|
||||||
|
BackupPrivateKey,
|
||||||
|
IBackupPrivateKey,
|
||||||
|
IncidentContactOrg,
|
||||||
|
IIncidentContactOrg,
|
||||||
|
Integration,
|
||||||
|
IIntegration,
|
||||||
|
IntegrationAuth,
|
||||||
|
IIntegrationAuth,
|
||||||
|
Key,
|
||||||
|
IKey,
|
||||||
|
Membership,
|
||||||
|
IMembership,
|
||||||
|
MembershipOrg,
|
||||||
|
IMembershipOrg,
|
||||||
|
Organization,
|
||||||
|
IOrganization,
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
ServiceToken,
|
||||||
|
IServiceToken,
|
||||||
|
Token,
|
||||||
|
IToken,
|
||||||
|
User,
|
||||||
|
IUser,
|
||||||
|
UserAction,
|
||||||
|
IUserAction,
|
||||||
|
Workspace,
|
||||||
|
IWorkspace
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
export interface IIntegration {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||||
|
isActive: boolean;
|
||||||
|
app: string;
|
||||||
|
integration: 'heroku' | 'netlify';
|
||||||
|
integrationAuth: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationSchema = new Schema<IIntegration>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
// name of app in provider
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
integration: {
|
||||||
|
type: String,
|
||||||
|
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
integrationAuth: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'IntegrationAuth',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Integration = model<IIntegration>('Integration', integrationSchema);
|
||||||
|
|
||||||
|
export default Integration;
|
@ -0,0 +1,67 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
|
||||||
|
|
||||||
|
export interface IIntegrationAuth {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
integration: 'heroku' | 'netlify';
|
||||||
|
refreshCiphertext?: string;
|
||||||
|
refreshIV?: string;
|
||||||
|
refreshTag?: string;
|
||||||
|
accessCiphertext?: string;
|
||||||
|
accessIV?: string;
|
||||||
|
accessTag?: string;
|
||||||
|
accessExpiresAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
integration: {
|
||||||
|
type: String,
|
||||||
|
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
refreshCiphertext: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
refreshIV: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
refreshTag: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
accessCiphertext: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
accessIV: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
accessTag: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
accessExpiresAt: {
|
||||||
|
type: Date,
|
||||||
|
select: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IntegrationAuth = model<IIntegrationAuth>(
|
||||||
|
'IntegrationAuth',
|
||||||
|
integrationAuthSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default IntegrationAuth;
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IKey {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
sender: Types.ObjectId;
|
||||||
|
receiver: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keySchema = new Schema<IKey>(
|
||||||
|
{
|
||||||
|
encryptedKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
receiver: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Key = model<IKey>('Key', keySchema);
|
||||||
|
|
||||||
|
export default Key;
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import { ADMIN, MEMBER, INVITED, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
|
export interface IMembership {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
inviteEmail?: string;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
role: 'admin' | 'member';
|
||||||
|
status: 'invited' | 'completed' | 'granted';
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipSchema = new Schema(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
inviteEmail: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: [ADMIN, MEMBER],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
// INVITED, COMPLETED, GRANTED
|
||||||
|
type: String,
|
||||||
|
enum: [INVITED, COMPLETED, GRANTED],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Membership = model<IMembership>('Membership', membershipSchema);
|
||||||
|
|
||||||
|
export default Membership;
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables';
|
||||||
|
|
||||||
|
export interface IMembershipOrg {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
inviteEmail: string;
|
||||||
|
organization: Types.ObjectId;
|
||||||
|
role: 'owner' | 'admin' | 'member';
|
||||||
|
status: 'invited' | 'accepted';
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipOrgSchema = new Schema(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
inviteEmail: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Organization'
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: [OWNER, ADMIN, MEMBER],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: [INVITED, ACCEPTED],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const MembershipOrg = model<IMembershipOrg>(
|
||||||
|
'MembershipOrg',
|
||||||
|
membershipOrgSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MembershipOrg;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IOrganization {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
customerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizationSchema = new Schema<IOrganization>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
customerId: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Organization = model<IOrganization>('Organization', organizationSchema);
|
||||||
|
|
||||||
|
export default Organization;
|
@ -0,0 +1,89 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
export interface ISecret {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
type: string;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretKeyHash: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretValueHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretSchema = new Schema<ISecret>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// user associated with the personal secret
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyCiphertext: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyIV: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyTag: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyHash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueCiphertext: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueIV: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueTag: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueHash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Secret = model<ISecret>('Secret', secretSchema);
|
||||||
|
|
||||||
|
export default Secret;
|
@ -0,0 +1,64 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
|
||||||
|
|
||||||
|
export interface IServiceToken {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceTokenSchema = new Schema<IServiceToken>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// token issuer
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: Date
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: true
|
||||||
|
},
|
||||||
|
encryptedKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: true
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ServiceToken = model<IServiceToken>('ServiceToken', serviceTokenSchema);
|
||||||
|
|
||||||
|
export default ServiceToken;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Schema, model } from 'mongoose';
|
||||||
|
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||||
|
|
||||||
|
export interface IToken {
|
||||||
|
email: String;
|
||||||
|
token: String;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenSchema = new Schema<IToken>({
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
expires: EMAIL_TOKEN_LIFETIME,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Token = model<IToken>('Token', tokenSchema);
|
||||||
|
|
||||||
|
export default Token;
|
@ -0,0 +1,65 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
email: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
salt?: string;
|
||||||
|
verifier?: string;
|
||||||
|
refreshVersion?: Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSchema = new Schema<IUser>(
|
||||||
|
{
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
firstName: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
iv: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
salt: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
verifier: {
|
||||||
|
type: String,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
refreshVersion: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const User = model<IUser>('User', userSchema);
|
||||||
|
|
||||||
|
export default User;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IUserAction {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userActionSchema = new Schema<IUserAction>(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserAction = model<IUserAction>('UserAction', userActionSchema);
|
||||||
|
|
||||||
|
export default UserAction;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IWorkspace {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
organization: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceSchema = new Schema<IWorkspace>({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Organization',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
||||||
|
|
||||||
|
export default Workspace;
|
@ -0,0 +1,35 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/token',
|
||||||
|
validateRequest,
|
||||||
|
authController.getNewToken
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/login1',
|
||||||
|
// loginLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('clientPublicKey').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.login1
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/login2',
|
||||||
|
// loginLimiter,
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('clientProof').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
authController.login2
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/logout', requireAuth, authController.logout);
|
||||||
|
router.post('/checkAuth', requireAuth, authController.checkAuth);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,35 @@
|
|||||||
|
import signup from './signup';
|
||||||
|
import auth from './auth';
|
||||||
|
import user from './user';
|
||||||
|
import userAction from './userAction';
|
||||||
|
import organization from './organization';
|
||||||
|
import workspace from './workspace';
|
||||||
|
import membershipOrg from './membershipOrg';
|
||||||
|
import membership from './membership';
|
||||||
|
import key from './key';
|
||||||
|
import inviteOrg from './inviteOrg';
|
||||||
|
import secret from './secret';
|
||||||
|
import serviceToken from './serviceToken';
|
||||||
|
import password from './password';
|
||||||
|
import stripe from './stripe';
|
||||||
|
import integration from './integration';
|
||||||
|
import integrationAuth from './integrationAuth';
|
||||||
|
|
||||||
|
export {
|
||||||
|
signup,
|
||||||
|
auth,
|
||||||
|
user,
|
||||||
|
userAction,
|
||||||
|
organization,
|
||||||
|
workspace,
|
||||||
|
membershipOrg,
|
||||||
|
membership,
|
||||||
|
key,
|
||||||
|
inviteOrg,
|
||||||
|
secret,
|
||||||
|
serviceToken,
|
||||||
|
password,
|
||||||
|
stripe,
|
||||||
|
integration,
|
||||||
|
integrationAuth
|
||||||
|
};
|
@ -0,0 +1,53 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import { integrationController } from '../controllers';
|
||||||
|
|
||||||
|
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:integrationId/sync',
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('integrationId').exists().trim(),
|
||||||
|
body('key').exists(),
|
||||||
|
body('secrets').exists(),
|
||||||
|
validateRequest,
|
||||||
|
integrationController.syncIntegration
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:integrationId',
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('integrationId'),
|
||||||
|
body('update'),
|
||||||
|
validateRequest,
|
||||||
|
integrationController.modifyIntegration
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:integrationId',
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('integrationId'),
|
||||||
|
validateRequest,
|
||||||
|
integrationController.deleteIntegration
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,52 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireIntegrationAuthorizationAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
|
import { integrationAuthController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/oauth-token',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
body('workspaceId').exists().trim().notEmpty(),
|
||||||
|
body('code').exists().trim().notEmpty(),
|
||||||
|
body('integration').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
integrationAuthController.integrationAuthOauthExchange
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:integrationAuthId/apps',
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuthorizationAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('integrationAuthId'),
|
||||||
|
validateRequest,
|
||||||
|
integrationAuthController.getIntegrationAuthApps
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:integrationAuthId',
|
||||||
|
requireAuth,
|
||||||
|
requireIntegrationAuthorizationAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('integrationAuthId'),
|
||||||
|
validateRequest,
|
||||||
|
integrationAuthController.deleteIntegrationAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,24 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import { requireAuth, validateRequest } from '../middleware';
|
||||||
|
import { membershipOrgController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/signup',
|
||||||
|
requireAuth,
|
||||||
|
body('inviteeEmail').exists().trim().notEmpty().isEmail(),
|
||||||
|
body('organizationId').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
membershipOrgController.inviteUserToOrganization
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/verify',
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
body('code').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
membershipOrgController.verifyUserToOrganization
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,39 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
import { keyController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('key').exists(),
|
||||||
|
validateRequest,
|
||||||
|
keyController.uploadKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/latest',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId'),
|
||||||
|
validateRequest,
|
||||||
|
keyController.getLatestKey
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/publicKey/infisical', keyController.getPublicKeyInfisical);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,31 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import { requireAuth, validateRequest } from '../middleware';
|
||||||
|
import { membershipController } from '../controllers';
|
||||||
|
|
||||||
|
router.get( // used for CLI (deprecate)
|
||||||
|
'/:workspaceId/connect',
|
||||||
|
requireAuth,
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
membershipController.validateMembership
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:membershipId',
|
||||||
|
requireAuth,
|
||||||
|
param('membershipId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
membershipController.deleteMembership
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:membershipId/change-role',
|
||||||
|
requireAuth,
|
||||||
|
body('role').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
membershipController.changeMembershipRole
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,24 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { param } from 'express-validator';
|
||||||
|
import { requireAuth, validateRequest } from '../middleware';
|
||||||
|
import { membershipOrgController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
// TODO
|
||||||
|
'/membershipOrg/:membershipOrgId/change-role',
|
||||||
|
requireAuth,
|
||||||
|
param('membershipOrgId'),
|
||||||
|
validateRequest,
|
||||||
|
membershipOrgController.changeMembershipOrgRole
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:membershipOrgId',
|
||||||
|
requireAuth,
|
||||||
|
param('membershipOrgId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
membershipOrgController.deleteMembershipOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,137 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables';
|
||||||
|
import { organizationController } from '../controllers';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
organizationController.getOrganizations
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post( // not used on frontend
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
body('organizationName').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.createOrganization
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.getOrganization
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/users',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.getOrganizationMembers
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/my-workspaces',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.getOrganizationWorkspaces
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:organizationId/name',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
body('name').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.changeOrganizationName
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/incidentContactOrg',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.getOrganizationIncidentContacts
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:organizationId/incidentContactOrg',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.addOrganizationIncidentContact
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:organizationId/incidentContactOrg',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.deleteOrganizationIncidentContact
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:organizationId/customer-portal-session',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.createOrganizationPortalSession
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/subscriptions',
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationController.getOrganizationSubscriptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,44 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import { requireAuth, validateRequest } from '../middleware';
|
||||||
|
import { passwordController } from '../controllers';
|
||||||
|
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/srp1',
|
||||||
|
requireAuth,
|
||||||
|
body('clientPublicKey').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
passwordController.srp1
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/change-password',
|
||||||
|
passwordLimiter,
|
||||||
|
requireAuth,
|
||||||
|
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
|
||||||
|
body('tag').exists().trim().notEmpty(), // new tag for private key
|
||||||
|
body('salt').exists().trim().notEmpty(), // part of new pwd
|
||||||
|
body('verifier').exists().trim().notEmpty(), // part of new pwd
|
||||||
|
validateRequest,
|
||||||
|
passwordController.changePassword
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/backup-private-key',
|
||||||
|
passwordLimiter,
|
||||||
|
requireAuth,
|
||||||
|
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
|
||||||
|
body('tag').exists().trim().notEmpty(), // new tag for (backup) private key
|
||||||
|
body('salt').exists().trim().notEmpty(), // salt generated from strong key
|
||||||
|
body('verifier').exists().trim().notEmpty(), // salt generated from strong key
|
||||||
|
validateRequest,
|
||||||
|
passwordController.createBackupPrivateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,53 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireServiceTokenAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { body, query, param } from 'express-validator';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
import { secretController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
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,
|
||||||
|
secretController.pushSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
query('environment').exists().trim(),
|
||||||
|
query('channel'),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
secretController.pullSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/service-token',
|
||||||
|
requireServiceTokenAuth,
|
||||||
|
query('environment').exists().trim(),
|
||||||
|
query('channel'),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
secretController.pullSecretsServiceToken
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,40 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireServiceTokenAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
|
import { serviceTokenController } from '../controllers';
|
||||||
|
|
||||||
|
// TODO: revoke service token
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireServiceTokenAuth,
|
||||||
|
serviceTokenController.getServiceToken
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
body('name').exists().trim().notEmpty(),
|
||||||
|
body('workspaceId').exists().trim().notEmpty(),
|
||||||
|
body('environment').exists().trim().notEmpty(),
|
||||||
|
body('expiresIn'), // measured in ms
|
||||||
|
body('publicKey').exists().trim().notEmpty(),
|
||||||
|
body('encryptedKey').exists().trim().notEmpty(),
|
||||||
|
body('nonce').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
serviceTokenController.createServiceToken
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,60 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/email/signup',
|
||||||
|
// signupLimiter,
|
||||||
|
body('email').exists().trim().notEmpty().isEmail(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.beginEmailSignup
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/email/verify',
|
||||||
|
// signupLimiter,
|
||||||
|
body('email').exists().trim().notEmpty().isEmail(),
|
||||||
|
body('code').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.verifyEmailSignup
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/complete-account/signup',
|
||||||
|
// signupLimiter,
|
||||||
|
requireSignupAuth,
|
||||||
|
body('email').exists().trim().notEmpty().isEmail(),
|
||||||
|
body('firstName').exists().trim().notEmpty(),
|
||||||
|
body('lastName').exists().trim().notEmpty(),
|
||||||
|
body('publicKey').exists().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
||||||
|
body('iv').exists().trim().notEmpty(),
|
||||||
|
body('tag').exists().trim().notEmpty(),
|
||||||
|
body('salt').exists().trim().notEmpty(),
|
||||||
|
body('verifier').exists().trim().notEmpty(),
|
||||||
|
body('organizationName').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.completeAccountSignup
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/complete-account/invite',
|
||||||
|
// signupLimiter,
|
||||||
|
requireSignupAuth,
|
||||||
|
body('email').exists().trim().notEmpty().isEmail(),
|
||||||
|
body('firstName').exists().trim().notEmpty(),
|
||||||
|
body('lastName').exists().trim().notEmpty(),
|
||||||
|
body('publicKey').exists().trim().notEmpty(),
|
||||||
|
body('encryptedPrivateKey').exists().trim().notEmpty(),
|
||||||
|
body('iv').exists().trim().notEmpty(),
|
||||||
|
body('tag').exists().trim().notEmpty(),
|
||||||
|
body('salt').exists().trim().notEmpty(),
|
||||||
|
body('verifier').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
signupController.completeAccountInvite
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,7 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { stripeController } from '../controllers';
|
||||||
|
|
||||||
|
router.post('/webhook', stripeController.handleWebhook);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,8 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { requireAuth } from '../middleware';
|
||||||
|
import { userController } from '../controllers';
|
||||||
|
|
||||||
|
router.get('/', requireAuth, userController.getUser);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,23 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { requireAuth, validateRequest } from '../middleware';
|
||||||
|
import { body, query } from 'express-validator';
|
||||||
|
import { userActionController } from '../controllers';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
body('action'),
|
||||||
|
validateRequest,
|
||||||
|
userActionController.addUserAction
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
query('action'),
|
||||||
|
validateRequest,
|
||||||
|
userActionController.getUserAction
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,133 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
import { workspaceController, membershipController } from '../controllers';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/keys',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspacePublicKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/users',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceMemberships
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/', requireAuth, workspaceController.getWorkspaces);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
body('workspaceName').exists().trim().notEmpty(),
|
||||||
|
body('organizationId').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.createWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:workspaceId',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.deleteWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/name',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('name').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.changeWorkspaceName
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/invite-signup',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('email').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
membershipController.inviteUserToWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/integrations',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceIntegrations
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/authorizations',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceIntegrationAuthorizations
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/service-tokens',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [GRANTED]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceServiceTokens
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Email Verification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>Confirm your email address</h2>
|
||||||
|
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
|
||||||
|
<h2>{{code}}</h2>
|
||||||
|
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue