Compare commits
123 Commits
docker-com
...
split-comp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3adf4879 | ||
|
|
532ad61500 | ||
|
|
a44d779670 | ||
|
|
ab5f9f50d0 | ||
|
|
f3f10db6db | ||
|
|
de5694681b | ||
|
|
b1a997c287 | ||
|
|
3e36146bce | ||
|
|
db833888c8 | ||
|
|
c415e3d693 | ||
|
|
e145eabf02 | ||
|
|
caab21647d | ||
|
|
98528e9e5b | ||
|
|
b993fad37f | ||
|
|
94ea44b58e | ||
|
|
877a57043a | ||
|
|
70415d1d63 | ||
|
|
ef2d1977d6 | ||
|
|
46ea26891d | ||
|
|
f750cede89 | ||
|
|
e5d0f16096 | ||
|
|
706694c768 | ||
|
|
d1a09e3b15 | ||
|
|
01c27b56ef | ||
|
|
0d8fb8cf25 | ||
|
|
6562e3ab8c | ||
|
|
c93650ffd3 | ||
|
|
e6a2825065 | ||
|
|
0c2a0b0010 | ||
|
|
a332c51249 | ||
|
|
0b6d0bc016 | ||
|
|
e6336b1451 | ||
|
|
46290c4d37 | ||
|
|
ff2b7563c8 | ||
|
|
b9572420ed | ||
|
|
35ebb9c2aa | ||
|
|
3ebeb29dc0 | ||
|
|
8e98068538 | ||
|
|
6a72594faf | ||
|
|
728729094a | ||
|
|
93d540fbd2 | ||
|
|
eb9b6ce717 | ||
|
|
f716d42d26 | ||
|
|
1c4c364f06 | ||
|
|
162ad91547 | ||
|
|
2950e5ede4 | ||
|
|
73b041d8d2 | ||
|
|
7bf008a9cb | ||
|
|
4d9e3ccfb4 | ||
|
|
1bfe19f26c | ||
|
|
a371cedb67 | ||
|
|
4ed9c36ebd | ||
|
|
e24b23ce7e | ||
|
|
19fe6e2423 | ||
|
|
aec09f178b | ||
|
|
ffe51bae07 | ||
|
|
68231d5edb | ||
|
|
e1ea5c402c | ||
|
|
34b2c2c8b4 | ||
|
|
5d96fe6aa0 | ||
|
|
d2b5084b42 | ||
|
|
81fb0734d5 | ||
|
|
3639ce44e5 | ||
|
|
a7c00d60d5 | ||
|
|
932750b62d | ||
|
|
c90ffed67f | ||
|
|
e92c4486aa | ||
|
|
aaceea5338 | ||
|
|
4d54d180bc | ||
|
|
8fdd98e34d | ||
|
|
d53c5ee5e6 | ||
|
|
4082e4e2b8 | ||
|
|
0c689459cb | ||
|
|
40ef02f215 | ||
|
|
d369f0bb17 | ||
|
|
425d0293cc | ||
|
|
b621650975 | ||
|
|
40948160fe | ||
|
|
aa6b9dd295 | ||
|
|
05c2232b97 | ||
|
|
8f6325d529 | ||
|
|
0aa681043d | ||
|
|
40bddfdfeb | ||
|
|
d6e2f01d70 | ||
|
|
2344d3d34d | ||
|
|
883c5dcb41 | ||
|
|
be10b8934d | ||
|
|
ce38c71fa7 | ||
|
|
1162fbc7c3 | ||
|
|
18b9e25f2b | ||
|
|
dd26bdc482 | ||
|
|
ad3c9ebfe9 | ||
|
|
36611652da | ||
|
|
06c7ee71b4 | ||
|
|
54d3188666 | ||
|
|
3ceb9adda2 | ||
|
|
1249415054 | ||
|
|
4d44ce4c7f | ||
|
|
6c96c371c5 | ||
|
|
6c61a69f10 | ||
|
|
981b65349d | ||
|
|
a7d29a31c8 | ||
|
|
c1d92b74f0 | ||
|
|
6f0f47f38a | ||
|
|
83510cfa70 | ||
|
|
903dc0522a | ||
|
|
eecbcacb90 | ||
|
|
cfbe4cfea0 | ||
|
|
8f039b77e7 | ||
|
|
672a1bbb82 | ||
|
|
b2f3585047 | ||
|
|
e6434ea2d1 | ||
|
|
a21d6a37e4 | ||
|
|
e9fdffa9d9 | ||
|
|
6bd30abddb | ||
|
|
920c8ea95c | ||
|
|
8de3717587 | ||
|
|
cc863f774a | ||
|
|
bcd08284b4 | ||
|
|
8e8161f207 | ||
|
|
7b4b50cf4b | ||
|
|
bca3e51fdf | ||
|
|
6faa378577 |
41
.env.example
@@ -8,27 +8,42 @@ MAIL_FROM_NAME=
|
||||
MAIL_FROM_ADDRESS=
|
||||
|
||||
# Database
|
||||
DB_USER=
|
||||
DB_HOST=
|
||||
DB_PASSWORD=
|
||||
DB_CHARSET=
|
||||
DB_HOST=mysql
|
||||
DB_USER=bigcapital
|
||||
DB_PASSWORD=bigcapital
|
||||
DB_ROOT_PASSWORD=root
|
||||
DB_CHARSET=utf8
|
||||
|
||||
# System database
|
||||
SYSTEM_DB_NAME=bigcapital_system
|
||||
# SYSTEM_DB_USER=
|
||||
# SYSTEM_DB_PASSWORD=
|
||||
# SYSTEM_DB_NAME=
|
||||
# SYSTEM_DB_CHARSET=
|
||||
|
||||
# Tenants databases
|
||||
# Tenant databases
|
||||
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||
|
||||
# MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
# TENANT_DB_HOST=
|
||||
# TENANT_DB_USER=
|
||||
# TENANT_DB_PASSWORD=
|
||||
# TENANT_DB_CHARSET=
|
||||
|
||||
# Application
|
||||
BASE_URL=https://bigcapital.ly
|
||||
CONTACT_US_MAIL=support@bigcapital.ly
|
||||
BASE_URL=http://example.com
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
|
||||
# Jobs MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# App proxy
|
||||
PUBLIC_PROXY_PORT=80
|
||||
PUBLIC_PROXY_SSL_PORT=443
|
||||
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
|
||||
# Sign-up restrictions
|
||||
SIGNUP_DISABLED=false
|
||||
SIGNUP_ALLOWED_DOMAINS=
|
||||
SIGNUP_ALLOWED_EMAILS=
|
||||
|
||||
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
docker/nginx/scripts/build-nginx.sh text eol=lf
|
||||
docker/mariadb/docker-entrypoint.sh text eol=lf
|
||||
68
.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: E2E
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
- '**/tsconfig.json'
|
||||
- 'yarn.lock'
|
||||
- '.github/workflows/e2e.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
- '**/tsconfig.json'
|
||||
- 'yarn.lock'
|
||||
- '.github/workflows/e2e.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
test_setup:
|
||||
name: Test setup
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
preview_url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }}
|
||||
steps:
|
||||
- name: Wait for Vercel preview deployment to be ready
|
||||
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
|
||||
id: waitForVercelPreviewDeployment
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
max_timeout: 3000
|
||||
|
||||
test_e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_setup
|
||||
name: Playwright tests
|
||||
timeout-minutes: 15
|
||||
environment: ${{ vars.ENVIRONMENT_STAGE }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14 # Need for npm >=7.7
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install Playwright with deps
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
PLAYWRIGHT_TEST_BASE_URL: ${{ needs.test_setup.outputs.preview_url }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
data
|
||||
.env
|
||||
.env
|
||||
test-results/
|
||||
82
CHANGELOG.md
@@ -2,6 +2,88 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.9.8] - 19-06-2023
|
||||
|
||||
`bigcapitalhq/webapp`
|
||||
|
||||
* add: Inventory Adjustment option to the item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/158
|
||||
* fix: use all drawers names from common enum object by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/157
|
||||
* fix: adjustment type options do not show up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/159
|
||||
* fix: change the remove line text to be red to intent as a danger action by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/162
|
||||
* fix: rename sidebar localization keys names to be keyword path by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/161
|
||||
* fix: manual journal placeholder text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/160
|
||||
* fix: warehouses select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/168
|
||||
|
||||
`bigcapitalhq/server`
|
||||
|
||||
* fix: sending emails on reset password and registration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/167
|
||||
|
||||
## [0.9.7] - 14-06-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
* fix: change the footer links of onboarding pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/139
|
||||
|
||||
`@bigcapital/server`
|
||||
* fix: expense transaction journal entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/155
|
||||
|
||||
## [0.9.6] - 12-06-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
* fix: remove duplicated form submitting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/138
|
||||
* feat: add monorepo version on the application sidebar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/136
|
||||
|
||||
## [0.9.5] - 11-06-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
|
||||
* fix: filter ledger entries that effect contact balance to AR/AP accounts only by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/132
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
* fix: catch journal error when create a journal with accounts have different currency by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/135
|
||||
* fix: add duplicate icon to context menu of customers and vendors table by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/133
|
||||
* fix: customer/vendor opening balance with exchange rate by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/134
|
||||
|
||||
## [0.9.4] - 08-06-2023
|
||||
|
||||
`@bigcapital/monorepo`
|
||||
- fixed: docker-compose line-ending issue on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/130
|
||||
|
||||
`@bigcapital/server`
|
||||
- fixed: Disable Webpack minification for JS class name reading.
|
||||
|
||||
## [0.9.3] -04-06-2023
|
||||
|
||||
`@bigcapital/monorepo`
|
||||
* Added: Add env variable to customize the proxy public ports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/125
|
||||
* Added: Migrate the server database to MariaDB by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/128
|
||||
|
||||
## [0.9.2] - 31-05-2023
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
- fixed: move `packaeg-lock.json` inside docker container.
|
||||
- fixed: remove Sentry from the web client.
|
||||
|
||||
## [0.9.1] - 28-05-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
- fix: deleting ledger entries of manual journal.
|
||||
- fix: base currency should be enabled.
|
||||
- fix: delete invoice transaction issue.
|
||||
|
||||
`@bigcapital/webapp`
|
||||
- fix: general, accoutant and items preferences.
|
||||
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
|
||||
- refactor: the setup organization form to use binded Formik components.
|
||||
|
||||
## [0.9.0] - 06-05-2023
|
||||
|
||||
`@bigcapital/server`
|
||||
|
||||
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
|
||||
|
||||
## [0.8.3] - 06-04-2023
|
||||
|
||||
`@bigcaptial/monorepo`
|
||||
|
||||
132
CONTRIBUTING.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Thank you for considering contributing to our project! We appreciate your interest and welcome any contributions you may have.
|
||||
|
||||
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution.
|
||||
|
||||
## Sections
|
||||
|
||||
- [General Instructions](#general-instructions)
|
||||
- [Contribute to Backend](#contribute-to-backend)
|
||||
- [Contribute to Frontend](#contribute-to-frontend)
|
||||
- [Other Ways to Contribute](#other-ways-to-contribute)
|
||||
|
||||
## General Instructions
|
||||
|
||||
## For Pull Request(s)
|
||||
|
||||
Contributions via pull requests are much appreciated. Once the approach is agreed upon ✅, make your changes and open a Pull Request(s). Before sending us a pull request, please ensure that,
|
||||
|
||||
- Fork the repo on GitHub, clone it on your machine.
|
||||
- Create a branch with your changes.
|
||||
- You are working against the latest source on the `develop` branch.
|
||||
- Modify the source; please focus only on the specific change.
|
||||
- Ensure local tests pass.
|
||||
- Commit to your fork using clear commit messages.
|
||||
- Send us a pull request.
|
||||
- Pay attention to any automated CI failures reported in the pull request.
|
||||
- Stay involved in the conversation
|
||||
|
||||
⚠️ Please note: If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
|
||||
|
||||
---
|
||||
|
||||
## Contribute to Backend
|
||||
|
||||
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
|
||||
- Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit these command on root directory and it will install dependencies of all packages.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
- Run all required docker containers in the development, we already configured all containers under `docker-compose.yml`.
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Wait some seconds, and hit `docker-compose ps` and you should see the same result below.
|
||||
|
||||
```
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
d974edfab9df bigcapital-mysql "docker-entrypoint.s…" 7 seconds ago Up 1 second 0.0.0.0:3306->3306/tcp, 33060/tcp bigcapital-mysql-1
|
||||
cefa73fe2881 bigcapital-redis "docker-entrypoint.s…" 7 seconds ago Up 1 second 6379/tcp bigcapital-redis-1
|
||||
1ea059198cb4 bigcapital-mongo "docker-entrypoint.s…" 7 seconds ago Up 1 second 0.0.0.0:27017->27017/tcp bigcapital-mongo-1
|
||||
```
|
||||
|
||||
- There're some CLI commands we should run before running the server like databaase migration, so we need to build the `server` app first.
|
||||
|
||||
```
|
||||
npm run build:server
|
||||
```
|
||||
|
||||
- Run the database migration for system database.
|
||||
|
||||
```
|
||||
node packages/server/build/commands.js system:migrate:latest
|
||||
```
|
||||
|
||||
And you should get something like that.
|
||||
|
||||
```
|
||||
Batch 1 run: 6 migrations
|
||||
```
|
||||
|
||||
- Next, start the webapp application.
|
||||
|
||||
```
|
||||
npm run dev:server
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
----
|
||||
|
||||
## Contribute to Frontend
|
||||
|
||||
- Clone the `bigcapital` repository and cd into `bigcapital` directory.
|
||||
|
||||
```
|
||||
git clone https://github.com/bigcapital/bigcapital.git && cd bigcaptial
|
||||
```
|
||||
|
||||
- Install all npm dependencies of the monorepo, you don't have to change directory to the `frontend` package. just hit that command and will install all packages across all application.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
- Next, start the webapp application.
|
||||
|
||||
```
|
||||
npm run dev:webapp
|
||||
```
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
---
|
||||
|
||||
## Code Review
|
||||
|
||||
We welcome constructive criticism and feedback on code submitted by contributors. All feedback should be constructive and respectful, and should focus on the code rather than the contributor. Code review may include suggestions for improvement or changes to the code.
|
||||
|
||||
---
|
||||
|
||||
## Other Ways to Contribute
|
||||
|
||||
There are many other ways to get involved with the community and to participate in this project:
|
||||
|
||||
- Use the product, submitting GitHub issues when a problem is found.
|
||||
- Help code review pull requests and participate in issue threads.
|
||||
- Submit a new feature request as an issue.
|
||||
- Help answer questions on forums such as Bigcapital Community Discord Channel.
|
||||
- Tell others about the project on Twitter, your blog, etc.
|
||||
|
||||
**[`^top^`](#)**
|
||||
|
||||
Again, Feel free to ping us on [`#contributing`](https://discord.com/invite/c8nPBJafeb) on our Discord community if you need any help on this :)
|
||||
|
||||
Thank You!
|
||||
22
README.md
@@ -7,6 +7,24 @@
|
||||
<p align="center">
|
||||
Simple, smart online accounting software for small and medium businesses.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
|
||||
</a>
|
||||
<a href="https://discord.com/invite/c8nPBJafeb">
|
||||
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
|
||||
</a>
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/contributors/bigcapitalhq/bigcapital" alt="" />
|
||||
</a>
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/blob/develop/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/bigcapitalhq/bigcapital" alt="" />
|
||||
</a>
|
||||
<a href="https://twitter.com/bigcapitalhq">
|
||||
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
# What's Bigcapital?
|
||||
@@ -22,10 +40,10 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
|
||||
# Resources
|
||||
|
||||
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
|
||||
- [Contribution](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md) - Welcome to any contributions.
|
||||
- [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
|
||||
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
|
||||
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
|
||||
|
||||
# Changlog
|
||||
# Changelog
|
||||
|
||||
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.
|
||||
|
||||
@@ -15,14 +15,14 @@ services:
|
||||
- ./data/logs/nginx/:/var/log/nginx
|
||||
- ./docker/certbot/certs/:/var/certs
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "${PUBLIC_PROXY_PORT:-80}:80"
|
||||
- "${PUBLIC_PROXY_SSL_PORT:-443}:443"
|
||||
tty: true
|
||||
depends_on:
|
||||
- server
|
||||
- webapp
|
||||
|
||||
webapp:
|
||||
webapp:
|
||||
container_name: bigcapital-webapp
|
||||
image: ghcr.io/bigcapitalhq/webapp:latest
|
||||
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
environment:
|
||||
# Mail
|
||||
- MAIL_HOST=${MAIL_HOST}
|
||||
- MAIL_USERNAME=${MAIL_USERNAM}
|
||||
- MAIL_USERNAME=${MAIL_USERNAME}
|
||||
- MAIL_PASSWORD=${MAIL_PASSWORD}
|
||||
- MAIL_PORT=${MAIL_PORT}
|
||||
- MAIL_SECURE=${MAIL_SECURE}
|
||||
@@ -72,6 +72,11 @@ services:
|
||||
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
|
||||
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
|
||||
|
||||
# Sign-up restrictions
|
||||
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
|
||||
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
|
||||
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
|
||||
|
||||
database_migration:
|
||||
container_name: bigcapital-database-migration
|
||||
build:
|
||||
@@ -89,12 +94,12 @@ services:
|
||||
mysql:
|
||||
container_name: bigcapital-mysql
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
context: ./docker/mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
mariadb:
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
context: ./docker/mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mysql:5.7
|
||||
FROM mariadb:10.2
|
||||
|
||||
USER root
|
||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
||||
@@ -17,7 +17,7 @@ ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
|
||||
COPY ./init.sql /scripts/init.template.sql
|
||||
COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh
|
||||
|
||||
# The scripts in the docker-entrypoint-initdb.d/ directory are executed as
|
||||
# The scripts in the `docker-entrypoint-initdb.d/` directory are executed as
|
||||
# the mysql user inside the MySQL Docker container.
|
||||
RUN chown -R mysql:root /docker-entrypoint-initdb.d
|
||||
RUN chown -R mysql:root /scripts
|
||||
@@ -1,2 +1,3 @@
|
||||
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
68
e2e/authentication.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
let authPage: Page;
|
||||
|
||||
test.describe('authentication', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
authPage = await browser.newPage();
|
||||
});
|
||||
|
||||
test.describe('login', () => {
|
||||
test.beforeAll(async () => {
|
||||
await authPage.goto('/auth/login');
|
||||
});
|
||||
test('should show the login page.', async () => {
|
||||
await expect(authPage.locator('body')).toContainText(
|
||||
"Don't have an account? Sign up"
|
||||
);
|
||||
});
|
||||
test('should email and password be required.', async () => {
|
||||
await authPage.getByRole('button', { name: 'Log in' }).click();
|
||||
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Email is a required field'
|
||||
);
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Password is a required field'
|
||||
);
|
||||
});
|
||||
test('should go to the register page when click on sign up link', async () => {
|
||||
await authPage.getByRole('link', { name: 'Sign up' }).click();
|
||||
await expect(authPage.url()).toContain('/auth/register');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('register', () => {
|
||||
test.beforeAll(async () => {
|
||||
await authPage.goto('/auth/register');
|
||||
});
|
||||
test('should first name, last name, email and password be required.', async () => {
|
||||
await authPage.getByRole('button', { name: 'Register' }).click();
|
||||
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'First name is a required field'
|
||||
);
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Last name is a required field'
|
||||
);
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Email is a required field'
|
||||
);
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Password is a required field'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('reset password', () => {
|
||||
test.beforeAll(async () => {
|
||||
await authPage.goto('/auth/send_reset_password');
|
||||
});
|
||||
test('should email be required.', async () => {
|
||||
await authPage.getByRole('button', { name: 'Reset Password' }).click();
|
||||
await expect(authPage.locator('form')).toContainText(
|
||||
'Email is a required field'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.9.6",
|
||||
"npmClient": "npm"
|
||||
}
|
||||
|
||||
26
package-lock.json
generated
@@ -941,6 +941,23 @@
|
||||
"esquery": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.32.3",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz",
|
||||
"integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.32.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": {
|
||||
"version": "1.32.3",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz",
|
||||
"integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -986,8 +1003,7 @@
|
||||
"@types/node": {
|
||||
"version": "18.14.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz",
|
||||
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA=="
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.1",
|
||||
@@ -2304,6 +2320,12 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dev:server": "lerna run dev --scope \"@bigcapital/server\"",
|
||||
"build:server": "lerna run build --scope \"@bigcapital/server\"",
|
||||
"serve:server": "lerna run serve --scope \"@bigcapital/server\"",
|
||||
"test:e2e": "playwright test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"workspaces": [
|
||||
@@ -21,7 +22,8 @@
|
||||
"@commitlint/config-lerna-scopes": "^17.4.2",
|
||||
"husky": "^8.0.3",
|
||||
"lerna": "^6.4.1",
|
||||
"@commitlint/cli": "^17.4.2"
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@playwright/test": "^1.32.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.x"
|
||||
@@ -30,6 +32,5 @@
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
|
||||
BASE_URL= \
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash \
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
AGENDASH_AUTH_PASSWORD=123123 \
|
||||
# Sign-up restriction
|
||||
SIGNUP_DISABLED= \
|
||||
SIGNUP_ALLOWED_DOMAINS= \
|
||||
SIGNUP_ALLOWED_EMAILS=
|
||||
|
||||
ENV MAIL_HOST=$MAIL_HOST \
|
||||
MAIL_USERNAME=$MAIL_USERNAME \
|
||||
@@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \
|
||||
# MongoDB
|
||||
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
|
||||
# Application
|
||||
BASE_URL=$BASE_URL
|
||||
BASE_URL=$BASE_URL \
|
||||
# Sign-up restriction
|
||||
SIGNUP_DISABLED=$SIGNUP_DISABLED \
|
||||
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
|
||||
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
|
||||
|
||||
# Create app directory.
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bigcapital/server",
|
||||
"version": "1.7.1",
|
||||
"version": "0.9.5",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -65,6 +65,9 @@ exports.getCommonWebpackOptions = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
|
||||
@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
||||
import {
|
||||
AbilitySubject,
|
||||
AccountAction,
|
||||
IAccountDTO,
|
||||
IAccountsStructureType,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('structure')
|
||||
.optional()
|
||||
.isString()
|
||||
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -341,6 +351,7 @@ export default class AccountsController extends BaseController {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
structure: IAccountsStructureType.Tree,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController {
|
||||
asyncMiddleware(this.resetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ export default class AuthenticationController extends BaseController {
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.isLength({ min: 6 })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
@@ -105,7 +107,7 @@ export default class AuthenticationController extends BaseController {
|
||||
return [
|
||||
check('password')
|
||||
.exists()
|
||||
.isLength({ min: 5 })
|
||||
.isLength({ min: 6 })
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
@@ -207,6 +209,23 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
* @returns {Response|void}
|
||||
*/
|
||||
private async getAuthMeta(req: Request, res: Response, next: Function) {
|
||||
try {
|
||||
const meta = await this.authApplication.getAuthMeta();
|
||||
|
||||
return res.status(200).send({ meta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service errors.
|
||||
*/
|
||||
@@ -247,6 +266,30 @@ export default class AuthenticationController extends BaseController {
|
||||
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SIGNUP_RESTRICTED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SIGNUP_RESTRICTED',
|
||||
message:
|
||||
'Sign-up is restricted no one can sign-up to the system.',
|
||||
code: 700,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||
message:
|
||||
'Sign-up is restricted the given email address is not allowed to sign-up.',
|
||||
code: 710,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
|
||||
get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
query('accounting_method').optional().isIn(['cash', 'accrual']),
|
||||
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
@@ -67,6 +67,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
|
||||
@@ -92,6 +92,7 @@ export default class InviteUsersController extends BaseController {
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.sendInvite(tenantId, sendInviteDTO, user);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||
|
||||
@@ -177,7 +177,7 @@ export default class ItemsController extends BaseController {
|
||||
/**
|
||||
* Validate list query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
private get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
@@ -193,32 +193,20 @@ export default class ItemsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate autocomplete list query schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
|
||||
query('keyword').optional().isString().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given item details to the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemDTO: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedItem = await this.itemsApplication.createItem(tenantId, itemDTO);
|
||||
const storedItem = await this.itemsApplication.createItem(
|
||||
tenantId,
|
||||
itemDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedItem.id,
|
||||
@@ -234,7 +222,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
const item: IItemDTO = this.matchedBodyData(req);
|
||||
@@ -257,7 +245,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
@@ -279,7 +267,11 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async inactivateItem(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
@@ -300,7 +292,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
@@ -322,7 +314,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
private async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
@@ -342,7 +334,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
private async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class OrganizationController extends BaseController {
|
||||
private get organizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional().isString(),
|
||||
check('industry').optional({ nullable: true }).isString().trim().escape(),
|
||||
check('location').exists().isString().isISO31661Alpha2(),
|
||||
check('base_currency').exists().isISO4217(),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
|
||||
@@ -4,6 +4,7 @@ import moment from 'moment';
|
||||
global.__root_dir = path.join(__dirname, '..');
|
||||
global.__resources_dir = path.join(global.__root_dir, 'resources');
|
||||
global.__locales_dir = path.join(global.__resources_dir, 'locales');
|
||||
global.__views_dir = path.join(global.__root_dir, 'views');
|
||||
|
||||
moment.prototype.toMySqlDateTime = function () {
|
||||
return this.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -94,16 +95,15 @@ module.exports = {
|
||||
* JWT secret.
|
||||
*/
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
resetPasswordSeconds: 600,
|
||||
|
||||
/**
|
||||
*
|
||||
* Application base URL.
|
||||
*/
|
||||
customerSuccess: {
|
||||
email: 'success@bigcapital.ly',
|
||||
phoneNumber: '(218) 92 791 8381',
|
||||
},
|
||||
|
||||
baseURL: process.env.BASE_URL,
|
||||
|
||||
/**
|
||||
@@ -137,13 +137,16 @@ module.exports = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Users registeration configuration.
|
||||
* Sign-up restrictions
|
||||
*/
|
||||
registration: {
|
||||
countries: {
|
||||
whitelist: ['LY'],
|
||||
blacklist: [],
|
||||
},
|
||||
signupRestrictions: {
|
||||
disabled: parseBoolean<boolean>(process.env.SIGNUP_DISABLED, false),
|
||||
allowedDomains: castCommaListEnvVarToArray(
|
||||
process.env.SIGNUP_ALLOWED_DOMAINS
|
||||
),
|
||||
allowedEmails: castCommaListEnvVarToArray(
|
||||
process.env.SIGNUP_ALLOWED_EMAILS
|
||||
),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -153,8 +156,6 @@ module.exports = {
|
||||
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
|
||||
},
|
||||
|
||||
protocol: '',
|
||||
hostname: '',
|
||||
scheduleComputeItemCost: 'in 5 seconds',
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,17 +3,17 @@ import AccountsData from '../data/accounts';
|
||||
|
||||
export default class SeedAccounts extends TenantSeeder {
|
||||
/**
|
||||
* Seeds initial accounts to the organization.
|
||||
* Seeds initial accounts to the organization.
|
||||
*/
|
||||
up(knex) {
|
||||
const data = AccountsData.map((account) => {
|
||||
return {
|
||||
...account,
|
||||
name: this.i18n.__(account.name),
|
||||
description: this.i18n.__(account.description),
|
||||
currencyCode: this.tenant.metadata.baseCurrency,
|
||||
};
|
||||
});
|
||||
const data = AccountsData.map((account) => ({
|
||||
...account,
|
||||
name: this.i18n.__(account.name),
|
||||
description: this.i18n.__(account.description),
|
||||
currencyCode: this.tenant.metadata.baseCurrency,
|
||||
seededAt: new Date(),
|
||||
})
|
||||
);
|
||||
return knex('accounts').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('accounts').insert(data);
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class SeedSettings extends TenantSeeder {
|
||||
up() {
|
||||
const settings = [
|
||||
// Orgnization settings.
|
||||
{ group: 'organization', key: 'accounting_basis', value: 'accural' },
|
||||
{ group: 'organization', key: 'accounting_basis', value: 'accrual' },
|
||||
|
||||
// Accounts settings.
|
||||
{ group: 'accounts', key: 'account_code_unique', value: true },
|
||||
|
||||
@@ -42,6 +42,7 @@ export enum AccountNormal {
|
||||
|
||||
export interface IAccountsTransactionsFilter {
|
||||
accountId?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IAccountTransaction {
|
||||
@@ -78,9 +79,15 @@ export interface IAccountTransaction {
|
||||
}
|
||||
export interface IAccountResponse extends IAccount {}
|
||||
|
||||
export enum IAccountsStructureType {
|
||||
Tree = 'tree',
|
||||
Flat = 'flat',
|
||||
}
|
||||
|
||||
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string;
|
||||
onlyInactive: boolean;
|
||||
structure?: IAccountsStructureType;
|
||||
}
|
||||
|
||||
export interface IAccountType {
|
||||
|
||||
@@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword {
|
||||
export interface IAuthSendedResetPassword {
|
||||
user: ISystemUser,
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IAuthGetMetaPOJO {
|
||||
signupDisabled: boolean;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
|
||||
numberFormat: INumberFormatQuery;
|
||||
noneTransactions: boolean;
|
||||
noneZero: boolean;
|
||||
basis: 'cash' | 'accural';
|
||||
basis: 'cash' | 'accrual';
|
||||
accountIds: number[];
|
||||
|
||||
percentageOfColumn: boolean;
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface ILedger {
|
||||
|
||||
getEntries(): ILedgerEntry[];
|
||||
|
||||
filter(cb: (entry: ILedgerEntry) => boolean): ILedger;
|
||||
|
||||
whereAccountId(accountId: number): ILedger;
|
||||
whereContactId(contactId: number): ILedger;
|
||||
whereFromDate(fromDate: Date | string): ILedger;
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface ITrialBalanceSheetQuery {
|
||||
fromDate: Date | string;
|
||||
toDate: Date | string;
|
||||
numberFormat: INumberFormatQuery;
|
||||
basis: 'cash' | 'accural';
|
||||
basis: 'cash' | 'accrual';
|
||||
noneZero: boolean;
|
||||
noneTransactions: boolean;
|
||||
onlyActive: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnyObject } from '@casl/ability/dist/types/types';
|
||||
import { ITenant } from '@/interfaces';
|
||||
import { Model } from 'objection';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
export interface ISystemUser extends Model {
|
||||
id: number;
|
||||
@@ -54,20 +55,52 @@ export interface IUserInvite {
|
||||
|
||||
export interface IInviteUserService {
|
||||
acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void>;
|
||||
|
||||
/**
|
||||
* Re-send user invite.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} email -
|
||||
* @return {Promise<{ invite: IUserInvite }>}
|
||||
*/
|
||||
resendInvite(
|
||||
tenantId: number,
|
||||
userId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<{
|
||||
invite: IUserInvite;
|
||||
user: ITenantUser;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Sends invite mail to the given email from the given tenant and user.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} email -
|
||||
* @param {IUser} authorizedUser -
|
||||
* @return {Promise<IUserInvite>}
|
||||
*/
|
||||
sendInvite(
|
||||
tenantId: number,
|
||||
email: string,
|
||||
sendInviteDTO: IUserSendInviteDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<{
|
||||
invite: IUserInvite;
|
||||
invitedUser: ITenantUser;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IAcceptInviteUserService {
|
||||
/**
|
||||
* Accept the received invite.
|
||||
* @param {string} token
|
||||
* @param {IInviteUserInput} inviteUserInput
|
||||
* @throws {ServiceErrors}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
acceptInvite(token: string, inviteUserDTO: IInviteUserInput): Promise<void>;
|
||||
|
||||
/**
|
||||
* Validate the given invite token.
|
||||
* @param {string} token - the given token string.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
checkInvite(
|
||||
token: string
|
||||
): Promise<{ inviteToken: IUserInvite; orgName: object }>;
|
||||
@@ -121,7 +154,7 @@ export interface IUserInvitedEventPayload {
|
||||
tenantId: number;
|
||||
user: ITenantUser;
|
||||
}
|
||||
export interface IUserInviteTenantSyncedEventPayload{
|
||||
export interface IUserInviteTenantSyncedEventPayload {
|
||||
invite: IUserInvite;
|
||||
authorizedUser: ISystemUser;
|
||||
tenantId: number;
|
||||
@@ -143,10 +176,10 @@ export interface IAcceptInviteEventPayload {
|
||||
|
||||
export interface ICheckInviteEventPayload {
|
||||
inviteToken: IUserInvite;
|
||||
tenant: ITenant
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
export interface IUserSendInviteDTO {
|
||||
email: string;
|
||||
roleId: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
import { Container } from 'typedi';
|
||||
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
export default class ResetPasswordEmailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Agenda} agenda
|
||||
* @param {Agenda} agenda
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'reset-password-mail',
|
||||
{ priority: 'high' },
|
||||
this.handler.bind(this),
|
||||
this.handler.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send welcome mail job.
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { data } = job.attrs;
|
||||
const { user, token } = data;
|
||||
const Logger = Container.get('logger');
|
||||
const authService = Container.get(AuthenticationService);
|
||||
const authService = Container.get(AuthenticationMailMesssages);
|
||||
|
||||
Logger.info(`[send_reset_password] started.`, { data });
|
||||
|
||||
try {
|
||||
await authService.mailMessages.sendResetPasswordMessage(user, token);
|
||||
Logger.info(`[send_reset_password] finished.`, { data });
|
||||
done()
|
||||
await authService.sendResetPasswordMessage(user, token);
|
||||
done();
|
||||
} catch (error) {
|
||||
Logger.error(`[send_reset_password] error.`, { data, error });
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export default class SmsNotification {
|
||||
constructor(agenda) {
|
||||
agenda.define('sms-notification', { priority: 'high' }, this.handler);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Job}job
|
||||
*/
|
||||
async handler(job) {
|
||||
const { message, to } = job.attrs.data;
|
||||
const smsClient = Container.get('SMSClient');
|
||||
|
||||
try {
|
||||
await smsClient.sendMessage(to, message);
|
||||
} catch (error) {
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import InviteUserService from '@/services/InviteUsers/AcceptInviteUser';
|
||||
import SendInviteUsersMailMessage from '@/services/InviteUsers/SendInviteUsersMailMessage';
|
||||
|
||||
export default class UserInviteMailJob {
|
||||
/**
|
||||
@@ -21,24 +22,17 @@ export default class UserInviteMailJob {
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { invite, authorizedUser, tenantId } = job.attrs.data;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const inviteUsersService = Container.get(InviteUserService);
|
||||
|
||||
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
|
||||
const sendInviteMailMessage = Container.get(SendInviteUsersMailMessage);
|
||||
|
||||
try {
|
||||
await inviteUsersService.mailMessages.sendInviteMail(
|
||||
await sendInviteMailMessage.sendInviteMail(
|
||||
tenantId,
|
||||
authorizedUser,
|
||||
invite
|
||||
);
|
||||
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch (error) {
|
||||
Logger.info(
|
||||
`Send invite user mail - error: ${job.attrs.data}, error: ${error}`
|
||||
);
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
|
||||
export default class WelcomeSMSJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Agenda} agenda
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define('welcome-sms', { priority: 'high' }, this.handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send welcome mail job.
|
||||
* @param {Job} job
|
||||
* @param {Function} done
|
||||
*/
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { tenant, user } = job.attrs.data;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const authService = Container.get(AuthenticationService);
|
||||
|
||||
Logger.info(`[welcome_sms] started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await authService.smsMessages.sendWelcomeMessage(tenant, user);
|
||||
Logger.info(`[welcome_sms] finished`, { tenant, user });
|
||||
done();
|
||||
} catch (error) {
|
||||
Logger.info(`[welcome_sms] error`, { error, tenant, user });
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Container } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {Agenda} agenda -
|
||||
*/
|
||||
constructor(agenda) {
|
||||
// Welcome mail and SMS message.
|
||||
agenda.define(
|
||||
'welcome-email',
|
||||
{ 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 { organizationId, user } = job.attrs.data;
|
||||
const Logger: any = Container.get('logger');
|
||||
const authService = Container.get(AuthenticationService);
|
||||
|
||||
Logger.info(`[welcome_mail] started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await authService.mailMessages.sendWelcomeMessage(user, organizationId);
|
||||
Logger.info(`[welcome_mail] finished: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch (error) {
|
||||
Logger.error(`[welcome_mail] error: ${job.attrs.data}, error: ${error}`);
|
||||
done(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export default class Mail {
|
||||
* Retrieve view content from the view directory.
|
||||
*/
|
||||
private getViewContent(): string {
|
||||
const filePath = path.join(global.__root_dir, `../views/${this.view}`);
|
||||
const filePath = path.join(global.__views_dir, `/${this.view}`);
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber } from 'utils';
|
||||
import { isArrayLikeObject } from 'lodash/fp';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -39,12 +40,33 @@ export class Transformer {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object
|
||||
* @returns
|
||||
*/
|
||||
protected preCollectionTransform = (object: any) => {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object
|
||||
* @returns
|
||||
*/
|
||||
protected postCollectionTransform = (object: any) => {
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public work = (object: any) => {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(this.getTransformation);
|
||||
const preTransformed = this.preCollectionTransform(object);
|
||||
const transformed = preTransformed.map(this.getTransformation);
|
||||
|
||||
return this.postCollectionTransform(transformed);
|
||||
} else if (isObject(object)) {
|
||||
return this.getTransformation(object);
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ import SaleInvoiceAutoIncrementSubscriber from '@/subscribers/SaleInvoices/AutoI
|
||||
import SaleInvoiceConvertFromEstimateSubscriber from '@/subscribers/SaleInvoices/ConvertFromEstimate';
|
||||
import PaymentReceiveAutoSerialSubscriber from '@/subscribers/PaymentReceive/AutoSerialIncrement';
|
||||
import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite';
|
||||
import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotification';
|
||||
import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotificationSubscribe';
|
||||
import SyncTenantAcceptInvite from '@/services/InviteUsers/SyncTenantAcceptInvite';
|
||||
import SyncTenantUserMutate from '@/services/Users/SyncTenantUserSaved';
|
||||
import { SyncTenantUserDelete } from '@/services/Users/SyncTenantUserDeleted';
|
||||
import OrgSyncTenantAdminUserSubscriber from '@/subscribers/Organization/SyncTenantAdminUser';
|
||||
import OrgBuildSmsNotificationSubscriber from '@/subscribers/Organization/BuildSmsNotification';
|
||||
import PurgeUserAbilityCache from '@/services/Users/PurgeUserAbilityCache';
|
||||
import ResetLoginThrottleSubscriber from '@/subscribers/Authentication/ResetLoginThrottle';
|
||||
import AuthenticationSubscriber from '@/subscribers/Authentication/SendResetPasswordMail';
|
||||
import AuthSendWelcomeMailSubscriber from '@/subscribers/Authentication/SendWelcomeMail';
|
||||
import PurgeAuthorizedUserOnceRoleMutate from '@/services/Roles/PurgeAuthorizedUser';
|
||||
import SendSmsNotificationToCustomer from '@/subscribers/SaleInvoices/SendSmsNotificationToCustomer';
|
||||
import SendSmsNotificationSaleReceipt from '@/subscribers/SaleReceipt/SendSmsNotificationToCustomer';
|
||||
@@ -113,12 +113,12 @@ export const susbcribers = () => {
|
||||
SyncTenantAcceptInvite,
|
||||
InviteSendMainNotification,
|
||||
SyncTenantUserMutate,
|
||||
SyncTenantUserDelete,
|
||||
OrgSyncTenantAdminUserSubscriber,
|
||||
OrgBuildSmsNotificationSubscriber,
|
||||
PurgeUserAbilityCache,
|
||||
ResetLoginThrottleSubscriber,
|
||||
AuthenticationSubscriber,
|
||||
AuthSendWelcomeMailSubscriber,
|
||||
PurgeAuthorizedUserOnceRoleMutate,
|
||||
SendSmsNotificationToCustomer,
|
||||
SendSmsNotificationSaleReceipt,
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import Agenda from 'agenda';
|
||||
import WelcomeEmailJob from 'jobs/welcomeEmail';
|
||||
import WelcomeSMSJob from 'jobs/WelcomeSMS';
|
||||
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
||||
import ComputeItemCost from 'jobs/ComputeItemCost';
|
||||
import RewriteInvoicesJournalEntries from 'jobs/writeInvoicesJEntries';
|
||||
import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
|
||||
import UserInviteMailJob from 'jobs/UserInviteMail';
|
||||
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
||||
import SmsNotification from 'jobs/SmsNotification';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new WelcomeEmailJob(agenda);
|
||||
new ResetPasswordMailJob(agenda);
|
||||
new WelcomeSMSJob(agenda);
|
||||
new UserInviteMailJob(agenda);
|
||||
new ComputeItemCost(agenda);
|
||||
new RewriteInvoicesJournalEntries(agenda);
|
||||
new OrganizationSetupJob(agenda);
|
||||
new OrganizationUpgrade(agenda);
|
||||
new SmsNotification(agenda);
|
||||
|
||||
agenda.start();
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import async from 'async';
|
||||
import { Knex } from 'knex';
|
||||
import { ILedger, ISaleContactsBalanceQueuePayload } from '@/interfaces';
|
||||
import {
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
ISaleContactsBalanceQueuePayload,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TenantMetadata } from '@/system/models';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
|
||||
@Service()
|
||||
export class LedgerContactsBalanceStorage {
|
||||
@@ -49,6 +54,29 @@ export class LedgerContactsBalanceStorage {
|
||||
await this.saveContactBalance(tenantId, ledger, contactId, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters AP/AR ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<(entry: ILedgerEntry) => boolean>}
|
||||
*/
|
||||
private filterARAPLedgerEntris = async (
|
||||
tenantId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<(entry: ILedgerEntry) => boolean> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const ARAPAcounts = await Account.query(trx).whereIn('accountType', [
|
||||
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
|
||||
ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
|
||||
]);
|
||||
const ARAPAcountsIds = ARAPAcounts.map((a) => a.id);
|
||||
|
||||
return (entry: ILedgerEntry) => {
|
||||
return ARAPAcountsIds.indexOf(entry.accountId) !== -1;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
@@ -63,16 +91,24 @@ export class LedgerContactsBalanceStorage {
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
const contact = await Contact.query().findById(contactId);
|
||||
const contact = await Contact.query(trx).findById(contactId);
|
||||
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
|
||||
// Detarmines whether the contact has foreign currency.
|
||||
const isForeignContact = contact.currencyCode !== tenantMeta.baseCurrency;
|
||||
|
||||
// Filters the ledger base on the given contact id.
|
||||
const contactLedger = ledger.whereContactId(contactId);
|
||||
const filterARAPLedgerEntris = await this.filterARAPLedgerEntris(
|
||||
tenantId,
|
||||
trx
|
||||
);
|
||||
const contactLedger = ledger
|
||||
// Filter entries only that have contact id.
|
||||
.whereContactId(contactId)
|
||||
// Filter entries on AR/AP accounts.
|
||||
.filter(filterARAPLedgerEntris);
|
||||
|
||||
const closingBalance = isForeignContact
|
||||
? contactLedger
|
||||
|
||||
@@ -10,7 +10,7 @@ export class LedgerRevert {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
ledgerStorage: LedgerStorageService;
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
/**
|
||||
* Reverts the jouranl entries.
|
||||
|
||||
@@ -106,7 +106,7 @@ export default class AccountTransactionTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedFcCredit(transaction: IAccountTransaction) {
|
||||
return this.formatMoney(this.fcDebit(transaction), {
|
||||
return this.formatMoney(this.fcCredit(transaction), {
|
||||
currencyCode: transaction.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
@@ -117,7 +117,7 @@ export default class AccountTransactionTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedFcDebit(transaction: IAccountTransaction) {
|
||||
return this.formatMoney(this.fcCredit(transaction), {
|
||||
return this.formatMoney(this.fcDebit(transaction), {
|
||||
currencyCode: transaction.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { IAccount, IAccountsStructureType } from '@/interfaces';
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
import {
|
||||
assocDepthLevelToObjectTree,
|
||||
flatToNestedArray,
|
||||
formatNumber,
|
||||
nestedArrayToFlatten,
|
||||
} from 'utils';
|
||||
|
||||
export class AccountTransformer extends Transformer {
|
||||
/**
|
||||
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formattedAmount'];
|
||||
return ['formattedAmount', 'flattenName'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the flatten name with all dependants accounts names.
|
||||
* @param {IAccount} account -
|
||||
* @returns {string}
|
||||
*/
|
||||
public flattenName = (account: IAccount): string => {
|
||||
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
|
||||
account.id
|
||||
);
|
||||
const prefixAccounts = parentDependantsIds.map((dependId) => {
|
||||
const node = this.options.accountsGraph.getNodeData(dependId);
|
||||
return `${node.name}: `;
|
||||
});
|
||||
return `${prefixAccounts}${account.name}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -17,8 +38,28 @@ export class AccountTransformer extends Transformer {
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (account: IAccount): string => {
|
||||
return formatNumber(account.amount, {
|
||||
currencyCode: account.currencyCode,
|
||||
return formatNumber(account.amount, { currencyCode: account.currencyCode });
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the accounts collection to flat or nested array.
|
||||
* @param {IAccount[]}
|
||||
* @returns {IAccount[]}
|
||||
*/
|
||||
protected postCollectionTransform = (accounts: IAccount[]) => {
|
||||
// Transfom the flatten to accounts tree.
|
||||
const transformed = flatToNestedArray(accounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
// Associate `accountLevel` attr to indicate object depth.
|
||||
const transformed2 = assocDepthLevelToObjectTree(
|
||||
transformed,
|
||||
1,
|
||||
'accountLevel'
|
||||
);
|
||||
return this.options.structure === IAccountsStructureType.Flat
|
||||
? nestedArrayToFlatten(transformed2)
|
||||
: transformed2;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,15 +22,19 @@ export class GetAccount {
|
||||
*/
|
||||
public getAccount = async (tenantId: number, accountId: number) => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Find the given account or throw not found error.
|
||||
const account = await Account.query().findById(accountId).throwIfNotFound();
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Transformes the account model to POJO.
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
account,
|
||||
new AccountTransformer()
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph }
|
||||
);
|
||||
return this.i18nService.i18nApply(
|
||||
[['accountTypeLabel'], ['accountNormalFormatted']],
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces';
|
||||
import {
|
||||
IAccountsFilter,
|
||||
IAccountResponse,
|
||||
IFilterMeta,
|
||||
IAccountsStructureType,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
@@ -38,6 +43,7 @@ export class GetAccounts {
|
||||
filterDTO: IAccountsFilter
|
||||
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Parses the stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(filterDTO);
|
||||
@@ -53,17 +59,16 @@ export class GetAccounts {
|
||||
dynamicList.buildQuery()(builder);
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
// Retrievs the formatted accounts collection.
|
||||
const preTransformedAccounts = await this.transformer.transform(
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieves the transformed accounts collection.
|
||||
const transformedAccounts = await this.transformer.transform(
|
||||
tenantId,
|
||||
accounts,
|
||||
new AccountTransformer()
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph, structure: filterDTO.structure }
|
||||
);
|
||||
// Transform accounts to nested array.
|
||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ISystemUser,
|
||||
IPasswordReset,
|
||||
IAuthGetMetaPOJO,
|
||||
} from '@/interfaces';
|
||||
import { AuthSigninService } from './AuthSignin';
|
||||
import { AuthSignupService } from './AuthSignup';
|
||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||
import { GetAuthMeta } from './GetAuthMeta';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationApplication {
|
||||
@@ -15,6 +21,9 @@ export default class AuthenticationApplication {
|
||||
@Inject()
|
||||
private authResetPasswordService: AuthSendResetPassword;
|
||||
|
||||
@Inject()
|
||||
private authGetMeta: GetAuthMeta;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
@@ -53,4 +62,12 @@ export default class AuthenticationApplication {
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
return this.authResetPasswordService.resetPassword(token, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||
*/
|
||||
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||
return this.authGetMeta.getAuthMeta();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { omit } from 'lodash';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import TenantsManagerService from '../Tenancy/TenantsManager';
|
||||
import events from '@/subscribers/events';
|
||||
import { hashPassword } from '@/utils';
|
||||
import config from '@/config';
|
||||
|
||||
export class AuthSignupService {
|
||||
@Inject()
|
||||
@@ -33,6 +34,9 @@ export class AuthSignupService {
|
||||
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
// Validates the signup disable restrictions.
|
||||
await this.validateSignupRestrictions(signupDTO.email);
|
||||
|
||||
// Validates the given email uniqiness.
|
||||
await this.validateEmailUniqiness(signupDTO.email);
|
||||
|
||||
@@ -74,4 +78,34 @@ export class AuthSignupService {
|
||||
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sign-up disable restrictions.
|
||||
* @param {string} email
|
||||
*/
|
||||
private async validateSignupRestrictions(email: string) {
|
||||
// Can't continue if the signup is not disabled.
|
||||
if (!config.signupRestrictions.disabled) return;
|
||||
|
||||
// Validate the allowed email addresses and domains.
|
||||
if (
|
||||
!isEmpty(config.signupRestrictions.allowedEmails) ||
|
||||
!isEmpty(config.signupRestrictions.allowedDomains)
|
||||
) {
|
||||
const emailDomain = email.split('@').pop();
|
||||
const isAllowedEmail =
|
||||
config.signupRestrictions.allowedEmails.indexOf(email) !== -1;
|
||||
|
||||
const isAllowedDomain = config.signupRestrictions.allowedDomains.some(
|
||||
(domain) => emailDomain === domain
|
||||
);
|
||||
|
||||
if (!isAllowedEmail && !isAllowedDomain) {
|
||||
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED);
|
||||
}
|
||||
// Throw error if the signup is disabled with no exceptions.
|
||||
} else {
|
||||
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,59 +5,24 @@ import Mail from '@/lib/Mail';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationMailMesssages {
|
||||
/**
|
||||
* Sends welcome message.
|
||||
* @param {ISystemUser} user - The system user.
|
||||
* @param {string} organizationName -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async sendWelcomeMessage(
|
||||
user: ISystemUser,
|
||||
organizationId: string
|
||||
): Promise<void> {
|
||||
const root = __dirname + '/../../../views/images/bigcapital.png';
|
||||
|
||||
const mail = new Mail()
|
||||
.setView('mail/Welcome.html')
|
||||
.setSubject('Welcome to Bigcapital')
|
||||
.setTo(user.email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: root,
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
.setData({
|
||||
firstName: user.firstName,
|
||||
organizationId,
|
||||
successPhoneNumber: config.customerSuccess.phoneNumber,
|
||||
successEmail: config.customerSuccess.email,
|
||||
});
|
||||
|
||||
await mail.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends reset password message.
|
||||
* @param {ISystemUser} user - The system user.
|
||||
* @param {string} token - Reset password token.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async sendResetPasswordMessage(
|
||||
public async sendResetPasswordMessage(
|
||||
user: ISystemUser,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const root = __dirname + '/../../../views/images/bigcapital.png';
|
||||
|
||||
const mail = new Mail()
|
||||
await new Mail()
|
||||
.setSubject('Bigcapital - Password Reset')
|
||||
.setView('mail/ResetPassword.html')
|
||||
.setTo(user.email)
|
||||
.setAttachments([
|
||||
{
|
||||
filename: 'bigcapital.png',
|
||||
path: root,
|
||||
path: `${global.__views_dir}/images/bigcapital.png`,
|
||||
cid: 'bigcapital_logo',
|
||||
},
|
||||
])
|
||||
@@ -65,9 +30,7 @@ export default class AuthenticationMailMesssages {
|
||||
resetPasswordUrl: `${config.baseURL}/auth/reset_password/${token}`,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
contact_us_email: config.contactUsMail,
|
||||
});
|
||||
|
||||
await mail.send();
|
||||
})
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ISystemUser, ITenant } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationSMSMessages {
|
||||
@Inject('SMSClient')
|
||||
smsClient: any;
|
||||
|
||||
/**
|
||||
* Sends welcome sms message.
|
||||
* @param {ITenant} tenant
|
||||
* @param {ISystemUser} user
|
||||
*/
|
||||
sendWelcomeMessage(tenant: ITenant, user: ISystemUser) {
|
||||
const message: string = `Hi ${user.firstName}, Welcome to Bigcapital, You've joined the new workspace, if you need any help please don't hesitate to contact us.`;
|
||||
|
||||
return this.smsClient.sendMessage(user.phoneNumber, message);
|
||||
}
|
||||
}
|
||||
16
packages/server/src/services/Authentication/GetAuthMeta.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Service } from 'typedi';
|
||||
import { IAuthGetMetaPOJO } from '@/interfaces';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class GetAuthMeta {
|
||||
/**
|
||||
* Retrieves the authentication meta for SPA.
|
||||
* @returns {Promise<IAuthGetMetaPOJO>}
|
||||
*/
|
||||
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
|
||||
return {
|
||||
signupDisabled: config.signupRestrictions.disabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,6 @@ export const ERRORS = {
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
|
||||
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export class CreateCustomer {
|
||||
} as ICustomerEventCreatingPayload);
|
||||
|
||||
// Creates a new contact as customer.
|
||||
const customer = await Contact.query().insertAndFetch({
|
||||
const customer = await Contact.query(trx).insertAndFetch({
|
||||
...customerObj,
|
||||
});
|
||||
// Triggers `onCustomerCreated` event.
|
||||
|
||||
@@ -5,18 +5,13 @@ import {
|
||||
ICreditNoteDeletedPayload,
|
||||
ICreditNoteEditedPayload,
|
||||
ICreditNoteOpenedPayload,
|
||||
IRefundCreditNoteOpenedPayload,
|
||||
} from '@/interfaces';
|
||||
import CreditNoteGLEntries from './CreditNoteGLEntries';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class CreditNoteGLEntriesSubscriber {
|
||||
@Inject()
|
||||
creditNoteGLEntries: CreditNoteGLEntries;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private creditNoteGLEntries: CreditNoteGLEntries;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import {
|
||||
ICurrencyEditDTO,
|
||||
ICurrencyDTO,
|
||||
ICurrenciesService,
|
||||
ICurrency,
|
||||
} from '@/interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { CurrencyTransformer } from './CurrencyTransformer';
|
||||
|
||||
const ERRORS = {
|
||||
CURRENCY_NOT_FOUND: 'currency_not_found',
|
||||
@@ -23,14 +20,11 @@ const ERRORS = {
|
||||
|
||||
@Service()
|
||||
export default class CurrenciesService implements ICurrenciesService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
@Inject()
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve currency by given currency code or throw not found error.
|
||||
@@ -105,7 +99,7 @@ export default class CurrenciesService implements ICurrenciesService {
|
||||
*/
|
||||
public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) {
|
||||
const { Currency } = this.tenancy.models(tenantId);
|
||||
|
||||
|
||||
// Validate currency code uniquiness.
|
||||
await this.validateCurrencyCodeUniquiness(
|
||||
tenantId,
|
||||
@@ -141,13 +135,15 @@ export default class CurrenciesService implements ICurrenciesService {
|
||||
* @param {number} tenantId
|
||||
* @param {string} currencyCode
|
||||
*/
|
||||
validateCannotDeleteBaseCurrency(tenantId: number, currencyCode: string) {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
if (baseCurrency === currencyCode) {
|
||||
private async validateCannotDeleteBaseCurrency(
|
||||
tenantId: number,
|
||||
currencyCode: string
|
||||
) {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
if (tenant.metadata.baseCurrency === currencyCode) {
|
||||
throw new ServiceError(ERRORS.CANNOT_DELETE_BASE_CURRENCY);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +152,7 @@ export default class CurrenciesService implements ICurrenciesService {
|
||||
* Delete the given currency code.
|
||||
* @param {number} tenantId
|
||||
* @param {string} currencyCode
|
||||
* @return {Promise<}
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteCurrency(
|
||||
tenantId: number,
|
||||
@@ -180,19 +176,13 @@ export default class CurrenciesService implements ICurrenciesService {
|
||||
public async listCurrencies(tenantId: number): Promise<ICurrency[]> {
|
||||
const { Currency } = this.tenancy.models(tenantId);
|
||||
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
const currencies = await Currency.query().onBuild((query) => {
|
||||
query.orderBy('createdAt', 'ASC');
|
||||
});
|
||||
const formattedCurrencies = currencies.map((currency) => ({
|
||||
isBaseCurrency: baseCurrency === currency.currencyCode,
|
||||
...currency,
|
||||
}));
|
||||
return formattedCurrencies;
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
currencies,
|
||||
new CurrencyTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class CurrencyTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['isBaseCurrency'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the currency is base currency.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isBaseCurrency(currency): boolean {
|
||||
return this.context.organization.baseCurrency === currency.currencyCode;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import events from '@/subscribers/events';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { CommandExpenseValidator } from './CommandExpenseValidator';
|
||||
import { ExpenseCategory } from 'models';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
@@ -37,7 +36,7 @@ export class DeleteExpense {
|
||||
expenseId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<void> => {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
const { Expense, ExpenseCategory } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the expense transaction with associated entries or
|
||||
// throw not found error.
|
||||
@@ -60,7 +59,7 @@ export class DeleteExpense {
|
||||
} as IExpenseDeletingPayload);
|
||||
|
||||
// Deletes expense associated entries.
|
||||
await ExpenseCategory.query(trx).findById(expenseId).delete();
|
||||
await ExpenseCategory.query(trx).where('expenseId', expenseId).delete();
|
||||
|
||||
// Deletes expense transactions.
|
||||
await Expense.query(trx).findById(expenseId).delete();
|
||||
|
||||
@@ -46,7 +46,7 @@ export class ExpenseGLEntries {
|
||||
...commonEntry,
|
||||
credit: expense.localAmount,
|
||||
accountId: expense.paymentAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsRepo {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
basis: 'accural',
|
||||
basis: 'accrual',
|
||||
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
basis: 'accural',
|
||||
basis: 'accrual',
|
||||
noneZero: false,
|
||||
noneTransactions: true,
|
||||
onlyActive: false,
|
||||
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
} from '@/interfaces';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { IAcceptInviteUserService } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class AcceptInviteUserService {
|
||||
export default class AcceptInviteUserService
|
||||
implements IAcceptInviteUserService
|
||||
{
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
IUserInvitedEventPayload,
|
||||
IUserInviteTenantSyncedEventPayload,
|
||||
} from '@/interfaces';
|
||||
import { IUserInviteTenantSyncedEventPayload } from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export default class InviteSendMainNotificationSubscribe {
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from 'path';
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Mail from '@/lib/Mail';
|
||||
import { Service, Container } from 'typedi';
|
||||
import config from '@/config';
|
||||
import { Service } from 'typedi';
|
||||
import { Tenant } from '@/system/models';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersMailMessages {
|
||||
export default class SendInviteUsersMailMessage {
|
||||
/**
|
||||
* Sends invite mail to the given email.
|
||||
* @param user
|
||||
@@ -18,7 +18,7 @@ export default class InviteUsersMailMessages {
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const root = __dirname + '/../../../views/images/bigcapital.png';
|
||||
const root = path.join(global.__views_dir, '/images/bigcapital.png');
|
||||
|
||||
const mail = new Mail()
|
||||
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)
|
||||
@@ -8,7 +8,7 @@ import { IAcceptInviteEventPayload } from '@/interfaces';
|
||||
@Service()
|
||||
export default class SyncTenantAcceptInvite {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
|
||||
@@ -3,7 +3,6 @@ import uniqid from 'uniqid';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
ISystemUser,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
IUserInvitedEventPayload,
|
||||
IUserInviteResendEventPayload,
|
||||
} from '@/interfaces';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import RolesService from '@/services/Roles/RolesService';
|
||||
@@ -21,25 +19,13 @@ import RolesService from '@/services/Roles/RolesService';
|
||||
@Service()
|
||||
export default class InviteTenantUserService implements IInviteUserService {
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
|
||||
@Inject()
|
||||
rolesService: RolesService;
|
||||
private rolesService: RolesService;
|
||||
|
||||
/**
|
||||
* Sends invite mail to the given email from the given tenant and user.
|
||||
@@ -88,19 +74,15 @@ export default class InviteTenantUserService implements IInviteUserService {
|
||||
|
||||
/**
|
||||
* Re-send user invite.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} email -
|
||||
* @param {number} tenantId -
|
||||
* @param {string} email -
|
||||
* @return {Promise<{ invite: IUserInvite }>}
|
||||
*/
|
||||
public async resendInvite(
|
||||
tenantId: number,
|
||||
userId: number,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<{
|
||||
user: ITenantUser;
|
||||
}> {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
): Promise<{ user: ITenantUser }> {
|
||||
// Retrieve the user by id or throw not found service error.
|
||||
const user = await this.getUserByIdOrThrowError(tenantId, userId);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { difference, sumBy, omit, map } from 'lodash';
|
||||
import { difference } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IManualJournalDTO,
|
||||
IManualJournalEntry,
|
||||
IManualJournal,
|
||||
IManualJournalEntryDTO,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
@@ -286,7 +285,7 @@ export class CommandManualJournalValidators {
|
||||
public validateJournalCurrencyWithAccountsCurrency = async (
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
baseCurrency: string,
|
||||
baseCurrency: string
|
||||
) => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
|
||||
@@ -3,25 +3,20 @@ import * as R from 'ramda';
|
||||
import {
|
||||
IManualJournal,
|
||||
IManualJournalEntry,
|
||||
IAccount,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { LedgerRevert } from '@/services/Accounting/LedgerStorageRevert';
|
||||
|
||||
@Service()
|
||||
export class ManualJournalGLEntries {
|
||||
@Inject()
|
||||
ledgerStorage: LedgerStorageService;
|
||||
private ledgerStorage: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
ledgerRevert: LedgerRevert;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Create manual journal GL entries.
|
||||
@@ -77,7 +72,7 @@ export class ManualJournalGLEntries {
|
||||
manualJournalId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> => {
|
||||
return this.ledgerRevert.revertGLEntries(
|
||||
return this.ledgerStorage.deleteByReference(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
'Journal',
|
||||
@@ -86,7 +81,7 @@ export class ManualJournalGLEntries {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the ledger of the given manual journal.
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
@@ -97,11 +92,13 @@ export class ManualJournalGLEntries {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the common entry details of the manual journal
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {}
|
||||
* @returns {Partial<ILedgerEntry>}
|
||||
*/
|
||||
private getManualJournalCommonEntry = (manualJournal: IManualJournal) => {
|
||||
private getManualJournalCommonEntry = (
|
||||
manualJournal: IManualJournal
|
||||
): Partial<ILedgerEntry> => {
|
||||
return {
|
||||
transactionNumber: manualJournal.journalNumber,
|
||||
referenceNumber: manualJournal.reference,
|
||||
@@ -118,7 +115,8 @@ export class ManualJournalGLEntries {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the ledger entry of the given manual journal and
|
||||
* its associated entry.
|
||||
* @param {IManualJournal} manualJournal -
|
||||
* @param {IManualJournalEntry} entry -
|
||||
* @returns {ILedgerEntry}
|
||||
@@ -149,7 +147,7 @@ export class ManualJournalGLEntries {
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the ledger of the given manual journal.
|
||||
* @param {IManualJournal} manualJournal
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
|
||||
@@ -23,8 +23,11 @@ export class ProjectBillableBillSubscriber {
|
||||
events.saleInvoice.onCreated,
|
||||
this.handleIncreaseBillableBill
|
||||
);
|
||||
bus.subscribe(events.saleInvoice.onEdited, this.handleDecreaseBillableBill);
|
||||
bus.subscribe(events.saleInvoice.onDeleted, this.handleEditBillableBill);
|
||||
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableBill);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleDecreaseBillableBill
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import async from 'async';
|
||||
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
ISaleInvoiceDTO,
|
||||
ProjectLinkRefType,
|
||||
} from '@/interfaces';
|
||||
import { ProjectBillableExpense } from './ProjectBillableExpense';
|
||||
import { filterEntriesByRefType } from './_utils';
|
||||
|
||||
|
||||
@@ -21,13 +21,10 @@ export class ProjectBillableExpensesSubscriber {
|
||||
events.saleInvoice.onCreated,
|
||||
this.handleIncreaseBillableExpenses
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEdited,
|
||||
this.handleDecreaseBillableExpenses
|
||||
);
|
||||
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableExpenses);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleEditBillableExpenses
|
||||
this.handleDecreaseBillableExpenses
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,18 +10,18 @@ export class RoleTransformer extends Transformer {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the localized role name if is predefined or stored name.
|
||||
* @param role
|
||||
* @returns
|
||||
* @returns {string}
|
||||
*/
|
||||
public name(role) {
|
||||
return role.predefined ? this.context.i18n.__(role.name) : role.name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the localized role description if is predefined or stored description.
|
||||
* @param role
|
||||
* @returns
|
||||
* @returns {string}
|
||||
*/
|
||||
public description(role) {
|
||||
return role.predefined
|
||||
|
||||
26
packages/server/src/services/Users/SyncTenantUserDeleted.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import events from '@/subscribers/events';
|
||||
import { ITenantUserDeletedPayload } from '@/interfaces';
|
||||
import { SystemUser } from '@/system/models';
|
||||
|
||||
export class SyncTenantUserDelete {
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.tenantUser.onDeleted,
|
||||
this.syncSystemUserOnceUserDeleted
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the system user once tenant user be deleted.
|
||||
* @param {ITenantUserDeletedPayload} payload -
|
||||
*/
|
||||
private syncSystemUserOnceUserDeleted = async ({
|
||||
tenantUser,
|
||||
}: ITenantUserDeletedPayload) => {
|
||||
await SystemUser.query().where('id', tenantUser.systemUserId).delete();
|
||||
};
|
||||
}
|
||||
50
packages/server/src/services/Users/UserTransformer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class UserTransformer extends Transformer {
|
||||
/**
|
||||
* Exclude these attributes from user object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['role'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['roleName', 'roleDescription', 'roleSlug'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the localized role name if is predefined or stored name.
|
||||
* @param role
|
||||
* @returns {string}
|
||||
*/
|
||||
public roleName(user) {
|
||||
return user.role.predefined
|
||||
? this.context.i18n.__(user.role.name)
|
||||
: user.role.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the localized role description if is predefined or stored description.
|
||||
* @param user
|
||||
* @returns {string}
|
||||
*/
|
||||
public roleDescription(user) {
|
||||
return user.role.predefined
|
||||
? this.context.i18n.__(user.role.description)
|
||||
: user.role.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the role slug.
|
||||
* @param user
|
||||
* @returns {string}
|
||||
*/
|
||||
public roleSlug(user) {
|
||||
return user.role.slug;
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,11 @@ import RolesService from '@/services/Roles/RolesService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { UserTransformer } from './UserTransformer';
|
||||
|
||||
@Service()
|
||||
export default class UsersService {
|
||||
@Inject('repositories')
|
||||
private repositories: any;
|
||||
|
||||
@Inject()
|
||||
private rolesService: RolesService;
|
||||
|
||||
@@ -29,6 +28,9 @@ export default class UsersService {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
@@ -91,9 +93,10 @@ export default class UsersService {
|
||||
// Retrieve user details or throw not found service error.
|
||||
const tenantUser = await this.getTenantUserOrThrowError(tenantId, userId);
|
||||
|
||||
// Validate the delete user should not be the last user.
|
||||
await this.validateNotLastUserDelete(tenantId);
|
||||
|
||||
// Validate the delete user should not be the last active user.
|
||||
if (tenantUser.isInviteAccepted) {
|
||||
await this.validateNotLastUserDelete(tenantId);
|
||||
}
|
||||
// Delete user from the storage.
|
||||
await User.query().findById(userId).delete();
|
||||
|
||||
@@ -183,7 +186,7 @@ export default class UsersService {
|
||||
|
||||
const users = await User.query().withGraphFetched('role');
|
||||
|
||||
return users;
|
||||
return this.transformer.transform(tenantId, users, new UserTransformer());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,11 +226,13 @@ export default class UsersService {
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
private async validateNotLastUserDelete(tenantId: number) {
|
||||
const { systemUserRepository } = this.repositories;
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
const usersFound = await systemUserRepository.find({ tenantId });
|
||||
const inviteAcceptedUsers = await User.query()
|
||||
.select(['id'])
|
||||
.whereNotNull('invite_accepted_at');
|
||||
|
||||
if (usersFound.length === 1) {
|
||||
if (inviteAcceptedUsers.length === 1) {
|
||||
throw new ServiceError(ERRORS.CANNOT_DELETE_LAST_USER);
|
||||
}
|
||||
}
|
||||
@@ -291,9 +296,9 @@ export default class UsersService {
|
||||
|
||||
/**
|
||||
* Validate the authorized user cannot mutate its role.
|
||||
* @param {ITenantUser} oldTenantUser
|
||||
* @param {IEditUserDTO} editUserDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
* @param {ITenantUser} oldTenantUser
|
||||
* @param {IEditUserDTO} editUserDTO
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
validateMutateRoleNotAuthorizedUser(
|
||||
oldTenantUser: ITenantUser,
|
||||
@@ -307,5 +312,4 @@ export default class UsersService {
|
||||
throw new ServiceError(ERRORS.CANNOT_AUTHORIZED_USER_MUTATE_ROLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import events from '@/subscribers/events';
|
||||
@Service()
|
||||
export default class AuthenticationSubscriber {
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class AuthSendWelcomeMailSubscriber {
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.auth.signUp, this.sendWelcomeEmailOnceUserRegister);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends welcome email once the user register.
|
||||
*/
|
||||
private sendWelcomeEmailOnceUserRegister = async (payload) => {
|
||||
const { tenant, user } = payload;
|
||||
|
||||
// Send welcome mail to the user.
|
||||
await this.agenda.now('welcome-email', {
|
||||
organizationId: tenant.organizationId,
|
||||
user,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -419,6 +419,58 @@ export const parseDate = (date: string) => {
|
||||
return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : '';
|
||||
};
|
||||
|
||||
const nestedArrayToFlatten = (
|
||||
collection,
|
||||
property = 'children',
|
||||
parseItem = (a, level) => a,
|
||||
level = 1
|
||||
) => {
|
||||
const parseObject = (obj) =>
|
||||
parseItem(
|
||||
{
|
||||
..._.omit(obj, [property]),
|
||||
},
|
||||
level
|
||||
);
|
||||
|
||||
return collection.reduce((items, currentValue, index) => {
|
||||
let localItems = [...items];
|
||||
const parsedItem = parseObject(currentValue, level);
|
||||
localItems.push(parsedItem);
|
||||
|
||||
if (Array.isArray(currentValue[property])) {
|
||||
const flattenArray = nestedArrayToFlatten(
|
||||
currentValue[property],
|
||||
property,
|
||||
parseItem,
|
||||
level + 1
|
||||
);
|
||||
localItems = _.concat(localItems, flattenArray);
|
||||
}
|
||||
return localItems;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const assocDepthLevelToObjectTree = (
|
||||
objects,
|
||||
level = 1,
|
||||
propertyName = 'level'
|
||||
) => {
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
const object = objects[i];
|
||||
object[propertyName] = level;
|
||||
|
||||
if (object.children) {
|
||||
assocDepthLevelToObjectTree(object.children, level + 1, propertyName);
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
|
||||
return envVar ? envVar?.split(',')?.map(_.trim) : [];
|
||||
};
|
||||
|
||||
export {
|
||||
templateRender,
|
||||
accumSum,
|
||||
@@ -449,4 +501,7 @@ export {
|
||||
dateRangeFromToCollection,
|
||||
transformToMapKeyValue,
|
||||
mergeObjectsBykey,
|
||||
nestedArrayToFlatten,
|
||||
assocDepthLevelToObjectTree,
|
||||
castCommaListEnvVarToArray
|
||||
};
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
<!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: #2d95fd;
|
||||
border-color: #2d95fd;
|
||||
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">This is preheader text. Some clients will show this text as a preview.</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">
|
||||
<svg data-icon="bigcapital" class="bigcapital" width="190" height="37" viewBox="0 0 309.09 42.89"><desc>bigcapital</desc><path d="M56,3.16,61.33,8.5,31.94,37.9l-5.35-5.35Z" class="path-1" fill-rule="evenodd"></path><path d="M29.53,6.94l5.35,5.34L5.49,41.67.14,36.33l15.8-15.8Z" class="path-2" fill-rule="evenodd"></path><path d="M94.36,38.87H79.62v-31H94c6.33,0,10.22,3.15,10.22,8V16a7.22,7.22,0,0,1-4.07,6.69c3.58,1.37,5.8,3.45,5.8,7.61v.09C106,36,101.35,38.87,94.36,38.87Zm3.1-21.81c0-2-1.59-3.19-4.47-3.19H86.26v6.55h6.29c3,0,4.91-1,4.91-3.28Zm1.72,12.39c0-2.08-1.54-3.37-5-3.37H86.26V32.9h8.1c3,0,4.82-1.06,4.82-3.36Z" class="path-3" fill-rule="evenodd"></path><path d="M110.56,12.54v-6h7.08v6Zm.18,26.33V15.15h6.72V38.87Z" class="path-4" fill-rule="evenodd"></path><path d="M134,46a22.55,22.55,0,0,1-10.49-2.47l2.3-5a15.52,15.52,0,0,0,8,2.17c4.61,0,6.78-2.21,6.78-6.46V33.08c-2,2.39-4.16,3.85-7.75,3.85-5.53,0-10.53-4-10.53-11.07v-.09c0-7.08,5.09-11.06,10.53-11.06a9.63,9.63,0,0,1,7.66,3.54v-3.1h6.72V33.52C147.2,42.46,142.78,46,134,46Zm6.6-20.27a5.79,5.79,0,0,0-11.56,0v.09a5.42,5.42,0,0,0,5.76,5.49,5.49,5.49,0,0,0,5.8-5.49Z" class="path-5" fill-rule="evenodd"></path><path d="M164,39.41a12.11,12.11,0,0,1-12.35-12.26v-.09a12.18,12.18,0,0,1,12.44-12.35c4.47,0,7.25,1.5,9.47,4l-4.12,4.43a6.93,6.93,0,0,0-5.4-2.61c-3.36,0-5.75,3-5.75,6.46v.09c0,3.63,2.34,6.55,6,6.55,2.26,0,3.8-1,5.44-2.53l3.94,4A12,12,0,0,1,164,39.41Z" class="path-6" fill-rule="evenodd"></path><path d="M191.51,38.87V36.31a9.15,9.15,0,0,1-7.17,3c-4.47,0-8.15-2.57-8.15-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.72-4.34-5.09-4.34a17.57,17.57,0,0,0-6.55,1.28l-1.68-5.13a21,21,0,0,1,9.21-1.9c7.34,0,10.57,3.8,10.57,10.22V38.87Zm.13-9.55a10.3,10.3,0,0,0-4.29-.89c-2.88,0-4.65,1.15-4.65,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.15,0,5.27-1.73,5.27-4.16Z" class="path-7" fill-rule="evenodd"></path><path d="M217.49,39.32a9.1,9.1,0,0,1-7.39-3.54V46h-6.73V15.15h6.73v3.41a8.7,8.7,0,0,1,7.39-3.85c5.53,0,10.8,4.34,10.8,12.26v.09C228.29,35,223.11,39.32,217.49,39.32ZM221.56,27c0-3.94-2.66-6.55-5.8-6.55S210,23,210,27v.09c0,3.94,2.61,6.55,5.75,6.55s5.8-2.57,5.8-6.55Z" class="path-8" fill-rule="evenodd"></path><path d="M232.93,12.54v-6H240v6Zm.18,26.33V15.15h6.73V38.87Z" class="path-9" fill-rule="evenodd"></path><path d="M253.73,39.27c-4.11,0-6.9-1.63-6.9-7.12V20.91H244V15.15h2.83V9.09h6.73v6.06h5.57v5.76h-5.57V31c0,1.55.66,2.3,2.16,2.3A6.84,6.84,0,0,0,259,32.5v5.4A9.9,9.9,0,0,1,253.73,39.27Z" class="path-10" fill-rule="evenodd"></path><path d="M277.55,38.87V36.31a9.15,9.15,0,0,1-7.18,3c-4.46,0-8.14-2.57-8.14-7.26V32c0-5.18,3.94-7.57,9.56-7.57a16.74,16.74,0,0,1,5.8,1V25c0-2.79-1.73-4.34-5.09-4.34A17.57,17.57,0,0,0,266,21.92l-1.68-5.13a20.94,20.94,0,0,1,9.2-1.9c7.35,0,10.58,3.8,10.58,10.22V38.87Zm.13-9.55a10.31,10.31,0,0,0-4.3-.89c-2.87,0-4.64,1.15-4.64,3.27v.09c0,1.82,1.5,2.88,3.67,2.88,3.14,0,5.27-1.73,5.27-4.16Z" class="path-11" fill-rule="evenodd"></path><path d="M289.72,38.87V6.57h6.72v32.3Z" class="path-12" fill-rule="evenodd"></path><path d="M302.06,38.87V31.79h7.17v7.08Z" class="path-13" fill-rule="evenodd"></path></svg>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="align-center">
|
||||
<h3>License Code</h3>
|
||||
</p>
|
||||
<p class="mgb-1x">
|
||||
<h1>{{ licenseCode }}</h1>
|
||||
</p>
|
||||
<p class="email-note">
|
||||
This is an automatically generated email please do not reply to
|
||||
this email. If you face any issues, please contact us at <a href="mailto:{{ successEmail }}">{{ successEmail }}</a> or call <u>{{ successPhoneNumber }}</u></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="http://htmlemail.io">Bigcapital.com</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -391,10 +391,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>If you did not make this request, please contact us or ignore this message.</p>
|
||||
<p class="email-note">
|
||||
This is an automatically generated email please do not reply to
|
||||
this email. If you face any issues, please contact us at {{ contact_us_email }}</p>
|
||||
<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>
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
<!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
|
||||
------------------------------------- */
|
||||
|
||||
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: #2d95fd;
|
||||
border-color: #2d95fd;
|
||||
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">This is preheader text. Some clients will show this text as a preview.</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">
|
||||
<h3>Hi {{ firstName }}, Welcome to Bigcapital, </h3>
|
||||
</p>
|
||||
|
||||
<p class="mgb-1x">
|
||||
Your organization Id: <strong>{{ organizationId }}</strong>
|
||||
</p>
|
||||
<p class="mgb-1x">We are available to help you get started and answer any questions you may have. You can also email <a href="mailto:{{ successEmail }}">{{ successEmail }}</a> or call <u>{{ successPhoneNumber }}</u> about your set-up questions.</p>
|
||||
|
||||
<p class="mgb-2-5x">Thank you for trusting Bigcapital Software for your business needs. We look forward to serving you!</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">
|
||||
Made by Bigcapital Technologies, Inc
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,10 +5,10 @@ USER root
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json ./
|
||||
COPY package*.json ./
|
||||
COPY lerna.json ./
|
||||
|
||||
COPY ./packages/webapp/package.json /app/packages/webapp/package.json
|
||||
COPY ./packages/webapp/package*.json /app/packages/webapp/
|
||||
|
||||
RUN npm install
|
||||
RUN npm run bootstrap
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const dotenv = require('dotenv-webpack');
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
plugins: [
|
||||
new dotenv(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
MONOREPO_VERSION: JSON.stringify(require('../../lerna.json').version),
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
|
||||
164
packages/webapp/package-lock.json
generated
@@ -1205,9 +1205,9 @@
|
||||
}
|
||||
},
|
||||
"@blueprintjs-formik/core": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.2.1.tgz",
|
||||
"integrity": "sha512-YGJe+QorDGbkWDSUg6x69LYGN62Kgvb92Iz/voqmszVRKj4KcoPvd/7coF8Jmu+ZQE6LcwM/9ccB2i63L99ITA==",
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.3.3.tgz",
|
||||
"integrity": "sha512-ko7g54YSEcSq2K/GEpmiTG0foGLqe7DwgXGhkGxYEiHhLAUv8WvQmrFsm8e/KOW7n8mLGq0uaZVe2l8m3JTGGQ==",
|
||||
"requires": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.keyby": "^4.6.0",
|
||||
@@ -1227,9 +1227,9 @@
|
||||
}
|
||||
},
|
||||
"@blueprintjs-formik/select": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.1.5.tgz",
|
||||
"integrity": "sha512-EqGbuoiS1VrWpzjd39uVhBAmfVobdpgqalGcpODyGA+XAYoft1UU12yzTzrEOwBZpQKiC12UQwekUPspYBsVKA==",
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.2.3.tgz",
|
||||
"integrity": "sha512-j/zkX0B9wgtoHgK6Z/rlowB7F7zemrAajBU+d3caCoEYMMqwAI0XA++GytqrIhv5fEGjkZ1hkxS9j8eqX8vtjA==",
|
||||
"requires": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.keyby": "^4.6.0",
|
||||
@@ -1929,137 +1929,6 @@
|
||||
"reselect": "^4.1.7"
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.19.7.tgz",
|
||||
"integrity": "sha512-oDbklp4O3MtAM4mtuwyZLrgO1qDVYIujzNJQzXmi9YzymJCuzMLSRDvhY83NNDCRxf0pds4DShgYeZdbSyKraA==",
|
||||
"requires": {
|
||||
"@sentry/core": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz",
|
||||
"integrity": "sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/minimal": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.19.7.tgz",
|
||||
"integrity": "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.19.7.tgz",
|
||||
"integrity": "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/react": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.19.7.tgz",
|
||||
"integrity": "sha512-VzJeBg/v41jfxUYPkH2WYrKjWc4YiMLzDX0f4Zf6WkJ4v3IlDDSkX6DfmWekjTKBho6wiMkSNy2hJ1dHfGZ9jA==",
|
||||
"requires": {
|
||||
"@sentry/browser": "6.19.7",
|
||||
"@sentry/minimal": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.19.7.tgz",
|
||||
"integrity": "sha512-ol4TupNnv9Zd+bZei7B6Ygnr9N3Gp1PUrNI761QSlHtPC25xXC5ssSD3GMhBgyQrcvpuRcCFHVNNM97tN5cZiA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/minimal": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz",
|
||||
"integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.19.7.tgz",
|
||||
"integrity": "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sheerun/mutationobserver-shim": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
|
||||
@@ -5985,11 +5854,27 @@
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"dotenv-defaults": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz",
|
||||
"integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==",
|
||||
"requires": {
|
||||
"dotenv": "^8.2.0"
|
||||
}
|
||||
},
|
||||
"dotenv-expand": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
|
||||
},
|
||||
"dotenv-webpack": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz",
|
||||
"integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==",
|
||||
"requires": {
|
||||
"dotenv-defaults": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
@@ -7298,6 +7183,11 @@
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
|
||||
},
|
||||
"flat-cache": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@bigcapital/webapp",
|
||||
"version": "1.7.1",
|
||||
"version": "0.9.6",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@blueprintjs-formik/core": "^0.2.1",
|
||||
"@blueprintjs-formik/core": "^0.3.3",
|
||||
"@blueprintjs-formik/datetime": "^0.3.4",
|
||||
"@blueprintjs-formik/select": "^0.1.4",
|
||||
"@blueprintjs-formik/select": "^0.2.3",
|
||||
"@blueprintjs/core": "^3.50.2",
|
||||
"@blueprintjs/datetime": "^3.23.12",
|
||||
"@blueprintjs/popover2": "^0.11.1",
|
||||
@@ -16,8 +16,6 @@
|
||||
"@casl/react": "^2.3.0",
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@reduxjs/toolkit": "^1.2.5",
|
||||
"@sentry/react": "^6.13.2",
|
||||
"@sentry/tracing": "^6.13.2",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.4.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
@@ -44,7 +42,9 @@
|
||||
"deep-map-keys": "^2.0.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"flat": "^5.0.2",
|
||||
"formik": "^2.2.5",
|
||||
"http-proxy-middleware": "^1.0.0",
|
||||
"jest": "24.9.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |