mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 16:19:49 +00:00
Compare commits
53 Commits
clean-up-t
...
big-163-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd02ae471e | ||
|
|
a5bfb0b02b | ||
|
|
f4440c9a03 | ||
|
|
cb88c234d1 | ||
|
|
b9fc0cdd9e | ||
|
|
4368c18479 | ||
|
|
b7214044bb | ||
|
|
93cb3615c3 | ||
|
|
7abfa6a162 | ||
|
|
1372a1f0a8 | ||
|
|
484024ec28 | ||
|
|
46639c7b86 | ||
|
|
d10d1654c1 | ||
|
|
2f06070ecb | ||
|
|
deefdb9bfd | ||
|
|
3cc62d80de | ||
|
|
4962c5d4d3 | ||
|
|
571a332658 | ||
|
|
b44c318a5d | ||
|
|
bd9717f4dc | ||
|
|
f48aea8e5a | ||
|
|
0ac3a5dea9 | ||
|
|
56b40ad4cb | ||
|
|
9b6f934990 | ||
|
|
80e3522f8a | ||
|
|
7975643765 | ||
|
|
2ac7f86bdb | ||
|
|
956b9b6812 | ||
|
|
60248ec3f6 | ||
|
|
9d3f1541eb | ||
|
|
9b5f1a36ab | ||
|
|
8ee691e1ed | ||
|
|
f9cb14da9e | ||
|
|
5e87581f4e | ||
|
|
8ca9cf39da | ||
|
|
9001fea524 | ||
|
|
dea0d71732 | ||
|
|
c191c4bd26 | ||
|
|
47d82ce591 | ||
|
|
9321db2a3a | ||
|
|
e486333c96 | ||
|
|
a9748b23c0 | ||
|
|
693ae61141 | ||
|
|
9807ac04b0 | ||
|
|
bddfde4138 | ||
|
|
a39dcd00d5 | ||
|
|
4d616e9287 | ||
|
|
dc52fb1de5 | ||
|
|
21a1777424 | ||
|
|
16b721db91 | ||
|
|
079491823d | ||
|
|
f7a87a6e9c | ||
|
|
2baa667c5d |
@@ -105,6 +105,24 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"bug"
|
"bug"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "benpsnyder",
|
||||||
|
"name": "Ben Snyder",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/707567?v=4",
|
||||||
|
"profile": "https://snyder.tech",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "cloudsbird",
|
||||||
|
"name": "Vederis Leunardus",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13505006?v=4",
|
||||||
|
"profile": "http://vederis.id",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ SIGNUP_DISABLED=false
|
|||||||
SIGNUP_ALLOWED_DOMAINS=
|
SIGNUP_ALLOWED_DOMAINS=
|
||||||
SIGNUP_ALLOWED_EMAILS=
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
|
# Sign-up Email Confirmation
|
||||||
|
SIGNUP_EMAIL_CONFIRMATION=false
|
||||||
|
|
||||||
# API rate limit (points,duration,block duration).
|
# API rate limit (points,duration,block duration).
|
||||||
API_RATE_LIMIT=120,60,600
|
API_RATE_LIMIT=120,60,600
|
||||||
|
|
||||||
@@ -95,3 +98,8 @@ PLAID_LINK_WEBHOOK=
|
|||||||
|
|
||||||
PLAID_SANDBOX_REDIRECT_URI=
|
PLAID_SANDBOX_REDIRECT_URI=
|
||||||
PLAID_DEVELOPMENT_REDIRECT_URI=
|
PLAID_DEVELOPMENT_REDIRECT_URI=
|
||||||
|
|
||||||
|
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
|
||||||
|
LEMONSQUEEZY_API_KEY=
|
||||||
|
LEMONSQUEEZY_STORE_ID=
|
||||||
|
LEMONSQUEEZY_WEBHOOK_SECRET=
|
||||||
|
|||||||
73
.github/workflows/build-deploy-container.yml
vendored
73
.github/workflows/build-deploy-container.yml
vendored
@@ -12,16 +12,33 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-publish-webapp:
|
build-publish-webapp:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
name: Build and deploy webapp container
|
name: Build and deploy webapp container
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: production
|
environment: production
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Login to Container registry.
|
# Login to Container registry.
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -35,14 +52,29 @@ jobs:
|
|||||||
|
|
||||||
# Builds and push the Docker image.
|
# Builds and push the Docker image.
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
|
id: build
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./packages/webapp/Dockerfile
|
file: ./packages/webapp/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
tags: ghcr.io/bigcapitalhq/webapp:latest
|
tags: ghcr.io/bigcapitalhq/webapp:latest
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
# Send notification to Slack channel.
|
# Send notification to Slack channel.
|
||||||
- name: Slack Notification built and published webapp container successfully.
|
- name: Slack Notification built and published webapp container successfully.
|
||||||
uses: rtCamp/action-slack-notify@v2
|
uses: rtCamp/action-slack-notify@v2
|
||||||
@@ -53,12 +85,23 @@ jobs:
|
|||||||
name: Build and deploy server container
|
name: Build and deploy server container
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Prepare
|
||||||
uses: actions/checkout@v2
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Login to Container registry.
|
# Login to Container registry.
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -66,14 +109,30 @@ jobs:
|
|||||||
|
|
||||||
# Builds and push the Docker image.
|
# Builds and push the Docker image.
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v5
|
||||||
|
id: build
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./packages/server/Dockerfile
|
file: ./packages/server/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
tags: ghcr.io/bigcapitalhq/server:latest
|
tags: ghcr.io/bigcapitalhq/server:latest
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
# Send notification to Slack channel.
|
# Send notification to Slack channel.
|
||||||
- name: Slack Notification built and published server container successfully.
|
- name: Slack Notification built and published server container successfully.
|
||||||
uses: rtCamp/action-slack-notify@v2
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
|||||||
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
@@ -8,14 +8,14 @@ on:
|
|||||||
- '**.ts'
|
- '**.ts'
|
||||||
- '**.tsx'
|
- '**.tsx'
|
||||||
- '**/tsconfig.json'
|
- '**/tsconfig.json'
|
||||||
- 'yarn.lock'
|
- 'pnpm-lock.yaml'
|
||||||
- '.github/workflows/e2e.yml'
|
- '.github/workflows/e2e.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '**.ts'
|
- '**.ts'
|
||||||
- '**.tsx'
|
- '**.tsx'
|
||||||
- '**/tsconfig.json'
|
- '**/tsconfig.json'
|
||||||
- 'yarn.lock'
|
- 'pnpm-lock.yaml'
|
||||||
- '.github/workflows/e2e.yml'
|
- '.github/workflows/e2e.yml'
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn commitlint --edit
|
pnpx commitlint --edit
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
|
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://app.bigcapital.ly">Bigcapital Cloud</a>
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# What's Bigcapital?
|
# What's Bigcapital?
|
||||||
@@ -118,6 +122,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://snyder.tech"><img src="https://avatars.githubusercontent.com/u/707567?v=4?s=100" width="100px;" alt="Ben Snyder"/><br /><sub><b>Ben Snyder</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=benpsnyder" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -21,16 +21,12 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- server
|
- server
|
||||||
- webapp
|
- webapp
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
|
|
||||||
webapp:
|
webapp:
|
||||||
container_name: bigcapital-webapp
|
container_name: bigcapital-webapp
|
||||||
image: ghcr.io/bigcapitalhq/webapp:latest
|
image: ghcr.io/bigcapitalhq/webapp:latest
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
container_name: bigcapital-server
|
container_name: bigcapital-server
|
||||||
@@ -45,9 +41,7 @@ services:
|
|||||||
- mysql
|
- mysql
|
||||||
- mongo
|
- mongo
|
||||||
- redis
|
- redis
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
# Mail
|
# Mail
|
||||||
- MAIL_HOST=${MAIL_HOST}
|
- MAIL_HOST=${MAIL_HOST}
|
||||||
@@ -92,6 +86,22 @@ services:
|
|||||||
- GOTENBERG_URL=${GOTENBERG_URL}
|
- GOTENBERG_URL=${GOTENBERG_URL}
|
||||||
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
|
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
|
||||||
|
|
||||||
|
# Bank Sync
|
||||||
|
- BANKING_CONNECT=${BANKING_CONNECT}
|
||||||
|
|
||||||
|
# Plaid
|
||||||
|
- PLAID_ENV=${PLAID_ENV}
|
||||||
|
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
|
||||||
|
- PLAID_SECRET_DEVELOPMENT=${PLAID_SECRET_DEVELOPMENT}
|
||||||
|
- PLAID_SECRET_SANDBOX=${b8cf42b441e110451e2f69ad7e1e9f}
|
||||||
|
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
|
||||||
|
|
||||||
|
# Lemon Squeez
|
||||||
|
- LEMONSQUEEZY_API_KEY=${LEMONSQUEEZY_API_KEY}
|
||||||
|
- LEMONSQUEEZY_STORE_ID=${LEMONSQUEEZY_STORE_ID}
|
||||||
|
- LEMONSQUEEZY_WEBHOOK_SECRET=${LEMONSQUEEZY_WEBHOOK_SECRET}
|
||||||
|
- HOSTED_ON_BIGCAPITAL_CLOUD=${HOSTED_ON_BIGCAPITAL_CLOUD}
|
||||||
|
|
||||||
database_migration:
|
database_migration:
|
||||||
container_name: bigcapital-database-migration
|
container_name: bigcapital-database-migration
|
||||||
build:
|
build:
|
||||||
@@ -111,9 +121,7 @@ services:
|
|||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
container_name: bigcapital-mysql
|
container_name: bigcapital-mysql
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
build:
|
build:
|
||||||
context: ./docker/mariadb
|
context: ./docker/mariadb
|
||||||
environment:
|
environment:
|
||||||
@@ -128,9 +136,7 @@ services:
|
|||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
container_name: bigcapital-mongo
|
container_name: bigcapital-mongo
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
build: ./docker/mongo
|
build: ./docker/mongo
|
||||||
expose:
|
expose:
|
||||||
- '27017'
|
- '27017'
|
||||||
@@ -139,9 +145,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: bigcapital-redis
|
container_name: bigcapital-redis
|
||||||
deploy:
|
restart: on-failure
|
||||||
restart_policy:
|
|
||||||
condition: unless-stopped
|
|
||||||
build:
|
build:
|
||||||
context: ./docker/redis
|
context: ./docker/redis
|
||||||
expose:
|
expose:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "independent",
|
"version": "independent",
|
||||||
"npmClient": "pnpm",
|
"npmClient": "pnpm",
|
||||||
"useWorkspaces": true,
|
"packages": [
|
||||||
"packages": ["packages/*"]
|
"packages/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"@faker-js/faker": "^8.0.2",
|
"@faker-js/faker": "^8.0.2",
|
||||||
"@playwright/test": "^1.32.3",
|
"@playwright/test": "^1.32.3",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lerna": "^6.4.1"
|
"lerna": "^8.1.2",
|
||||||
|
"pnpm": "^9.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.x || 17.x || 18.x"
|
"node": "16.x || 17.x || 18.x"
|
||||||
|
|||||||
2
packages/server/.gitignore
vendored
2
packages/server/.gitignore
vendored
@@ -4,3 +4,5 @@ stdout.log
|
|||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
/public/imports
|
/public/imports
|
||||||
|
|
||||||
|
dist
|
||||||
17747
packages/server/package-lock.json
generated
17747
packages/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^5.4.3",
|
"@casl/ability": "^5.4.3",
|
||||||
"@hapi/boom": "^7.4.3",
|
"@hapi/boom": "^7.4.3",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
"@types/i18n": "^0.8.7",
|
"@types/i18n": "^0.8.7",
|
||||||
"@types/knex": "^0.16.1",
|
"@types/knex": "^0.16.1",
|
||||||
"@types/mathjs": "^6.0.12",
|
"@types/mathjs": "^6.0.12",
|
||||||
@@ -89,17 +90,17 @@
|
|||||||
"objection-filter": "^4.0.1",
|
"objection-filter": "^4.0.1",
|
||||||
"objection-soft-delete": "^1.0.7",
|
"objection-soft-delete": "^1.0.7",
|
||||||
"objection-unique": "^1.2.2",
|
"objection-unique": "^1.2.2",
|
||||||
|
"plaid": "^10.3.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"puppeteer": "^10.2.0",
|
"puppeteer": "^10.2.0",
|
||||||
"plaid": "^10.3.0",
|
|
||||||
"qim": "0.0.52",
|
"qim": "0.0.52",
|
||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"rate-limiter-flexible": "^2.1.14",
|
"rate-limiter-flexible": "^2.1.14",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rtl-detect": "^1.0.4",
|
"rtl-detect": "^1.0.4",
|
||||||
"source-map-loader": "^4.0.1",
|
|
||||||
"socket.io": "^4.7.4",
|
"socket.io": "^4.7.4",
|
||||||
|
"source-map-loader": "^4.0.1",
|
||||||
"tmp-promise": "^3.0.3",
|
"tmp-promise": "^3.0.3",
|
||||||
"ts-transformer-keys": "^0.4.2",
|
"ts-transformer-keys": "^0.4.2",
|
||||||
"tsyringe": "^4.3.0",
|
"tsyringe": "^4.3.0",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
|||||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||||
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationController extends BaseController {
|
export default class AuthenticationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -28,6 +30,20 @@ export default class AuthenticationController extends BaseController {
|
|||||||
asyncMiddleware(this.login.bind(this)),
|
asyncMiddleware(this.login.bind(this)),
|
||||||
this.handlerErrors
|
this.handlerErrors
|
||||||
);
|
);
|
||||||
|
router.use('/register/verify/resend', JWTAuth);
|
||||||
|
router.use('/register/verify/resend', AttachCurrentTenantUser);
|
||||||
|
router.post(
|
||||||
|
'/register/verify/resend',
|
||||||
|
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
|
||||||
|
this.handlerErrors
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/register/verify',
|
||||||
|
this.signupVerifySchema,
|
||||||
|
this.validationResult,
|
||||||
|
asyncMiddleware(this.registerVerify.bind(this)),
|
||||||
|
this.handlerErrors
|
||||||
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/register',
|
'/register',
|
||||||
this.registerSchema,
|
this.registerSchema,
|
||||||
@@ -99,6 +115,17 @@ export default class AuthenticationController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get signupVerifySchema(): ValidationChain[] {
|
||||||
|
return [
|
||||||
|
check('email')
|
||||||
|
.exists()
|
||||||
|
.isString()
|
||||||
|
.isEmail()
|
||||||
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
check('token').exists().isString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset password schema.
|
* Reset password schema.
|
||||||
* @returns {ValidationChain[]}
|
* @returns {ValidationChain[]}
|
||||||
@@ -166,6 +193,58 @@ export default class AuthenticationController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provider user's email after signin-up.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response}| res
|
||||||
|
* @param {Function} next
|
||||||
|
* @returns {Response|void}
|
||||||
|
*/
|
||||||
|
private async registerVerify(req: Request, res: Response, next: Function) {
|
||||||
|
const signUpVerifyDTO: { email: string; token: string } =
|
||||||
|
this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.authApplication.signUpConfirm(
|
||||||
|
signUpVerifyDTO.email,
|
||||||
|
signUpVerifyDTO.token
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
type: 'success',
|
||||||
|
message: 'The given user has verified successfully',
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends the confirmation email to the user.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response}| res
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
private async registerVerifyResendMail(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: Function
|
||||||
|
) {
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.authApplication.signUpConfirmResend(user.id);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
type: 'success',
|
||||||
|
message: 'The given user has verified successfully',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send reset password handler
|
* Send reset password handler
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -144,10 +144,8 @@ export default class VendorsController extends ContactsController {
|
|||||||
try {
|
try {
|
||||||
const vendor = await this.vendorsApplication.createVendor(
|
const vendor = await this.vendorsApplication.createVendor(
|
||||||
tenantId,
|
tenantId,
|
||||||
contactDTO,
|
contactDTO
|
||||||
user
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: vendor.id,
|
id: vendor.id,
|
||||||
message: 'The vendor has been created successfully.',
|
message: 'The vendor has been created successfully.',
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export default class DashboardMetaController {
|
|||||||
dashboardService: DashboardService;
|
dashboardService: DashboardService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Constructor router.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/boot', this.getDashboardBoot);
|
router.get('/boot', this.getDashboardBoot);
|
||||||
@@ -25,7 +25,7 @@ export default class DashboardMetaController {
|
|||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
* @param {NextFunction} next -
|
* @param {NextFunction} next -
|
||||||
*/
|
*/
|
||||||
getDashboardBoot = async (
|
private getDashboardBoot = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Multer from 'multer';
|
import Multer from 'multer';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { getImportsStoragePath } from '@/services/Import/_utils';
|
||||||
|
|
||||||
export function allowSheetExtensions(req, file, cb) {
|
export function allowSheetExtensions(req, file, cb) {
|
||||||
if (
|
if (
|
||||||
@@ -16,7 +17,8 @@ export function allowSheetExtensions(req, file, cb) {
|
|||||||
|
|
||||||
const storage = Multer.diskStorage({
|
const storage = Multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
cb(null, './public/imports');
|
const path = getImportsStoragePath();
|
||||||
|
cb(null, path);
|
||||||
},
|
},
|
||||||
filename: function (req, file, cb) {
|
filename: function (req, file, cb) {
|
||||||
// Add the creation timestamp to clean up temp files later.
|
// Add the creation timestamp to clean up temp files later.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { check, ValidationChain } from 'express-validator';
|
|||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
|
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||||
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
|
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
|
||||||
@@ -17,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
|
|||||||
@Service()
|
@Service()
|
||||||
export default class OrganizationController extends BaseController {
|
export default class OrganizationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private organizationService: OrganizationService;
|
organizationService: OrganizationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
@@ -25,10 +26,13 @@ export default class OrganizationController extends BaseController {
|
|||||||
router() {
|
router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Should before build tenant database the user be authorized and
|
||||||
|
// most important than that, should be subscribed to any plan.
|
||||||
router.use(JWTAuth);
|
router.use(JWTAuth);
|
||||||
router.use(AttachCurrentTenantUser);
|
router.use(AttachCurrentTenantUser);
|
||||||
router.use(TenancyMiddleware);
|
router.use(TenancyMiddleware);
|
||||||
|
|
||||||
|
router.use('/build', SubscriptionMiddleware('main'));
|
||||||
router.post(
|
router.post(
|
||||||
'/build',
|
'/build',
|
||||||
this.buildOrganizationValidationSchema,
|
this.buildOrganizationValidationSchema,
|
||||||
|
|||||||
@@ -297,8 +297,7 @@ export default class VendorCreditController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
const vendorCredit = await this.createVendorCreditService.newVendorCredit(
|
const vendorCredit = await this.createVendorCreditService.newVendorCredit(
|
||||||
tenantId,
|
tenantId,
|
||||||
vendorCreditCreateDTO,
|
vendorCreditCreateDTO
|
||||||
user
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
|||||||
@@ -338,8 +338,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
const creditNote = await this.createCreditNoteService.newCreditNote(
|
const creditNote = await this.createCreditNoteService.newCreditNote(
|
||||||
tenantId,
|
tenantId,
|
||||||
creditNoteDTO,
|
creditNoteDTO
|
||||||
user
|
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: creditNote.id,
|
id: creditNote.id,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
|
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||||
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
|
import BaseController from '../BaseController';
|
||||||
|
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SubscriptionController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
private subscriptionService: SubscriptionService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private lemonSqueezyService: LemonSqueezyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(JWTAuth);
|
||||||
|
router.use(AttachCurrentTenantUser);
|
||||||
|
router.use(TenancyMiddleware);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/lemon/checkout_url',
|
||||||
|
[body('variantId').exists().trim()],
|
||||||
|
this.validationResult,
|
||||||
|
this.getCheckoutUrl.bind(this)
|
||||||
|
);
|
||||||
|
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all subscriptions of the authenticated user's tenant.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async getSubscriptions(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscriptions = await this.subscriptionService.getSubscriptions(
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ subscriptions });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the LemonSqueezy checkout url.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async getCheckoutUrl(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { variantId } = this.matchedBodyData(req);
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkout = await this.lemonSqueezyService.getCheckout(
|
||||||
|
variantId,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
return res.status(200).send(checkout);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './SubscriptionController';
|
||||||
@@ -3,6 +3,7 @@ import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
|
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
|
||||||
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -10,18 +11,39 @@ export class Webhooks extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private plaidApp: PlaidApplication;
|
private plaidApp: PlaidApplication;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private lemonWebhooksService: LemonSqueezyWebhooks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(PlaidWebhookTenantBootMiddleware);
|
router.use('/plaid', PlaidWebhookTenantBootMiddleware);
|
||||||
router.post('/plaid', this.plaidWebhooks.bind(this));
|
router.post('/plaid', this.plaidWebhooks.bind(this));
|
||||||
|
|
||||||
|
router.post('/lemon', this.lemonWebhooks.bind(this));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to Lemon Squeezy webhooks events.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @returns {Response}
|
||||||
|
*/
|
||||||
|
public async lemonWebhooks(req: Request, res: Response) {
|
||||||
|
const data = req.body;
|
||||||
|
const signature = req.headers['x-signature'] ?? '';
|
||||||
|
const rawBody = req.rawBody;
|
||||||
|
|
||||||
|
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature);
|
||||||
|
|
||||||
|
return res.status(200).send();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens to Plaid webhooks.
|
* Listens to Plaid webhooks.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Container } from 'typedi';
|
|||||||
// Middlewares
|
// Middlewares
|
||||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
|
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||||
@@ -36,6 +37,7 @@ import Resources from './controllers/Resources';
|
|||||||
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
||||||
import Media from '@/api/controllers/Media';
|
import Media from '@/api/controllers/Media';
|
||||||
import Ping from '@/api/controllers/Ping';
|
import Ping from '@/api/controllers/Ping';
|
||||||
|
import { SubscriptionController } from '@/api/controllers/Subscription';
|
||||||
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
|
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
|
||||||
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
||||||
import Jobs from './controllers/Jobs';
|
import Jobs from './controllers/Jobs';
|
||||||
@@ -70,6 +72,7 @@ export default () => {
|
|||||||
|
|
||||||
app.use('/auth', Container.get(Authentication).router());
|
app.use('/auth', Container.get(Authentication).router());
|
||||||
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
|
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
|
||||||
|
app.use('/subscription', Container.get(SubscriptionController).router());
|
||||||
app.use('/organization', Container.get(Organization).router());
|
app.use('/organization', Container.get(Organization).router());
|
||||||
app.use('/ping', Container.get(Ping).router());
|
app.use('/ping', Container.get(Ping).router());
|
||||||
app.use('/jobs', Container.get(Jobs).router());
|
app.use('/jobs', Container.get(Jobs).router());
|
||||||
@@ -83,6 +86,7 @@ export default () => {
|
|||||||
dashboard.use(JWTAuth);
|
dashboard.use(JWTAuth);
|
||||||
dashboard.use(AttachCurrentTenantUser);
|
dashboard.use(AttachCurrentTenantUser);
|
||||||
dashboard.use(TenancyMiddleware);
|
dashboard.use(TenancyMiddleware);
|
||||||
|
dashboard.use(SubscriptionMiddleware('main'));
|
||||||
dashboard.use(EnsureTenantIsInitialized);
|
dashboard.use(EnsureTenantIsInitialized);
|
||||||
dashboard.use(SettingsMiddleware);
|
dashboard.use(SettingsMiddleware);
|
||||||
dashboard.use(I18nAuthenticatedMiddlware);
|
dashboard.use(I18nAuthenticatedMiddlware);
|
||||||
@@ -136,12 +140,10 @@ export default () => {
|
|||||||
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
||||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||||
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
||||||
|
|
||||||
dashboard.use('/import', Container.get(ImportController).router());
|
dashboard.use('/import', Container.get(ImportController).router());
|
||||||
|
|
||||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||||
|
|
||||||
dashboard.use('/', Container.get(WarehousesItemController).router());
|
dashboard.use('/', Container.get(WarehousesItemController).router());
|
||||||
|
|
||||||
dashboard.use('/dashboard', Container.get(DashboardController).router());
|
dashboard.use('/dashboard', Container.get(DashboardController).router());
|
||||||
|
|||||||
29
packages/server/src/api/middleware/SubscriptionMiddleware.ts
Normal file
29
packages/server/src/api/middleware/SubscriptionMiddleware.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export default (subscriptionSlug = 'main') =>
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const { tenant, tenantId } = req;
|
||||||
|
const { subscriptionRepository } = Container.get('repositories');
|
||||||
|
|
||||||
|
if (!tenant) {
|
||||||
|
throw new Error('Should load `TenancyMiddlware` before this middleware.');
|
||||||
|
}
|
||||||
|
const subscription = await subscriptionRepository.getBySlugInTenant(
|
||||||
|
subscriptionSlug,
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
|
// Validate in case there is no any already subscription.
|
||||||
|
if (!subscription) {
|
||||||
|
return res.boom.badRequest('Tenant has no subscription.', {
|
||||||
|
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate in case the subscription is inactive.
|
||||||
|
else if (subscription.inactive()) {
|
||||||
|
return res.boom.badRequest(null, {
|
||||||
|
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { toInteger } from 'lodash';
|
import { defaultTo, toInteger } from 'lodash';
|
||||||
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
|
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -153,6 +153,13 @@ module.exports = {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign-up email confirmation
|
||||||
|
*/
|
||||||
|
signupConfirmation: {
|
||||||
|
enabled: parseBoolean<boolean>(process.env.SIGNUP_EMAIL_CONFIRMATION, false),
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puppeteer remote browserless connection.
|
* Puppeteer remote browserless connection.
|
||||||
*/
|
*/
|
||||||
@@ -180,6 +187,14 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank Synchronization.
|
||||||
|
*/
|
||||||
|
bankSync: {
|
||||||
|
enabled: parseBoolean(defaultTo(process.env.BANKING_CONNECT, false), false),
|
||||||
|
provider: 'plaid',
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plaid.
|
* Plaid.
|
||||||
*/
|
*/
|
||||||
@@ -190,6 +205,24 @@ module.exports = {
|
|||||||
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
|
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
|
||||||
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
|
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
|
||||||
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
|
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
|
||||||
linkWebhook: process.env.PLAID_LINK_WEBHOOK
|
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lemon Squeezy.
|
||||||
|
*/
|
||||||
|
lemonSqueezy: {
|
||||||
|
key: process.env.LEMONSQUEEZY_API_KEY,
|
||||||
|
storeId: process.env.LEMONSQUEEZY_STORE_ID,
|
||||||
|
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bigcapital (Cloud).
|
||||||
|
* NOTE: DO NOT CHANGE THIS OPTION OR ADD THIS ENV VAR.
|
||||||
|
*/
|
||||||
|
hostedOnBigcapitalCloud: parseBoolean(
|
||||||
|
defaultTo(process.env.HOSTED_ON_BIGCAPITAL_CLOUD, false),
|
||||||
|
false
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class NotAllowedChangeSubscriptionPlan {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.name = "NotAllowedChangeSubscriptionPlan";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
|
||||||
import ServiceError from './ServiceError';
|
import ServiceError from './ServiceError';
|
||||||
import ServiceErrors from './ServiceErrors';
|
import ServiceErrors from './ServiceErrors';
|
||||||
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
|
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
|
||||||
@@ -6,6 +7,7 @@ import TenantDBAlreadyExists from './TenantDBAlreadyExists';
|
|||||||
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
|
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
NotAllowedChangeSubscriptionPlan,
|
||||||
ServiceError,
|
ServiceError,
|
||||||
ServiceErrors,
|
ServiceErrors,
|
||||||
TenantAlreadyInitialized,
|
TenantAlreadyInitialized,
|
||||||
|
|||||||
@@ -66,16 +66,27 @@ export interface IAuthResetedPasswordEventPayload {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface IAuthSendingResetPassword {
|
export interface IAuthSendingResetPassword {
|
||||||
user: ISystemUser,
|
user: ISystemUser;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
export interface IAuthSendedResetPassword {
|
export interface IAuthSendedResetPassword {
|
||||||
user: ISystemUser,
|
user: ISystemUser;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthGetMetaPOJO {
|
export interface IAuthGetMetaPOJO {
|
||||||
signupDisabled: boolean;
|
signupDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAuthSignUpVerifingEventPayload {
|
||||||
|
email: string;
|
||||||
|
verifyToken: string;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthSignUpVerifiedEventPayload {
|
||||||
|
email: string;
|
||||||
|
verifyToken: string;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export enum Features {
|
export enum Features {
|
||||||
WAREHOUSES = 'warehouses',
|
WAREHOUSES = 'warehouses',
|
||||||
BRANCHES = 'branches',
|
BRANCHES = 'branches',
|
||||||
|
BankSyncing = 'BankSyncing'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureAllItem {
|
export interface IFeatureAllItem {
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ export default class MetableStore implements IMetableStore {
|
|||||||
* @param {String} key -
|
* @param {String} key -
|
||||||
* @param {Mixied} defaultValue -
|
* @param {Mixied} defaultValue -
|
||||||
*/
|
*/
|
||||||
get(query: string | IMetaQuery, defaultValue: any): any | false {
|
get(query: string | IMetaQuery, defaultValue: any): any | null {
|
||||||
const metadata = this.find(query);
|
const metadata = this.find(query);
|
||||||
return metadata
|
return metadata
|
||||||
? metadata.value
|
? metadata.value
|
||||||
: typeof defaultValue !== 'undefined'
|
: typeof defaultValue !== 'undefined'
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: false;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
|
|||||||
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
|
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
|
||||||
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
|
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
|
||||||
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
||||||
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
|
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
|
||||||
|
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
|
||||||
|
import { SendVerfiyMailOnSignUp } from '@/services/Authentication/events/SendVerfiyMailOnSignUp';
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -218,6 +221,9 @@ export const susbcribers = () => {
|
|||||||
|
|
||||||
// Cashflow
|
// Cashflow
|
||||||
DeleteCashflowTransactionOnUncategorize,
|
DeleteCashflowTransactionOnUncategorize,
|
||||||
PreventDeleteTransactionOnDelete
|
PreventDeleteTransactionOnDelete,
|
||||||
|
|
||||||
|
SubscribeFreeOnSignupCommunity,
|
||||||
|
SendVerfiyMailOnSignUp
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ export default ({ app }) => {
|
|||||||
// Boom response objects.
|
// Boom response objects.
|
||||||
app.use(boom());
|
app.use(boom());
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(
|
||||||
|
bodyParser.json({
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Parses both json and urlencoded.
|
// Parses both json and urlencoded.
|
||||||
app.use(json());
|
app.use(json());
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleRe
|
|||||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||||
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
||||||
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
||||||
|
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
new ResetPasswordMailJob(agenda);
|
new ResetPasswordMailJob(agenda);
|
||||||
@@ -27,6 +28,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new PaymentReceiveMailNotificationJob(agenda);
|
new PaymentReceiveMailNotificationJob(agenda);
|
||||||
new PlaidFetchTransactionsJob(agenda);
|
new PlaidFetchTransactionsJob(agenda);
|
||||||
new ImportDeleteExpiredFilesJobs(agenda);
|
new ImportDeleteExpiredFilesJobs(agenda);
|
||||||
|
new SendVerifyMailJob(agenda);
|
||||||
|
|
||||||
agenda.start().then(() => {
|
agenda.start().then(() => {
|
||||||
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import {
|
import {
|
||||||
SystemUserRepository,
|
SystemUserRepository,
|
||||||
|
SubscriptionRepository,
|
||||||
TenantRepository,
|
TenantRepository,
|
||||||
} from '@/system/repositories';
|
} from '@/system/repositories';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export default () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
systemUserRepository: new SystemUserRepository(knex, cache),
|
systemUserRepository: new SystemUserRepository(knex, cache),
|
||||||
|
subscriptionRepository: new SubscriptionRepository(knex, cache),
|
||||||
tenantRepository: new TenantRepository(knex, cache),
|
tenantRepository: new TenantRepository(knex, cache),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the item name or code."
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'Rate',
|
name: 'Rate',
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export default {
|
|||||||
name: 'bill_payment.field.payment_number',
|
name: 'bill_payment.field.payment_number',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
unique: true,
|
unique: true,
|
||||||
|
importHint: "The payment number should be unique."
|
||||||
},
|
},
|
||||||
paymentAccountId: {
|
paymentAccountId: {
|
||||||
name: 'bill_payment.field.payment_account',
|
name: 'bill_payment.field.payment_account',
|
||||||
@@ -91,6 +92,7 @@ export default {
|
|||||||
relationModel: 'Account',
|
relationModel: 'Account',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the account name or code."
|
||||||
},
|
},
|
||||||
exchangeRate: {
|
exchangeRate: {
|
||||||
name: 'bill_payment.field.exchange_rate',
|
name: 'bill_payment.field.exchange_rate',
|
||||||
@@ -118,6 +120,7 @@ export default {
|
|||||||
relationModel: 'Bill',
|
relationModel: 'Bill',
|
||||||
relationImportMatch: 'billNumber',
|
relationImportMatch: 'billNumber',
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the bill number."
|
||||||
},
|
},
|
||||||
paymentAmount: {
|
paymentAmount: {
|
||||||
name: 'bill_payment.field.entries.payment_amount',
|
name: 'bill_payment.field.entries.payment_amount',
|
||||||
|
|||||||
@@ -187,18 +187,4 @@ export default class Contact extends TenantModel {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static get fields() {
|
|
||||||
return {
|
|
||||||
contact_service: {
|
|
||||||
column: 'contact_service',
|
|
||||||
},
|
|
||||||
display_name: {
|
|
||||||
column: 'display_name',
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
column: 'created_at',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: 'Matches the item name or code.',
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'Rate',
|
name: 'Rate',
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default {
|
|||||||
relationModel: 'Account',
|
relationModel: 'Account',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the account name or code."
|
||||||
},
|
},
|
||||||
referenceNo: {
|
referenceNo: {
|
||||||
name: 'expense.field.reference_no',
|
name: 'expense.field.reference_no',
|
||||||
@@ -101,6 +102,7 @@ export default {
|
|||||||
relationModel: 'Account',
|
relationModel: 'Account',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the account name or code."
|
||||||
},
|
},
|
||||||
amount: {
|
amount: {
|
||||||
name: 'expense.field.amount',
|
name: 'expense.field.amount',
|
||||||
|
|||||||
@@ -124,117 +124,82 @@ export default {
|
|||||||
fields2: {
|
fields2: {
|
||||||
type: {
|
type: {
|
||||||
name: 'item.field.type',
|
name: 'item.field.type',
|
||||||
column: 'type',
|
|
||||||
fieldType: 'enumeration',
|
fieldType: 'enumeration',
|
||||||
options: [
|
options: [
|
||||||
{ key: 'inventory', label: 'item.field.type.inventory' },
|
{ key: 'inventory', label: 'item.field.type.inventory' },
|
||||||
{ key: 'service', label: 'item.field.type.service' },
|
{ key: 'service', label: 'item.field.type.service' },
|
||||||
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
||||||
],
|
],
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
name: 'item.field.name',
|
name: 'item.field.name',
|
||||||
column: 'name',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
name: 'item.field.code',
|
name: 'item.field.code',
|
||||||
column: 'code',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
sellable: {
|
sellable: {
|
||||||
name: 'item.field.sellable',
|
name: 'item.field.sellable',
|
||||||
column: 'sellable',
|
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
},
|
},
|
||||||
purchasable: {
|
purchasable: {
|
||||||
name: 'item.field.purchasable',
|
name: 'item.field.purchasable',
|
||||||
column: 'purchasable',
|
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
},
|
},
|
||||||
sell_price: {
|
sellPrice: {
|
||||||
name: 'item.field.cost_price',
|
name: 'item.field.sell_price',
|
||||||
column: 'sell_price',
|
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
},
|
},
|
||||||
cost_price: {
|
cost_price: {
|
||||||
|
name: 'item.field.cost_price',
|
||||||
|
fieldType: 'number',
|
||||||
|
},
|
||||||
|
costAccount: {
|
||||||
name: 'item.field.cost_account',
|
name: 'item.field.cost_account',
|
||||||
column: 'cost_price',
|
fieldType: 'relation',
|
||||||
fieldType: 'number',
|
relationModel: 'Account',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
importHint: 'Matches the account name or code.',
|
||||||
},
|
},
|
||||||
cost_account: {
|
sellAccount: {
|
||||||
name: 'item.field.sell_account',
|
name: 'item.field.sell_account',
|
||||||
column: 'cost_account_id',
|
|
||||||
fieldType: 'relation',
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Account',
|
||||||
relationType: 'enumeration',
|
relationImportMatch: ['name', 'code'],
|
||||||
relationKey: 'costAccount',
|
importHint: 'Matches the account name or code.',
|
||||||
|
|
||||||
relationEntityLabel: 'name',
|
|
||||||
relationEntityKey: 'slug',
|
|
||||||
},
|
},
|
||||||
sell_account: {
|
inventoryAccount: {
|
||||||
name: 'item.field.sell_description',
|
|
||||||
column: 'sell_account_id',
|
|
||||||
fieldType: 'relation',
|
|
||||||
|
|
||||||
relationType: 'enumeration',
|
|
||||||
relationKey: 'sellAccount',
|
|
||||||
|
|
||||||
relationEntityLabel: 'name',
|
|
||||||
relationEntityKey: 'slug',
|
|
||||||
},
|
|
||||||
inventory_account: {
|
|
||||||
name: 'item.field.inventory_account',
|
name: 'item.field.inventory_account',
|
||||||
column: 'inventory_account_id',
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Account',
|
||||||
relationType: 'enumeration',
|
relationImportMatch: ['name', 'code'],
|
||||||
relationKey: 'inventoryAccount',
|
importHint: 'Matches the account name or code.',
|
||||||
|
|
||||||
relationEntityLabel: 'name',
|
|
||||||
relationEntityKey: 'slug',
|
|
||||||
},
|
},
|
||||||
sell_description: {
|
sellDescription: {
|
||||||
name: 'Sell description',
|
name: 'Sell Description',
|
||||||
column: 'sell_description',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
purchase_description: {
|
purchaseDescription: {
|
||||||
name: 'Purchase description',
|
name: 'Purchase Description',
|
||||||
column: 'purchase_description',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
quantity_on_hand: {
|
|
||||||
name: 'item.field.quantity_on_hand',
|
|
||||||
column: 'quantity_on_hand',
|
|
||||||
fieldType: 'number',
|
|
||||||
},
|
|
||||||
note: {
|
note: {
|
||||||
name: 'item.field.note',
|
name: 'item.field.note',
|
||||||
column: 'note',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
name: 'item.field.category',
|
name: 'item.field.category',
|
||||||
column: 'category_id',
|
fieldType: 'relation',
|
||||||
|
relationModel: 'ItemCategory',
|
||||||
relationType: 'enumeration',
|
relationImportMatch: ['name'],
|
||||||
relationKey: 'category',
|
importHint: "Matches the category name."
|
||||||
|
|
||||||
relationEntityLabel: 'name',
|
|
||||||
relationEntityKey: 'id',
|
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
name: 'item.field.active',
|
name: 'item.field.active',
|
||||||
column: 'active',
|
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
filterable: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
name: 'item.field.created_at',
|
|
||||||
column: 'created_at',
|
|
||||||
columnType: 'date',
|
|
||||||
fieldType: 'date',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,10 +84,12 @@ export default {
|
|||||||
relationModel: 'Account',
|
relationModel: 'Account',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the account name or code."
|
||||||
},
|
},
|
||||||
paymentReceiveNo: {
|
paymentReceiveNo: {
|
||||||
name: 'payment_receive.field.payment_receive_no',
|
name: 'payment_receive.field.payment_receive_no',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importHint: "The payment number should be unique."
|
||||||
},
|
},
|
||||||
statement: {
|
statement: {
|
||||||
name: 'payment_receive.field.statement',
|
name: 'payment_receive.field.statement',
|
||||||
@@ -106,6 +108,7 @@ export default {
|
|||||||
relationModel: 'SaleInvoice',
|
relationModel: 'SaleInvoice',
|
||||||
relationImportMatch: 'invoiceNo',
|
relationImportMatch: 'invoiceNo',
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the invoice number."
|
||||||
},
|
},
|
||||||
paymentAmount: {
|
paymentAmount: {
|
||||||
name: 'payment_receive.field.entries.payment_amount',
|
name: 'payment_receive.field.entries.payment_amount',
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the item name or code."
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'invoice.field.rate',
|
name: 'invoice.field.rate',
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the item name or code."
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'invoice.field.rate',
|
name: 'invoice.field.rate',
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the item name or code."
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'invoice.field.rate',
|
name: 'invoice.field.rate',
|
||||||
|
|||||||
@@ -53,23 +53,19 @@ export default {
|
|||||||
},
|
},
|
||||||
payee: {
|
payee: {
|
||||||
name: 'Payee',
|
name: 'Payee',
|
||||||
column: 'payee',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
name: 'Description',
|
name: 'Description',
|
||||||
column: 'description',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
referenceNo: {
|
referenceNo: {
|
||||||
name: 'Reference No.',
|
name: 'Reference No.',
|
||||||
column: 'reference_no',
|
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
amount: {
|
amount: {
|
||||||
name: 'Amount',
|
name: 'Amount',
|
||||||
column: 'Amount',
|
fieldType: 'number',
|
||||||
fieldType: 'numeric',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export default {
|
|||||||
relationModel: 'Item',
|
relationModel: 'Item',
|
||||||
relationImportMatch: ['name', 'code'],
|
relationImportMatch: ['name', 'code'],
|
||||||
required: true,
|
required: true,
|
||||||
|
importHint: "Matches the item name or code."
|
||||||
},
|
},
|
||||||
rate: {
|
rate: {
|
||||||
name: 'Rate',
|
name: 'Rate',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Service, Inject, Container } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import {
|
import {
|
||||||
IRegisterDTO,
|
IRegisterDTO,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
@@ -9,6 +9,9 @@ import { AuthSigninService } from './AuthSignin';
|
|||||||
import { AuthSignupService } from './AuthSignup';
|
import { AuthSignupService } from './AuthSignup';
|
||||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||||
import { GetAuthMeta } from './GetAuthMeta';
|
import { GetAuthMeta } from './GetAuthMeta';
|
||||||
|
import { AuthSignupConfirmService } from './AuthSignupConfirm';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { AuthSignupConfirmResend } from './AuthSignupResend';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationApplication {
|
export default class AuthenticationApplication {
|
||||||
@@ -18,6 +21,12 @@ export default class AuthenticationApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private authSignupService: AuthSignupService;
|
private authSignupService: AuthSignupService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authSignupConfirmService: AuthSignupConfirmService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private authSignUpConfirmResendService: AuthSignupConfirmResend;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private authResetPasswordService: AuthSendResetPassword;
|
private authResetPasswordService: AuthSendResetPassword;
|
||||||
|
|
||||||
@@ -44,6 +53,28 @@ export default class AuthenticationApplication {
|
|||||||
return this.authSignupService.signUp(signupDTO);
|
return this.authSignupService.signUp(signupDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verfying the provided user's email after signin-up.
|
||||||
|
* @param {string} email
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<SystemUser>}
|
||||||
|
*/
|
||||||
|
public async signUpConfirm(
|
||||||
|
email: string,
|
||||||
|
token: string
|
||||||
|
): Promise<SystemUser> {
|
||||||
|
return this.authSignupConfirmService.signUpConfirm(email, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends the confirmation email of the given system user.
|
||||||
|
* @param {number} userId - System user id.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async signUpConfirmResend(userId: number) {
|
||||||
|
return this.authSignUpConfirmResendService.signUpConfirmResend(userId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates and retrieve password reset token for the given user email.
|
* Generates and retrieve password reset token for the given user email.
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isEmpty, omit } from 'lodash';
|
import { defaultTo, isEmpty, omit } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import crypto from 'crypto';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import {
|
||||||
IAuthSignedUpEventPayload,
|
IAuthSignedUpEventPayload,
|
||||||
@@ -42,6 +43,13 @@ export class AuthSignupService {
|
|||||||
|
|
||||||
const hashedPassword = await hashPassword(signupDTO.password);
|
const hashedPassword = await hashPassword(signupDTO.password);
|
||||||
|
|
||||||
|
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
||||||
|
const verifiedEnabed = defaultTo(config.signupConfirmation.enabled, false);
|
||||||
|
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
||||||
|
const verified = !verifiedEnabed;
|
||||||
|
|
||||||
|
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
|
||||||
|
|
||||||
// Triggers signin up event.
|
// Triggers signin up event.
|
||||||
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
||||||
signupDTO,
|
signupDTO,
|
||||||
@@ -50,10 +58,12 @@ export class AuthSignupService {
|
|||||||
const tenant = await this.tenantsManager.createTenant();
|
const tenant = await this.tenantsManager.createTenant();
|
||||||
const registeredUser = await systemUserRepository.create({
|
const registeredUser = await systemUserRepository.create({
|
||||||
...omit(signupDTO, 'country'),
|
...omit(signupDTO, 'country'),
|
||||||
|
verifyToken,
|
||||||
|
verified,
|
||||||
active: true,
|
active: true,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
inviteAcceptedAt,
|
||||||
});
|
});
|
||||||
// Triggers signed up event.
|
// Triggers signed up event.
|
||||||
await this.eventPublisher.emitAsync(events.auth.signUp, {
|
await this.eventPublisher.emitAsync(events.auth.signUp, {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { ERRORS } from './_constants';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IAuthSignUpVerifiedEventPayload,
|
||||||
|
IAuthSignUpVerifingEventPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AuthSignupConfirmService {
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided user's email after signing-up.
|
||||||
|
* @throws {ServiceErrors}
|
||||||
|
* @param {IRegisterDTO} signupDTO
|
||||||
|
* @returns {Promise<ISystemUser>}
|
||||||
|
*/
|
||||||
|
public async signUpConfirm(
|
||||||
|
email: string,
|
||||||
|
verifyToken: string
|
||||||
|
): Promise<SystemUser> {
|
||||||
|
const foundUser = await SystemUser.query().findOne({ email, verifyToken });
|
||||||
|
|
||||||
|
if (!foundUser) {
|
||||||
|
throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
const userId = foundUser.id;
|
||||||
|
|
||||||
|
// Triggers `signUpConfirming` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.auth.signUpConfirming, {
|
||||||
|
email,
|
||||||
|
verifyToken,
|
||||||
|
userId,
|
||||||
|
} as IAuthSignUpVerifingEventPayload);
|
||||||
|
|
||||||
|
const updatedUser = await SystemUser.query().patchAndFetchById(
|
||||||
|
foundUser.id,
|
||||||
|
{
|
||||||
|
verified: true,
|
||||||
|
verifyToken: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Triggers `signUpConfirmed` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, {
|
||||||
|
email,
|
||||||
|
verifyToken,
|
||||||
|
userId,
|
||||||
|
} as IAuthSignUpVerifiedEventPayload);
|
||||||
|
|
||||||
|
return updatedUser as SystemUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { ERRORS } from './_constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AuthSignupConfirmResend {
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resends the email confirmation of the given user.
|
||||||
|
* @param {number} userId - User ID.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async signUpConfirmResend(userId: number) {
|
||||||
|
const user = await SystemUser.query().findById(userId).throwIfNotFound();
|
||||||
|
|
||||||
|
// Throw error if the user is already verified.
|
||||||
|
if (user.verified) {
|
||||||
|
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||||
|
}
|
||||||
|
// Throw error if the verification token is not exist.
|
||||||
|
if (!user.verifyToken) {
|
||||||
|
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
email: user.email,
|
||||||
|
token: user.verifyToken,
|
||||||
|
fullName: user.firstName,
|
||||||
|
};
|
||||||
|
await this.agenda.now('send-signup-verify-mail', payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,4 +33,33 @@ export default class AuthenticationMailMesssages {
|
|||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends signup verification mail.
|
||||||
|
* @param {string} email - Email address
|
||||||
|
* @param {string} fullName - User name.
|
||||||
|
* @param {string} token - Verification token.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async sendSignupVerificationMail(
|
||||||
|
email: string,
|
||||||
|
fullName: string,
|
||||||
|
token: string
|
||||||
|
) {
|
||||||
|
const verifyUrl = `${config.baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
||||||
|
|
||||||
|
await new Mail()
|
||||||
|
.setSubject('Bigcapital - Verify your email')
|
||||||
|
.setView('mail/SignupVerifyEmail.html')
|
||||||
|
.setTo(email)
|
||||||
|
.setAttachments([
|
||||||
|
{
|
||||||
|
filename: 'bigcapital.png',
|
||||||
|
path: `${global.__views_dir}/images/bigcapital.png`,
|
||||||
|
cid: 'bigcapital_logo',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.setData({ verifyUrl, fullName })
|
||||||
|
.send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ export const ERRORS = {
|
|||||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||||
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||||
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
||||||
|
SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID',
|
||||||
|
USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { Inject } from 'typedi';
|
||||||
|
|
||||||
|
export class SendVerfiyMailOnSignUp {
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handles.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(events.auth.signUp, this.handleSendVerifyMailOnSignup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ITaxRateEditedPayload} payload -
|
||||||
|
*/
|
||||||
|
private handleSendVerifyMailOnSignup = async ({
|
||||||
|
user,
|
||||||
|
}: IAuthSignedUpEventPayload) => {
|
||||||
|
const payload = {
|
||||||
|
email: user.email,
|
||||||
|
token: user.verifyToken,
|
||||||
|
fullName: user.firstName,
|
||||||
|
};
|
||||||
|
await this.agenda.now('send-signup-verify-mail', payload);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Container } from 'typedi';
|
||||||
|
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages';
|
||||||
|
|
||||||
|
export class SendVerifyMailJob {
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {Agenda} agenda
|
||||||
|
*/
|
||||||
|
constructor(agenda) {
|
||||||
|
agenda.define(
|
||||||
|
'send-signup-verify-mail',
|
||||||
|
{ priority: 'high' },
|
||||||
|
this.handler.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle send welcome mail job.
|
||||||
|
* @param {Job} job
|
||||||
|
* @param {Function} done
|
||||||
|
*/
|
||||||
|
public async handler(job, done: Function): Promise<void> {
|
||||||
|
const { data } = job.attrs;
|
||||||
|
const { email, fullName, token } = data;
|
||||||
|
const authService = Container.get(AuthenticationMailMesssages);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authService.sendSignupVerificationMail(email, fullName, token);
|
||||||
|
done();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
done(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,7 +79,6 @@ export interface ICashflowTransactionTypeMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BankTransactionsSampleData = [
|
export const BankTransactionsSampleData = [
|
||||||
[
|
|
||||||
{
|
{
|
||||||
Amount: '6,410.19',
|
Amount: '6,410.19',
|
||||||
Date: '2024-03-26',
|
Date: '2024-03-26',
|
||||||
@@ -101,5 +100,4 @@ export const BankTransactionsSampleData = [
|
|||||||
'Reference No.': 'REF-1',
|
'Reference No.': 'REF-1',
|
||||||
Description: 'Occaecati consequuntur cum impedit illo.',
|
Description: 'Occaecati consequuntur cum impedit illo.',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ export class CustomersApplication {
|
|||||||
* @param {ISystemUser} authorizedUser
|
* @param {ISystemUser} authorizedUser
|
||||||
* @returns {Promise<ICustomer>}
|
* @returns {Promise<ICustomer>}
|
||||||
*/
|
*/
|
||||||
public createCustomer = (
|
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
|
||||||
tenantId: number,
|
|
||||||
customerDTO: ICustomerNewDTO,
|
|
||||||
) => {
|
|
||||||
return this.createCustomerService.createCustomer(tenantId, customerDTO);
|
return this.createCustomerService.createCustomer(tenantId, customerDTO);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
IVendorEditDTO,
|
IVendorEditDTO,
|
||||||
@@ -42,13 +43,9 @@ export class VendorsApplication {
|
|||||||
public createVendor = (
|
public createVendor = (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
vendorDTO: IVendorNewDTO,
|
vendorDTO: IVendorNewDTO,
|
||||||
authorizedUser: ISystemUser
|
trx?: Knex.Transaction
|
||||||
) => {
|
) => {
|
||||||
return this.createVendorService.createVendor(
|
return this.createVendorService.createVendor(tenantId, vendorDTO, trx);
|
||||||
tenantId,
|
|
||||||
vendorDTO,
|
|
||||||
authorizedUser
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
ICreditNoteCreatedPayload,
|
ICreditNoteCreatedPayload,
|
||||||
ICreditNoteCreatingPayload,
|
ICreditNoteCreatingPayload,
|
||||||
ICreditNoteNewDTO,
|
ICreditNoteNewDTO,
|
||||||
ISystemUser,
|
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { defaultTo } from 'lodash';
|
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { omit } from 'lodash';
|
|
||||||
import { FeaturesSettingsDriver } from './FeaturesSettingsDriver';
|
import { FeaturesSettingsDriver } from './FeaturesSettingsDriver';
|
||||||
import { FeaturesConfigureManager } from './FeaturesConfigureManager';
|
|
||||||
import { IFeatureAllItem } from '@/interfaces';
|
import { IFeatureAllItem } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -10,9 +7,6 @@ export class FeaturesManager {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private drive: FeaturesSettingsDriver;
|
private drive: FeaturesSettingsDriver;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private configure: FeaturesConfigureManager;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns-on the given feature name.
|
* Turns-on the given feature name.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -40,35 +34,15 @@ export class FeaturesManager {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async accessible(tenantId: number, feature: string) {
|
public async accessible(tenantId: number, feature: string) {
|
||||||
// Retrieves the feature default accessible value.
|
return this.drive.accessible(tenantId, feature);
|
||||||
const defaultValue = this.configure.getFeatureConfigure(
|
|
||||||
feature,
|
|
||||||
'defaultValue'
|
|
||||||
);
|
|
||||||
const isAccessible = await this.drive.accessible(tenantId, feature);
|
|
||||||
|
|
||||||
return defaultTo(isAccessible, defaultValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the all features and their accessible value and default value.
|
* Retrieves the all features and their accessible value and default value.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @returns
|
* @returns {Promise<IFeatureAllItem[]>}
|
||||||
*/
|
*/
|
||||||
public async all(tenantId: number): Promise<IFeatureAllItem[]> {
|
public async all(tenantId: number): Promise<IFeatureAllItem[]> {
|
||||||
const all = await this.drive.all(tenantId);
|
return this.drive.all(tenantId);
|
||||||
|
|
||||||
return all.map((feature: IFeatureAllItem) => {
|
|
||||||
const defaultAccessible = this.configure.getFeatureConfigure(
|
|
||||||
feature.name,
|
|
||||||
'defaultValue'
|
|
||||||
);
|
|
||||||
const isAccessible = feature.isAccessible;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...feature,
|
|
||||||
isAccessible: defaultTo(isAccessible, defaultAccessible),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import { Service, Inject } from 'typedi';
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { FeaturesConfigure } from './constants';
|
import { FeaturesConfigure } from './constants';
|
||||||
import { IFeatureAllItem } from '@/interfaces';
|
import { IFeatureAllItem } from '@/interfaces';
|
||||||
|
import { FeaturesConfigureManager } from './FeaturesConfigureManager';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class FeaturesSettingsDriver {
|
export class FeaturesSettingsDriver {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private configure: FeaturesConfigureManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns-on the given feature name.
|
* Turns-on the given feature name.
|
||||||
@@ -41,7 +45,15 @@ export class FeaturesSettingsDriver {
|
|||||||
async accessible(tenantId: number, feature: string) {
|
async accessible(tenantId: number, feature: string) {
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
return !!settings.get({ group: 'features', key: feature });
|
const defaultValue = this.configure.getFeatureConfigure(
|
||||||
|
feature,
|
||||||
|
'defaultValue'
|
||||||
|
);
|
||||||
|
const settingValue = settings.get(
|
||||||
|
{ group: 'features', key: feature },
|
||||||
|
defaultValue
|
||||||
|
);
|
||||||
|
return settingValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Features, IFeatureConfiugration } from '@/interfaces';
|
import { Features, IFeatureConfiugration } from '@/interfaces';
|
||||||
|
import config from '@/config';
|
||||||
|
import { defaultTo } from 'lodash';
|
||||||
|
|
||||||
export const FeaturesConfigure: IFeatureConfiugration[] = [
|
export const FeaturesConfigure: IFeatureConfiugration[] = [
|
||||||
{
|
{
|
||||||
@@ -9,4 +11,8 @@ export const FeaturesConfigure: IFeatureConfiugration[] = [
|
|||||||
name: Features.WAREHOUSES,
|
name: Features.WAREHOUSES,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: Features.BankSyncing,
|
||||||
|
defaultValue: defaultTo(config.bankSync.enabled, false),
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { chain } from 'lodash';
|
import { chain } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ERRORS, getSheetColumns, getUnmappedSheetColumns, readImportFile } from './_utils';
|
import {
|
||||||
|
ERRORS,
|
||||||
|
getSheetColumns,
|
||||||
|
getUnmappedSheetColumns,
|
||||||
|
readImportFile,
|
||||||
|
} from './_utils';
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
@@ -49,10 +54,9 @@ export class ImportFileProcess {
|
|||||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||||
const header = getSheetColumns(sheetData);
|
const header = getSheetColumns(sheetData);
|
||||||
|
|
||||||
const resourceFields = this.resource.getResourceFields2(
|
const resource = importFile.resource;
|
||||||
tenantId,
|
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||||
importFile.resource
|
|
||||||
);
|
|
||||||
// Runs the importing operation with ability to return errors that will happen.
|
// Runs the importing operation with ability to return errors that will happen.
|
||||||
const [successedImport, failedImport, allData] =
|
const [successedImport, failedImport, allData] =
|
||||||
await this.uow.withTransaction(
|
await this.uow.withTransaction(
|
||||||
@@ -91,6 +95,7 @@ export class ImportFileProcess {
|
|||||||
const skippedCount = errorsCount;
|
const skippedCount = errorsCount;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
resource,
|
||||||
createdCount,
|
createdCount,
|
||||||
skippedCount,
|
skippedCount,
|
||||||
totalCount,
|
totalCount,
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ export class ImportFileUploadService {
|
|||||||
filename: string,
|
filename: string,
|
||||||
params: Record<string, number | string>
|
params: Record<string, number | string>
|
||||||
): Promise<ImportFileUploadPOJO> {
|
): Promise<ImportFileUploadPOJO> {
|
||||||
console.log(filename, 'filename');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.importUnhandled(
|
return await this.importUnhandled(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
import {
|
import {
|
||||||
defaultTo,
|
defaultTo,
|
||||||
upperFirst,
|
upperFirst,
|
||||||
@@ -353,7 +354,6 @@ export const parseKey = R.curry(
|
|||||||
_key = `${fieldKey}`;
|
_key = `${fieldKey}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(_key);
|
|
||||||
return _key;
|
return _key;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -432,13 +432,19 @@ export const sanitizeSheetData = (json) => {
|
|||||||
export const getMapToPath = (to: string, group = '') =>
|
export const getMapToPath = (to: string, group = '') =>
|
||||||
group ? `${group}.${to}` : to;
|
group ? `${group}.${to}` : to;
|
||||||
|
|
||||||
|
export const getImportsStoragePath = () => {
|
||||||
|
return path.join(global.__storage_dir, `/imports`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the imported file from the storage and database.
|
* Deletes the imported file from the storage and database.
|
||||||
* @param {string} filename
|
* @param {string} filename
|
||||||
*/
|
*/
|
||||||
export const deleteImportFile = async (filename: string) => {
|
export const deleteImportFile = async (filename: string) => {
|
||||||
|
const filePath = getImportsStoragePath();
|
||||||
|
|
||||||
// Deletes the imported file.
|
// Deletes the imported file.
|
||||||
await fs.unlink(`public/imports/${filename}`);
|
await fs.unlink(`${filePath}/${filename}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -447,5 +453,7 @@ export const deleteImportFile = async (filename: string) => {
|
|||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
export const readImportFile = (filename: string) => {
|
export const readImportFile = (filename: string) => {
|
||||||
return fs.readFile(`public/imports/${filename}`);
|
const filePath = getImportsStoragePath();
|
||||||
|
|
||||||
|
return fs.readFile(`${filePath}/${filename}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ImportFileMapPOJO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportFilePreviewPOJO {
|
export interface ImportFilePreviewPOJO {
|
||||||
|
resource: string;
|
||||||
createdCount: number;
|
createdCount: number;
|
||||||
skippedCount: number;
|
skippedCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Knex } from 'knex';
|
|||||||
import { Importable } from '@/services/Import/Importable';
|
import { Importable } from '@/services/Import/Importable';
|
||||||
import { IItemCreateDTO } from '@/interfaces';
|
import { IItemCreateDTO } from '@/interfaces';
|
||||||
import { CreateItem } from './CreateItem';
|
import { CreateItem } from './CreateItem';
|
||||||
|
import { ItemsSampleData } from './constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ItemsImportable extends Importable {
|
export class ItemsImportable extends Importable {
|
||||||
@@ -28,6 +29,6 @@ export class ItemsImportable extends Importable {
|
|||||||
* Retrieves the sample data of customers used to download sample sheet.
|
* Retrieves the sample data of customers used to download sample sheet.
|
||||||
*/
|
*/
|
||||||
public sampleData(): any[] {
|
public sampleData(): any[] {
|
||||||
return [];
|
return ItemsSampleData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
NOT_FOUND: 'NOT_FOUND',
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||||
@@ -19,7 +18,8 @@ export const ERRORS = {
|
|||||||
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
||||||
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||||
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
|
||||||
|
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||||
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||||
|
|
||||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||||
@@ -53,8 +53,84 @@ export const DEFAULT_VIEWS = [
|
|||||||
slug: 'non-inventory',
|
slug: 'non-inventory',
|
||||||
rolesLogicExpression: '1',
|
rolesLogicExpression: '1',
|
||||||
roles: [
|
roles: [
|
||||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'non-inventory' },
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'type',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'non-inventory',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
|
export const ItemsSampleData = [
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||||
|
'Item Code': '1000',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'At dolor est non tempore et quisquam.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Schmitt Group',
|
||||||
|
'Item Code': '1001',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Marks - Carroll',
|
||||||
|
'Item Code': '1002',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Odio odio minus similique.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'VonRueden, Ruecker and Hettinger',
|
||||||
|
'Item Code': '1003',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Quibusdam dolores illo.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export default class OrganizationService {
|
|||||||
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
||||||
const tenant = await Tenant.query()
|
const tenant = await Tenant.query()
|
||||||
.findById(tenantId)
|
.findById(tenantId)
|
||||||
|
.withGraphFetched('subscriptions')
|
||||||
.withGraphFetched('metadata');
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
this.throwIfTenantNotExists(tenant);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default class CreateVendorCredit extends BaseVendorCredit {
|
|||||||
* Creates a new vendor credit.
|
* Creates a new vendor credit.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {IVendorCreditCreateDTO} vendorCreditCreateDTO -
|
* @param {IVendorCreditCreateDTO} vendorCreditCreateDTO -
|
||||||
|
* @param {Knex.Transaction} trx -
|
||||||
*/
|
*/
|
||||||
public newVendorCredit = async (
|
public newVendorCredit = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
import { configureLemonSqueezy } from './utils';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LemonSqueezyService {
|
||||||
|
/**
|
||||||
|
* Retrieves the LemonSqueezy checkout url.
|
||||||
|
* @param {number} variantId
|
||||||
|
* @param {SystemUser} user
|
||||||
|
*/
|
||||||
|
async getCheckout(variantId: number, user: SystemUser) {
|
||||||
|
configureLemonSqueezy();
|
||||||
|
|
||||||
|
return createCheckout(process.env.LEMONSQUEEZY_STORE_ID!, variantId, {
|
||||||
|
checkoutOptions: {
|
||||||
|
embed: true,
|
||||||
|
media: true,
|
||||||
|
logo: true,
|
||||||
|
},
|
||||||
|
checkoutData: {
|
||||||
|
email: user.email,
|
||||||
|
custom: {
|
||||||
|
user_id: user.id + '',
|
||||||
|
tenant_id: user.tenantId + '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
productOptions: {
|
||||||
|
enabledVariants: [variantId],
|
||||||
|
redirectUrl: `http://localhost:4000/dashboard/billing/`,
|
||||||
|
receiptButtonText: 'Go to Dashboard',
|
||||||
|
receiptThankYouNote: 'Thank you for signing up to Lemon Stand!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { getPrice } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
import config from '@/config';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
compareSignatures,
|
||||||
|
configureLemonSqueezy,
|
||||||
|
createHmacSignature,
|
||||||
|
webhookHasData,
|
||||||
|
webhookHasMeta,
|
||||||
|
} from './utils';
|
||||||
|
import { Plan } from '@/system/models';
|
||||||
|
import { Subscription } from './Subscription';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LemonSqueezyWebhooks {
|
||||||
|
@Inject()
|
||||||
|
private subscriptionService: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle the LemonSqueezy webhooks.
|
||||||
|
* @param {string} rawBody
|
||||||
|
* @param {string} signature
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async handlePostWebhook(
|
||||||
|
rawData: any,
|
||||||
|
data: Record<string, any>,
|
||||||
|
signature: string
|
||||||
|
): Promise<void> {
|
||||||
|
configureLemonSqueezy();
|
||||||
|
|
||||||
|
if (!config.lemonSqueezy.webhookSecret) {
|
||||||
|
throw new Error('Lemon Squeezy Webhook Secret not set in .env');
|
||||||
|
}
|
||||||
|
const secret = config.lemonSqueezy.webhookSecret;
|
||||||
|
const hmacSignature = createHmacSignature(secret, rawData);
|
||||||
|
|
||||||
|
if (!compareSignatures(hmacSignature, signature)) {
|
||||||
|
throw new Error('Invalid signature');
|
||||||
|
}
|
||||||
|
// Type guard to check if the object has a 'meta' property.
|
||||||
|
if (webhookHasMeta(data)) {
|
||||||
|
// Non-blocking call to process the webhook event.
|
||||||
|
void this.processWebhookEvent(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Data invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This action will process a webhook event in the database.
|
||||||
|
* @param {unknown} eventBody -
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
private async processWebhookEvent(eventBody): Promise<void> {
|
||||||
|
const webhookEvent = eventBody.meta.event_name;
|
||||||
|
|
||||||
|
const userId = eventBody.meta.custom_data?.user_id;
|
||||||
|
const tenantId = eventBody.meta.custom_data?.tenant_id;
|
||||||
|
|
||||||
|
if (!webhookHasMeta(eventBody)) {
|
||||||
|
throw new Error("Event body is missing the 'meta' property.");
|
||||||
|
} else if (webhookHasData(eventBody)) {
|
||||||
|
if (webhookEvent.startsWith('subscription_payment_')) {
|
||||||
|
// Save subscription invoices; eventBody is a SubscriptionInvoice
|
||||||
|
// Not implemented.
|
||||||
|
} else if (webhookEvent.startsWith('subscription_')) {
|
||||||
|
// Save subscription events; obj is a Subscription
|
||||||
|
const attributes = eventBody.data.attributes;
|
||||||
|
const variantId = attributes.variant_id as string;
|
||||||
|
|
||||||
|
// We assume that the Plan table is up to date.
|
||||||
|
const plan = await Plan.query().findOne('slug', 'early-adaptor');
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
throw new Error(`Plan with variantId ${variantId} not found.`);
|
||||||
|
} else {
|
||||||
|
// Update the subscription in the database.
|
||||||
|
const priceId = attributes.first_subscription_item.price_id;
|
||||||
|
|
||||||
|
// Get the price data from Lemon Squeezy.
|
||||||
|
const priceData = await getPrice(priceId);
|
||||||
|
|
||||||
|
if (priceData.error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get the price data for the subscription ${eventBody.data.id}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isUsageBased =
|
||||||
|
attributes.first_subscription_item.is_usage_based;
|
||||||
|
const price = isUsageBased
|
||||||
|
? priceData.data?.data.attributes.unit_price_decimal
|
||||||
|
: priceData.data?.data.attributes.unit_price;
|
||||||
|
|
||||||
|
// Create a new subscription of the tenant.
|
||||||
|
if (webhookEvent === 'subscription_created') {
|
||||||
|
await this.subscriptionService.newSubscribtion(
|
||||||
|
tenantId,
|
||||||
|
'early-adaptor',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (webhookEvent.startsWith('order_')) {
|
||||||
|
// Save orders; eventBody is a "Order"
|
||||||
|
/* Not implemented */
|
||||||
|
} else if (webhookEvent.startsWith('license_')) {
|
||||||
|
// Save license keys; eventBody is a "License key"
|
||||||
|
/* Not implemented */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
packages/server/src/services/Subscription/Subscription.ts
Normal file
52
packages/server/src/services/Subscription/Subscription.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
||||||
|
import { Plan, Tenant } from '@/system/models';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class Subscription {
|
||||||
|
/**
|
||||||
|
* Give the tenant a new subscription.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {string} planSlug - Plan slug.
|
||||||
|
* @param {string} invoiceInterval
|
||||||
|
* @param {number} invoicePeriod
|
||||||
|
* @param {string} subscriptionSlug
|
||||||
|
*/
|
||||||
|
public async newSubscribtion(
|
||||||
|
tenantId: number,
|
||||||
|
planSlug: string,
|
||||||
|
subscriptionSlug: string = 'main'
|
||||||
|
) {
|
||||||
|
const tenant = await Tenant.query().findById(tenantId).throwIfNotFound();
|
||||||
|
const plan = await Plan.query().findOne('slug', planSlug).throwIfNotFound();
|
||||||
|
|
||||||
|
const isFree = plan.price === 0;
|
||||||
|
|
||||||
|
// Take the invoice interval and period from the given plan.
|
||||||
|
const invoiceInterval = plan.invoiceInternal;
|
||||||
|
const invoicePeriod = isFree ? Infinity : plan.invoicePeriod;
|
||||||
|
|
||||||
|
const subscription = await tenant
|
||||||
|
.$relatedQuery('subscriptions')
|
||||||
|
.modify('subscriptionBySlug', subscriptionSlug)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
// No allowed to re-new the the subscription while the subscription is active.
|
||||||
|
if (subscription && subscription.active()) {
|
||||||
|
throw new NotAllowedChangeSubscriptionPlan();
|
||||||
|
|
||||||
|
// In case there is already subscription associated to the given tenant renew it.
|
||||||
|
} else if (subscription && subscription.inactive()) {
|
||||||
|
await subscription.renew(invoiceInterval, invoicePeriod);
|
||||||
|
|
||||||
|
// No stored past tenant subscriptions create new one.
|
||||||
|
} else {
|
||||||
|
await tenant.newSubscription(
|
||||||
|
plan.id,
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
subscriptionSlug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import moment, { unitOfTime } from 'moment';
|
||||||
|
|
||||||
|
export default class SubscriptionPeriod {
|
||||||
|
private start: Date;
|
||||||
|
private end: Date;
|
||||||
|
private interval: string;
|
||||||
|
private count: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {string} interval -
|
||||||
|
* @param {number} count -
|
||||||
|
* @param {Date} start -
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
interval: unitOfTime.DurationConstructor = 'month',
|
||||||
|
count: number,
|
||||||
|
start?: Date
|
||||||
|
) {
|
||||||
|
this.interval = interval;
|
||||||
|
this.count = count;
|
||||||
|
this.start = start;
|
||||||
|
|
||||||
|
if (!start) {
|
||||||
|
this.start = moment().toDate();
|
||||||
|
}
|
||||||
|
if (count === Infinity) {
|
||||||
|
this.end = null;
|
||||||
|
} else {
|
||||||
|
this.end = moment(start).add(count, interval).toDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartDate() {
|
||||||
|
return this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndDate() {
|
||||||
|
return this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInterval() {
|
||||||
|
return this.interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIntervalCount() {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SubscriptionService {
|
||||||
|
/**
|
||||||
|
* Retrieve all subscription of the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
public async getSubscriptions(tenantId: number) {
|
||||||
|
const subscriptions = await PlanSubscription.query().where(
|
||||||
|
'tenant_id',
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
|
return subscriptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { IAuthSignedUpEventPayload } from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import config from '@/config';
|
||||||
|
import { Subscription } from '../Subscription';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SubscribeFreeOnSignupCommunity {
|
||||||
|
@Inject()
|
||||||
|
private subscriptionService: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handlers.
|
||||||
|
*/
|
||||||
|
public attach = (bus) => {
|
||||||
|
bus.subscribe(
|
||||||
|
events.auth.signUp,
|
||||||
|
this.subscribeFreeOnSigupCommunity.bind(this)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new free subscription once the user signup if the app is self-hosted.
|
||||||
|
* @param {IAuthSignedUpEventPayload}
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
private async subscribeFreeOnSigupCommunity({
|
||||||
|
signupDTO,
|
||||||
|
tenant,
|
||||||
|
user,
|
||||||
|
}: IAuthSignedUpEventPayload) {
|
||||||
|
if (config.hostedOnBigcapitalCloud) return null;
|
||||||
|
|
||||||
|
await this.subscriptionService.newSubscribtion(tenant.id, 'free');
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/server/src/services/Subscription/utils.ts
Normal file
100
packages/server/src/services/Subscription/utils.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that required environment variables are set and sets up the Lemon
|
||||||
|
* Squeezy JS SDK. Throws an error if any environment variables are missing or
|
||||||
|
* if there's an error setting up the SDK.
|
||||||
|
*/
|
||||||
|
export function configureLemonSqueezy() {
|
||||||
|
const requiredVars = [
|
||||||
|
'LEMONSQUEEZY_API_KEY',
|
||||||
|
'LEMONSQUEEZY_STORE_ID',
|
||||||
|
'LEMONSQUEEZY_WEBHOOK_SECRET',
|
||||||
|
];
|
||||||
|
const missingVars = requiredVars.filter((varName) => !process.env[varName]);
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing required LEMONSQUEEZY env variables: ${missingVars.join(
|
||||||
|
', '
|
||||||
|
)}. Please, set them in your .env file.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lemonSqueezySetup({
|
||||||
|
apiKey: process.env.LEMONSQUEEZY_API_KEY,
|
||||||
|
onError: (error) => {
|
||||||
|
// eslint-disable-next-line no-console -- allow logging
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`Lemon Squeezy API error: ${error.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if the value is an object.
|
||||||
|
*/
|
||||||
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typeguard to check if the object has a 'meta' property
|
||||||
|
* and that the 'meta' property has the correct shape.
|
||||||
|
*/
|
||||||
|
export function webhookHasMeta(obj: unknown): obj is {
|
||||||
|
meta: {
|
||||||
|
event_name: string;
|
||||||
|
custom_data: {
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
if (
|
||||||
|
isObject(obj) &&
|
||||||
|
isObject(obj.meta) &&
|
||||||
|
typeof obj.meta.event_name === 'string' &&
|
||||||
|
isObject(obj.meta.custom_data) &&
|
||||||
|
typeof obj.meta.custom_data.user_id === 'string'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typeguard to check if the object has a 'data' property and the correct shape.
|
||||||
|
*
|
||||||
|
* @param obj - The object to check.
|
||||||
|
* @returns True if the object has a 'data' property.
|
||||||
|
*/
|
||||||
|
export function webhookHasData(obj: unknown): obj is {
|
||||||
|
data: {
|
||||||
|
attributes: Record<string, unknown> & {
|
||||||
|
first_subscription_item: {
|
||||||
|
id: number;
|
||||||
|
price_id: number;
|
||||||
|
is_usage_based: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
isObject(obj) &&
|
||||||
|
'data' in obj &&
|
||||||
|
isObject(obj.data) &&
|
||||||
|
'attributes' in obj.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHmacSignature(secretKey, body) {
|
||||||
|
return require('crypto')
|
||||||
|
.createHmac('sha256', secretKey)
|
||||||
|
.update(body)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareSignatures(signature, comparison_signature) {
|
||||||
|
const source = Buffer.from(signature, 'utf8');
|
||||||
|
const comparison = Buffer.from(comparison_signature, 'utf8');
|
||||||
|
return require('crypto').timingSafeEqual(source, comparison);
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ export default {
|
|||||||
signUp: 'onSignUp',
|
signUp: 'onSignUp',
|
||||||
signingUp: 'onSigningUp',
|
signingUp: 'onSigningUp',
|
||||||
|
|
||||||
|
signUpConfirming: 'signUpConfirming',
|
||||||
|
signUpConfirmed: 'signUpConfirmed',
|
||||||
|
|
||||||
sendingResetPassword: 'onSendingResetPassword',
|
sendingResetPassword: 'onSendingResetPassword',
|
||||||
sendResetPassword: 'onSendResetPassword',
|
sendResetPassword: 'onSendResetPassword',
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('subscriptions_plans', table => {
|
||||||
|
table.increments();
|
||||||
|
|
||||||
|
table.string('name');
|
||||||
|
table.string('description');
|
||||||
|
table.decimal('price');
|
||||||
|
table.string('currency', 3);
|
||||||
|
|
||||||
|
table.integer('trial_period');
|
||||||
|
table.string('trial_interval');
|
||||||
|
|
||||||
|
table.integer('invoice_period');
|
||||||
|
table.string('invoice_interval');
|
||||||
|
table.timestamps();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('subscriptions_plans')
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('subscription_plans', table => {
|
||||||
|
table.increments();
|
||||||
|
table.string('slug');
|
||||||
|
table.string('name');
|
||||||
|
table.string('desc');
|
||||||
|
table.boolean('active');
|
||||||
|
|
||||||
|
table.decimal('price').unsigned();
|
||||||
|
table.string('currency', 3);
|
||||||
|
|
||||||
|
table.decimal('trial_period').nullable();
|
||||||
|
table.string('trial_interval').nullable();
|
||||||
|
|
||||||
|
table.decimal('invoice_period').nullable();
|
||||||
|
table.string('invoice_interval').nullable();
|
||||||
|
|
||||||
|
table.integer('index').unsigned();
|
||||||
|
table.timestamps();
|
||||||
|
}).then(() => {
|
||||||
|
return knex.seed.run({
|
||||||
|
specific: 'seed_subscriptions_plans.js',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('subscription_plans')
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
exports.up = function(knex) {
|
||||||
|
return knex.schema.createTable('subscription_plan_subscriptions', table => {
|
||||||
|
table.increments('id');
|
||||||
|
table.string('slug');
|
||||||
|
|
||||||
|
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||||
|
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||||
|
|
||||||
|
table.dateTime('starts_at').nullable();
|
||||||
|
table.dateTime('ends_at').nullable();
|
||||||
|
|
||||||
|
table.dateTime('cancels_at').nullable();
|
||||||
|
table.dateTime('canceled_at').nullable();
|
||||||
|
|
||||||
|
table.timestamps();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex) {
|
||||||
|
return knex.schema.dropTableIfExists('subscription_plan_subscriptions');
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.seed.run({
|
||||||
|
specific: 'seed_tenants_free_subscription.js',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.table('users', (table) => {
|
||||||
|
table.string('verify_token');
|
||||||
|
table.boolean('verified').defaultTo(false);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return knex('USERS').update({ verified: true });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (knex) => {};
|
||||||
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Model, mixin } from 'objection';
|
||||||
|
import SystemModel from '@/system/models/SystemModel';
|
||||||
|
import { PlanSubscription } from '..';
|
||||||
|
|
||||||
|
export default class Plan extends mixin(SystemModel) {
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'subscription_plans';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['isFree', 'hasTrial'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
getFeatureBySlug(builder, featureSlug) {
|
||||||
|
builder.where('slug', featureSlug);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const PlanSubscription = require('system/models/Subscriptions/PlanSubscription');
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* The plan may have many subscriptions.
|
||||||
|
*/
|
||||||
|
subscriptions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: PlanSubscription.default,
|
||||||
|
join: {
|
||||||
|
from: 'subscription_plans.id',
|
||||||
|
to: 'subscription_plan_subscriptions.planId',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if plan is free.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isFree() {
|
||||||
|
return this.price <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if plan is paid.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isPaid() {
|
||||||
|
return !this.isFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if plan has trial.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
hasTrial() {
|
||||||
|
return this.trialPeriod && this.trialInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Model, mixin } from 'objection';
|
||||||
|
import SystemModel from '@/system/models/SystemModel';
|
||||||
|
import moment from 'moment';
|
||||||
|
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||||
|
|
||||||
|
export default class PlanSubscription extends mixin(SystemModel) {
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'subscription_plan_subscriptions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['active', 'inactive', 'ended', 'onTrial'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifiers queries.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
activeSubscriptions(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const now = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('ends_at', '>', now);
|
||||||
|
builder.where('trial_ends_at', '>', now);
|
||||||
|
},
|
||||||
|
|
||||||
|
inactiveSubscriptions() {
|
||||||
|
builder.modify('endedTrial');
|
||||||
|
builder.modify('endedPeriod');
|
||||||
|
},
|
||||||
|
|
||||||
|
subscriptionBySlug(builder, subscriptionSlug) {
|
||||||
|
builder.where('slug', subscriptionSlug);
|
||||||
|
},
|
||||||
|
|
||||||
|
endedTrial(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const endDate = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('ends_at', '<=', endDate);
|
||||||
|
},
|
||||||
|
|
||||||
|
endedPeriod(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const endDate = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('trial_ends_at', '<=', endDate);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relations mappings.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const Tenant = require('system/models/Tenant');
|
||||||
|
const Plan = require('system/models/Subscriptions/Plan');
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Plan subscription belongs to tenant.
|
||||||
|
*/
|
||||||
|
tenant: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Tenant.default,
|
||||||
|
join: {
|
||||||
|
from: 'subscription_plan_subscriptions.tenantId',
|
||||||
|
to: 'tenants.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan description belongs to plan.
|
||||||
|
*/
|
||||||
|
plan: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Plan.default,
|
||||||
|
join: {
|
||||||
|
from: 'subscription_plan_subscriptions.planId',
|
||||||
|
to: 'subscription_plans.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if subscription is active.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
active() {
|
||||||
|
return !this.ended() || this.onTrial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if subscription is inactive.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
inactive() {
|
||||||
|
return !this.active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if subscription period has ended.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
ended() {
|
||||||
|
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if subscription is currently on trial.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
onTrial() {
|
||||||
|
return this.trailEndsAt ? moment().isAfter(this.trailEndsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new period from the given details.
|
||||||
|
* @param {string} invoiceInterval
|
||||||
|
* @param {number} invoicePeriod
|
||||||
|
* @param {string} start
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
static setNewPeriod(invoiceInterval, invoicePeriod, start) {
|
||||||
|
const period = new SubscriptionPeriod(
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
start,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startsAt = period.getStartDate();
|
||||||
|
const endsAt = period.getEndDate();
|
||||||
|
|
||||||
|
return { startsAt, endsAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renews subscription period.
|
||||||
|
* @Promise
|
||||||
|
*/
|
||||||
|
renew(invoiceInterval, invoicePeriod) {
|
||||||
|
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
);
|
||||||
|
return this.$query().update({ startsAt, endsAt });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,12 @@ import SystemModel from '@/system/models/SystemModel';
|
|||||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||||
|
|
||||||
export default class SystemUser extends SystemModel {
|
export default class SystemUser extends SystemModel {
|
||||||
|
firstName!: string;
|
||||||
|
lastName!: string;
|
||||||
|
verified!: boolean;
|
||||||
|
inviteAcceptedAt!: Date | null;
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -29,23 +35,33 @@ export default class SystemUser extends SystemModel {
|
|||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['fullName', 'isDeleted', 'isInviteAccepted'];
|
return ['fullName', 'isDeleted', 'isInviteAccepted', 'isVerified'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Detarmines whether the user is deleted.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
get isDeleted() {
|
get isDeleted() {
|
||||||
return !!this.deletedAt;
|
return !!this.deletedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Detarmines whether the sent invite is accepted.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
get isInviteAccepted() {
|
get isInviteAccepted() {
|
||||||
return !!this.inviteAcceptedAt;
|
return !!this.inviteAcceptedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the user's email is verified.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isVerified() {
|
||||||
|
return !!this.verified;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full name attribute.
|
* Full name attribute.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import uniqid from 'uniqid';
|
import uniqid from 'uniqid';
|
||||||
|
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||||
import BaseModel from 'models/Model';
|
import BaseModel from 'models/Model';
|
||||||
import TenantMetadata from './TenantMetadata';
|
import TenantMetadata from './TenantMetadata';
|
||||||
|
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||||
|
|
||||||
export default class Tenant extends BaseModel {
|
export default class Tenant extends BaseModel {
|
||||||
upgradeJobId: string;
|
upgradeJobId: string;
|
||||||
@@ -57,13 +59,33 @@ export default class Tenant extends BaseModel {
|
|||||||
return !!this.upgradeJobId;
|
return !!this.upgradeJobId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query modifiers.
|
||||||
|
*/
|
||||||
|
static modifiers() {
|
||||||
|
return {
|
||||||
|
subscriptions(builder) {
|
||||||
|
builder.withGraphFetched('subscriptions');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relations mappings.
|
* Relations mappings.
|
||||||
*/
|
*/
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
|
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
||||||
const TenantMetadata = require('./TenantMetadata');
|
const TenantMetadata = require('./TenantMetadata');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
subscriptions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: PlanSubscription.default,
|
||||||
|
join: {
|
||||||
|
from: 'tenants.id',
|
||||||
|
to: 'subscription_plan_subscriptions.tenantId',
|
||||||
|
},
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: TenantMetadata.default,
|
modelClass: TenantMetadata.default,
|
||||||
@@ -163,4 +185,48 @@ export default class Tenant extends BaseModel {
|
|||||||
saveMetadata(metadata) {
|
saveMetadata(metadata) {
|
||||||
return Tenant.saveMetadata(this.id, metadata);
|
return Tenant.saveMetadata(this.id, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} planId
|
||||||
|
* @param {*} invoiceInterval
|
||||||
|
* @param {*} invoicePeriod
|
||||||
|
* @param {*} subscriptionSlug
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public newSubscription(
|
||||||
|
planId,
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
subscriptionSlug
|
||||||
|
) {
|
||||||
|
return Tenant.newSubscription(
|
||||||
|
this.id,
|
||||||
|
planId,
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
subscriptionSlug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a new subscription for the associated tenant.
|
||||||
|
*/
|
||||||
|
static newSubscription(
|
||||||
|
tenantId: number,
|
||||||
|
planId: number,
|
||||||
|
invoiceInterval: 'month' | 'year',
|
||||||
|
invoicePeriod: number,
|
||||||
|
subscriptionSlug: string
|
||||||
|
) {
|
||||||
|
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
|
||||||
|
|
||||||
|
return PlanSubscription.query().insert({
|
||||||
|
tenantId,
|
||||||
|
slug: subscriptionSlug,
|
||||||
|
planId,
|
||||||
|
startsAt: period.getStartDate(),
|
||||||
|
endsAt: period.getEndDate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import Plan from './Subscriptions/Plan';
|
||||||
|
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||||
import Tenant from './Tenant';
|
import Tenant from './Tenant';
|
||||||
import TenantMetadata from './TenantMetadata';
|
import TenantMetadata from './TenantMetadata';
|
||||||
import SystemUser from './SystemUser';
|
import SystemUser from './SystemUser';
|
||||||
@@ -7,6 +9,8 @@ import SystemPlaidItem from './SystemPlaidItem';
|
|||||||
import { Import } from './Import';
|
import { Import } from './Import';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
Plan,
|
||||||
|
PlanSubscription,
|
||||||
Tenant,
|
Tenant,
|
||||||
TenantMetadata,
|
TenantMetadata,
|
||||||
SystemUser,
|
SystemUser,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import SystemRepository from '@/system/repositories/SystemRepository';
|
||||||
|
import { PlanSubscription } from '@/system/models';
|
||||||
|
|
||||||
|
export default class SubscriptionRepository extends SystemRepository {
|
||||||
|
/**
|
||||||
|
* Gets the repository's model.
|
||||||
|
*/
|
||||||
|
get model() {
|
||||||
|
return PlanSubscription.bindKnex(this.knex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve subscription from a given slug in specific tenant.
|
||||||
|
* @param {string} slug
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
getBySlugInTenant(slug: string, tenantId: number) {
|
||||||
|
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
|
||||||
|
|
||||||
|
return this.cache.get(cacheKey, () => {
|
||||||
|
return PlanSubscription.query()
|
||||||
|
.findOne('slug', slug)
|
||||||
|
.where('tenant_id', tenantId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
||||||
|
import SubscriptionRepository from '@/system/repositories/SubscriptionRepository';
|
||||||
import TenantRepository from '@/system/repositories/TenantRepository';
|
import TenantRepository from '@/system/repositories/TenantRepository';
|
||||||
|
|
||||||
export { SystemUserRepository, TenantRepository };
|
export {
|
||||||
|
SystemUserRepository,
|
||||||
|
SubscriptionRepository,
|
||||||
|
TenantRepository,
|
||||||
|
};
|
||||||
26
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
26
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
exports.seed = (knex) => {
|
||||||
|
// Deletes ALL existing entries
|
||||||
|
return knex('subscription_plans')
|
||||||
|
.del()
|
||||||
|
.then(() => {
|
||||||
|
// Inserts seed entries
|
||||||
|
return knex('subscription_plans').insert([
|
||||||
|
{
|
||||||
|
name: 'Free',
|
||||||
|
slug: 'free',
|
||||||
|
price: 0,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Early Adaptor',
|
||||||
|
slug: 'early-adaptor',
|
||||||
|
price: 29,
|
||||||
|
active: true,
|
||||||
|
currency: 'USD',
|
||||||
|
invoice_period: 12,
|
||||||
|
invoice_interval: 'month',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
exports.seed = (knex) => {
|
||||||
|
// Deletes ALL existing entries
|
||||||
|
return knex('subscription_plan_subscriptions')
|
||||||
|
.then(async () => {
|
||||||
|
const tenants = await knex('tenants');
|
||||||
|
|
||||||
|
for (const tenant of tenants) {
|
||||||
|
const existingSubscription = await knex('subscription_plan_subscriptions')
|
||||||
|
.where('tenantId', tenant.id)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!existingSubscription) {
|
||||||
|
const freePlan = await knex('subscription_plans').where('slug', 'free').first();
|
||||||
|
|
||||||
|
await knex('subscription_plan_subscriptions').insert({
|
||||||
|
tenantId: tenant.id,
|
||||||
|
planId: freePlan.id,
|
||||||
|
slug: 'main',
|
||||||
|
startsAt: knex.fn.now(),
|
||||||
|
endsAt: null,
|
||||||
|
createdAt: knex.fn.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _, { isEmpty } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ const booleanValuesRepresentingTrue: string[] = ['true', '1'];
|
|||||||
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
|
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
|
||||||
|
|
||||||
const normalizeValue = (value: any): string =>
|
const normalizeValue = (value: any): string =>
|
||||||
value.toString().trim().toLowerCase();
|
value?.toString().trim().toLowerCase();
|
||||||
|
|
||||||
const booleanValues: string[] = [
|
const booleanValues: string[] = [
|
||||||
...booleanValuesRepresentingTrue,
|
...booleanValuesRepresentingTrue,
|
||||||
@@ -338,7 +338,7 @@ const booleanValues: string[] = [
|
|||||||
|
|
||||||
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
|
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
|
||||||
const normalizedValue = normalizeValue(value);
|
const normalizedValue = normalizeValue(value);
|
||||||
if (booleanValues.indexOf(normalizedValue) === -1) {
|
if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;
|
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;
|
||||||
|
|||||||
1
packages/server/storage/.gitignore
vendored
1
packages/server/storage/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
*
|
*
|
||||||
!pdf/
|
!pdf/
|
||||||
|
!imports/
|
||||||
!.gitignore
|
!.gitignore
|
||||||
2
packages/server/storage/imports/.gitignore
vendored
Normal file
2
packages/server/storage/imports/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
424
packages/server/views/mail/SignupVerifyEmail.html
Normal file
424
packages/server/views/mail/SignupVerifyEmail.html
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Bigcapital | Reset your password</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
/*All the styling goes here*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #2d95fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #1968F0;
|
||||||
|
border-color: #1968F0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb4{
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #004dd0 !important;
|
||||||
|
}
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #004dd0 !important;
|
||||||
|
border-color: #004dd0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[data-icon="bigcapital"] path {
|
||||||
|
fill: #004dd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-icon='bigcapital'] .path-1,
|
||||||
|
[data-icon='bigcapital'] .path-13 {
|
||||||
|
fill: #2d95fd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<span class="preheader">Verify your email.</span>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<table role="presentation" class="main">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="align-center">
|
||||||
|
<img src="cid:bigcapital_logo" />
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p class="align-center">
|
||||||
|
<h2>Verify your email</h2>
|
||||||
|
</p>
|
||||||
|
<p class="mgb-1x">Hi <strong>{{ fullName }}<strong>,</p>
|
||||||
|
<p class="mgb-2-5x">To continue setting up your Bigcapital account, please verify that this is your email address.</p>
|
||||||
|
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{{ verifyUrl }}" target="_blank">Verify email address</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>If this was a mistake, just ignore this email and nothing will happen.</p>
|
||||||
|
<p class="email-note">This is an automatically generated email please do not reply to this email.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by <a href="https://Bigcapital.ly">Bigcapital.ly</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
packages/webapp/.gitignore
vendored
3
packages/webapp/.gitignore
vendored
@@ -20,4 +20,5 @@
|
|||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
|
||||||
|
dist
|
||||||
@@ -51,5 +51,6 @@
|
|||||||
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
/>
|
/>
|
||||||
|
<script src="https://app.lemonsqueezy.com/js/lemon.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,13 +9,24 @@ import 'moment/locale/ar-ly';
|
|||||||
import 'moment/locale/es-us';
|
import 'moment/locale/es-us';
|
||||||
|
|
||||||
import AppIntlLoader from './AppIntlLoader';
|
import AppIntlLoader from './AppIntlLoader';
|
||||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
|
||||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||||
import { Authentication } from '@/containers/Authentication/Authentication';
|
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||||
|
|
||||||
|
import LazyLoader from '@/components/LazyLoader';
|
||||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||||
import { queryConfig } from '../hooks/query/base';
|
import { queryConfig } from '../hooks/query/base';
|
||||||
|
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
|
||||||
|
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
|
||||||
|
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
|
||||||
|
|
||||||
|
const EmailConfirmation = LazyLoader({
|
||||||
|
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||||
|
});
|
||||||
|
const RegisterVerify = LazyLoader({
|
||||||
|
loader: () => import('@/containers/Authentication/RegisterVerify'),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App inner.
|
* App inner.
|
||||||
@@ -26,9 +37,30 @@ function AppInsider({ history }) {
|
|||||||
<DashboardThemeProvider>
|
<DashboardThemeProvider>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={'/auth'} component={Authentication} />
|
<Route path={'/auth/register/verify'}>
|
||||||
|
<EnsureAuthenticated>
|
||||||
|
<EnsureUserEmailNotVerified>
|
||||||
|
<RegisterVerify />
|
||||||
|
</EnsureUserEmailNotVerified>
|
||||||
|
</EnsureAuthenticated>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={'/auth/email_confirmation'}>
|
||||||
|
<EmailConfirmation />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={'/auth'}>
|
||||||
|
<EnsureAuthNotAuthenticated>
|
||||||
|
<Authentication />
|
||||||
|
</EnsureAuthNotAuthenticated>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path={'/'}>
|
<Route path={'/'}>
|
||||||
<PrivateRoute component={DashboardPrivatePages} />
|
<EnsureAuthenticated>
|
||||||
|
<EnsureUserEmailVerified>
|
||||||
|
<DashboardPrivatePages />
|
||||||
|
</EnsureUserEmailVerified>
|
||||||
|
</EnsureAuthenticated>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
useAuthenticatedAccount,
|
useAuthenticatedAccount,
|
||||||
useCurrentOrganization,
|
useCurrentOrganization,
|
||||||
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
|
|||||||
isBooted.current = true;
|
isBooted.current = true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// Reset the loading states once the hook unmount.
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
isAuthUserLoading && !isBooted.current && stopLoading();
|
||||||
|
isOrgLoading && !isBooted.current && stopLoading();
|
||||||
|
},
|
||||||
|
[isAuthUserLoading, isOrgLoading, stopLoading],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: isOrgLoading || isAuthUserLoading,
|
isLoading: isOrgLoading || isAuthUserLoading,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user