mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-10 09:52:00 +00:00
Compare commits
35 Commits
agpl
...
contributi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
813bed3676 | ||
|
|
eecbcacb90 | ||
|
|
cfbe4cfea0 | ||
|
|
8f039b77e7 | ||
|
|
672a1bbb82 | ||
|
|
b2f3585047 | ||
|
|
e6434ea2d1 | ||
|
|
a21d6a37e4 | ||
|
|
e9fdffa9d9 | ||
|
|
6bd30abddb | ||
|
|
920c8ea95c | ||
|
|
8de3717587 | ||
|
|
cc863f774a | ||
|
|
bcd08284b4 | ||
|
|
8e8161f207 | ||
|
|
7b4b50cf4b | ||
|
|
bca3e51fdf | ||
|
|
6faa378577 | ||
|
|
012b13ad4a | ||
|
|
ad8770f12c | ||
|
|
c6cdbe11e6 | ||
|
|
308980604a | ||
|
|
32148a3207 | ||
|
|
fe270b3703 | ||
|
|
950b5407c3 | ||
|
|
e4a647376c | ||
|
|
85b24c7a4f | ||
|
|
4a22576d88 | ||
|
|
d1ab64e9bd | ||
|
|
110fdbaa4e | ||
|
|
961ff74880 | ||
|
|
da20b7c837 | ||
|
|
a5c190e094 | ||
|
|
7177276b12 | ||
|
|
65bb3a1cb8 |
68
.github/workflows/e2e.yml
vendored
Normal file
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
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
data
|
||||
.env
|
||||
.env
|
||||
test-results/
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,32 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
## [0.8.3] - 06-04-2023
|
||||
|
||||
`@bigcaptial/monorepo`
|
||||
|
||||
- Switch to AGPL license to protect application's networks. by @abouolia
|
||||
|
||||
`@bigcapital/webapp`
|
||||
|
||||
### Added
|
||||
|
||||
- Improve the style of authentication pages. by @abouolia
|
||||
- Remove the phone number field from the authentication pages. by @abouolia
|
||||
- Remove the phone number field from the users management. by @abouolia
|
||||
- Add all countries options to the setup page. by @abouolia
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix intent type of reset password success toast.
|
||||
|
||||
`@bigcapital/server`
|
||||
|
||||
### Added
|
||||
|
||||
- Remove the phone number field from the authentication service. by @abouolia
|
||||
- Remove the phone number field from the users service. by @abouolia
|
||||
|
||||
## [0.8.1] - 26-03-2023
|
||||
|
||||
`@bigcaptial/monorepo`
|
||||
|
||||
132
CONTRIBUTING.md
Normal file
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 the 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` to see the result 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 Stack Overflow and SigNoz Community Slack 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!
|
||||
@@ -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}
|
||||
@@ -77,24 +77,26 @@ services:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: docker/migration/Dockerfile
|
||||
args:
|
||||
- DB_HOST=mysql
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_CHARSET=${DB_CHARSET}
|
||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_CHARSET=${DB_CHARSET}
|
||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||
depends_on:
|
||||
- mysql
|
||||
|
||||
mysql:
|
||||
container_name: bigcapital-mysql
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
args:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_NAME}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- ./data/mysql/:/var/lib/mysql
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
- '3306'
|
||||
|
||||
@@ -104,7 +106,7 @@ services:
|
||||
expose:
|
||||
- '27017'
|
||||
volumes:
|
||||
- ./data/mongo/:/var/lib/mongodb
|
||||
- mongo:/var/lib/mongodb
|
||||
|
||||
redis:
|
||||
container_name: bigcapital-redis
|
||||
@@ -113,4 +115,18 @@ services:
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
- redis:/data
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
mysql:
|
||||
name: bigcapital_prod_mysql
|
||||
driver: local
|
||||
|
||||
mongo:
|
||||
name: bigcapital_prod_mongo
|
||||
driver: local
|
||||
|
||||
redis:
|
||||
name: bigcapital_prod_redis
|
||||
driver: local
|
||||
|
||||
@@ -9,13 +9,13 @@ services:
|
||||
mysql:
|
||||
build:
|
||||
context: ./docker/mysql
|
||||
args:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_NAME}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
environment:
|
||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- ./data/mysql/:/var/lib/mysql
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
- '3306'
|
||||
ports:
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
expose:
|
||||
- '27017'
|
||||
volumes:
|
||||
- ./data/mongo/:/var/lib/mongodb
|
||||
- mongo:/var/lib/mongodb
|
||||
ports:
|
||||
- '27017:27017'
|
||||
|
||||
@@ -36,4 +36,18 @@ services:
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
- redis:/data
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
mysql:
|
||||
name: bigcapital_dev_mysql
|
||||
driver: local
|
||||
|
||||
mongo:
|
||||
name: bigcapital_dev_mongo
|
||||
driver: local
|
||||
|
||||
redis:
|
||||
name: bigcapital_dev_redis
|
||||
driver: local
|
||||
@@ -1,9 +1,8 @@
|
||||
FROM mysql:5.7
|
||||
|
||||
USER root
|
||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
||||
|
||||
RUN chown -R mysql:root /var/lib/mysql/
|
||||
|
||||
ARG MYSQL_DATABASE=default_database
|
||||
ARG MYSQL_USER=default_user
|
||||
ARG MYSQL_PASSWORD=secret
|
||||
@@ -14,5 +13,14 @@ ENV MYSQL_USER=$MYSQL_USER
|
||||
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
|
||||
ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
|
||||
|
||||
# Copy init sql file with env vars and then the script will substitute the variables.
|
||||
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 mysql user inside the MySQL Docker container.
|
||||
RUN chown -R mysql:root /docker-entrypoint-initdb.d
|
||||
RUN chown -R mysql:root /scripts
|
||||
|
||||
CMD ["mysqld"]
|
||||
EXPOSE 3306
|
||||
18
docker/mysql/docker-entrypoint.sh
Normal file
18
docker/mysql/docker-entrypoint.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# chmod u+rwx /scripts/init.template.sql
|
||||
cp /scripts/init.template.sql /scripts/init.sql
|
||||
|
||||
# Replace environment variables in SQL files with their values
|
||||
if [ -n "$MYSQL_USER" ]; then
|
||||
sed -i "s/{MYSQL_USER}/$MYSQL_USER/g" /scripts/init.sql
|
||||
fi
|
||||
if [ -n "$MYSQL_PASSWORD" ]; then
|
||||
sed -i "s/{MYSQL_PASSWORD}/$MYSQL_PASSWORD/g" /scripts/init.sql
|
||||
fi
|
||||
if [ -n "$MYSQL_DATABASE" ]; then
|
||||
sed -i "s/{MYSQL_DATABASE}/$MYSQL_DATABASE/g" /scripts/init.sql
|
||||
fi
|
||||
|
||||
# Execute SQL file
|
||||
mysql -u root -p$MYSQL_ROOT_PASSWORD < /scripts/init.sql
|
||||
2
docker/mysql/init.sql
Normal file
2
docker/mysql/init.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
68
e2e/authentication.spec.ts
Normal file
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
package-lock.json
generated
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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import countries from 'country-codes-list';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||
import config from '@/config';
|
||||
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController extends BaseController {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
private authApplication: AuthenticationApplication;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
@@ -56,9 +53,10 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Login schema.
|
||||
* Login validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get loginSchema(): ValidationChain[] {
|
||||
private get loginSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
@@ -66,9 +64,10 @@ export default class AuthenticationController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register schema.
|
||||
* Register validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get registerSchema(): ValidationChain[] {
|
||||
private get registerSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('first_name')
|
||||
.exists()
|
||||
@@ -89,71 +88,20 @@ export default class AuthenticationController extends BaseController {
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('phone_number')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.phoneNumberValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('country')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.countryValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Country validator.
|
||||
*/
|
||||
countryValidator(value, { req }) {
|
||||
const {
|
||||
countries: { whitelist, blacklist },
|
||||
} = config.registration;
|
||||
const foundCountry = countries.findOne('countryCode', value);
|
||||
|
||||
if (!foundCountry) {
|
||||
throw new Error('The country code is invalid.');
|
||||
}
|
||||
if (
|
||||
// Focus with me! In case whitelist is not empty and the given coutry is not
|
||||
// in whitelist throw the error.
|
||||
//
|
||||
// Or in case the blacklist is not empty and the given country exists
|
||||
// in the blacklist throw the goddamn error.
|
||||
(whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
|
||||
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
|
||||
) {
|
||||
throw new Error('The country code is not supported yet.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phone number validator.
|
||||
*/
|
||||
phoneNumberValidator(value, { req }) {
|
||||
const phoneNumber = parsePhoneNumber(value, req.body.country);
|
||||
|
||||
if (!phoneNumber || !phoneNumber.isValid()) {
|
||||
throw new Error('Phone number is invalid with the given country code.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get resetPasswordSchema(): ValidationChain[] {
|
||||
private get resetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('password')
|
||||
.exists()
|
||||
@@ -170,8 +118,9 @@ export default class AuthenticationController extends BaseController {
|
||||
|
||||
/**
|
||||
* Send reset password validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
private get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [check('email').exists().isEmail().trim().escape()];
|
||||
}
|
||||
|
||||
@@ -180,11 +129,11 @@ export default class AuthenticationController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async login(req: Request, res: Response, next: Function): Response {
|
||||
private async login(req: Request, res: Response, next: Function): Response {
|
||||
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { token, user, tenant } = await this.authService.signIn(
|
||||
const { token, user, tenant } = await this.authApplication.signIn(
|
||||
userDTO.crediential,
|
||||
userDTO.password
|
||||
);
|
||||
@@ -199,13 +148,11 @@ export default class AuthenticationController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
private async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const registeredUser: ISystemUser = await this.authService.register(
|
||||
registerDTO
|
||||
);
|
||||
await this.authApplication.signUp(registerDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
@@ -222,11 +169,11 @@ export default class AuthenticationController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
private async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
await this.authApplication.sendResetPassword(email);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||
@@ -244,12 +191,12 @@ export default class AuthenticationController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response, next: Function) {
|
||||
private async resetPassword(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.resetPassword(token, password);
|
||||
await this.authApplication.resetPassword(token, password);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'RESET_PASSWORD_SUCCESS',
|
||||
@@ -263,7 +210,7 @@ export default class AuthenticationController extends BaseController {
|
||||
/**
|
||||
* Handles the service errors.
|
||||
*/
|
||||
handlerErrors(error, req: Request, res: Response, next: Function) {
|
||||
private handlerErrors(error, req: Request, res: Response, next: Function) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (
|
||||
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
|
||||
@@ -295,18 +242,10 @@ export default class AuthenticationController extends BaseController {
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('PHONE_NUMBER_EXISTS')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (error.hasType('EMAIL_EXISTS')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
if (error.errorType === 'EMAIL_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
|
||||
@@ -11,10 +11,10 @@ import AcceptInviteUserService from '@/services/InviteUsers/AcceptInviteUser';
|
||||
@Service()
|
||||
export default class InviteUsersController extends BaseController {
|
||||
@Inject()
|
||||
inviteUsersService: InviteTenantUserService;
|
||||
private inviteUsersService: InviteTenantUserService;
|
||||
|
||||
@Inject()
|
||||
acceptInviteService: AcceptInviteUserService;
|
||||
private acceptInviteService: AcceptInviteUserService;
|
||||
|
||||
/**
|
||||
* Routes that require authentication.
|
||||
@@ -68,13 +68,13 @@ export default class InviteUsersController extends BaseController {
|
||||
|
||||
/**
|
||||
* Invite DTO schema validation.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get inviteUserDTO() {
|
||||
private get inviteUserDTO() {
|
||||
return [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
check('password').exists().trim().escape().isLength({ min: 5 }),
|
||||
param('token').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
@@ -85,17 +85,14 @@ export default class InviteUsersController extends BaseController {
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async sendInvite(req: Request, res: Response, next: Function) {
|
||||
private async sendInvite(req: Request, res: Response, next: Function) {
|
||||
const sendInviteDTO = this.matchedBodyData(req);
|
||||
const { tenantId } = req;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const { invite } = await this.inviteUsersService.sendInvite(
|
||||
tenantId,
|
||||
sendInviteDTO,
|
||||
user
|
||||
);
|
||||
await this.inviteUsersService.sendInvite(tenantId, sendInviteDTO, user);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||
@@ -112,7 +109,7 @@ export default class InviteUsersController extends BaseController {
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async resendInvite(req: Request, res: Response, next: NextFunction) {
|
||||
private async resendInvite(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { userId } = req.params;
|
||||
|
||||
@@ -135,7 +132,7 @@ export default class InviteUsersController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async accept(req: Request, res: Response, next: Function) {
|
||||
private async accept(req: Request, res: Response, next: Function) {
|
||||
const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
@@ -161,7 +158,7 @@ export default class InviteUsersController extends BaseController {
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async invited(req: Request, res: Response, next: Function) {
|
||||
private async invited(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
@@ -181,7 +178,12 @@ export default class InviteUsersController extends BaseController {
|
||||
/**
|
||||
* Handles the service error.
|
||||
*/
|
||||
handleServicesError(error, req: Request, res: Response, next: Function) {
|
||||
private handleServicesError(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EMAIL_EXISTS') {
|
||||
return res.status(400).send({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -8,18 +8,12 @@ import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||
import {
|
||||
ACCEPTED_CURRENCIES,
|
||||
MONTHS,
|
||||
ACCEPTED_LOCALES,
|
||||
} from '@/services/Organization/constants';
|
||||
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
|
||||
import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants';
|
||||
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
|
||||
const ACCEPTED_LOCATIONS = ['libya'];
|
||||
|
||||
@Service()
|
||||
export default class OrganizationController extends BaseController {
|
||||
@Inject()
|
||||
@@ -65,8 +59,8 @@ export default class OrganizationController extends BaseController {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional().isString(),
|
||||
check('location').exists().isString().isIn(ACCEPTED_LOCATIONS),
|
||||
check('base_currency').exists().isIn(ACCEPTED_CURRENCIES),
|
||||
check('location').exists().isString().isISO31661Alpha2(),
|
||||
check('base_currency').exists().isISO4217(),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
check('fiscal_year').exists().isIn(MONTHS),
|
||||
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
|
||||
|
||||
@@ -47,7 +47,6 @@ export default class UsersController extends BaseController {
|
||||
check('first_name').exists(),
|
||||
check('last_name').exists(),
|
||||
check('email').exists().isEmail(),
|
||||
check('phone_number').optional().isMobilePhone(),
|
||||
check('role_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
|
||||
@@ -4,6 +4,7 @@ import color from 'colorette';
|
||||
import argv from 'getopts';
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import '../before';
|
||||
import config from '../config';
|
||||
|
||||
function initSystemKnex() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('phone_number');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('users', (table) => {});
|
||||
};
|
||||
@@ -42,6 +42,7 @@ export enum AccountNormal {
|
||||
|
||||
export interface IAccountsTransactionsFilter {
|
||||
accountId?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IAccountTransaction {
|
||||
|
||||
@@ -1,29 +1,77 @@
|
||||
import { ISystemUser } from './User';
|
||||
import { ITenant } from './Tenancy';
|
||||
import { SystemUser } from '@/system/models';
|
||||
|
||||
export interface IRegisterDTO {
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
password: string,
|
||||
organizationName: string,
|
||||
};
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
export interface ILoginDTO {
|
||||
crediential: string,
|
||||
password: string,
|
||||
};
|
||||
crediential: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IPasswordReset {
|
||||
id: number,
|
||||
email: string,
|
||||
token: string,
|
||||
createdAt: Date,
|
||||
};
|
||||
id: number;
|
||||
email: string;
|
||||
token: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IAuthenticationService {
|
||||
signIn(emailOrPhone: string, password: string): Promise<{ user: ISystemUser, token: string, tenant: ITenant }>;
|
||||
signIn(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ user: ISystemUser; token: string; tenant: ITenant }>;
|
||||
register(registerDTO: IRegisterDTO): Promise<ISystemUser>;
|
||||
sendResetPassword(email: string): Promise<IPasswordReset>;
|
||||
resetPassword(token: string, password: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IAuthSigningInEventPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
user: ISystemUser;
|
||||
}
|
||||
|
||||
export interface IAuthSignedInEventPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
user: ISystemUser;
|
||||
}
|
||||
|
||||
export interface IAuthSigningUpEventPayload {
|
||||
signupDTO: IRegisterDTO;
|
||||
}
|
||||
|
||||
export interface IAuthSignedUpEventPayload {
|
||||
signupDTO: IRegisterDTO;
|
||||
tenant: ITenant;
|
||||
user: ISystemUser;
|
||||
}
|
||||
|
||||
export interface IAuthSignInPOJO {
|
||||
user: ISystemUser;
|
||||
token: string;
|
||||
tenant: ITenant;
|
||||
}
|
||||
|
||||
export interface IAuthResetedPasswordEventPayload {
|
||||
user: SystemUser;
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IAuthSendingResetPassword {
|
||||
user: ISystemUser,
|
||||
token: string;
|
||||
}
|
||||
export interface IAuthSendedResetPassword {
|
||||
user: ISystemUser,
|
||||
token: string;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export interface ISystemUser extends Model {
|
||||
active: boolean;
|
||||
password: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
|
||||
roleId: number;
|
||||
tenantId: number;
|
||||
@@ -26,7 +25,6 @@ export interface ISystemUserDTO {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
phoneNumber: string;
|
||||
active: boolean;
|
||||
email: string;
|
||||
roleId?: number;
|
||||
@@ -35,7 +33,6 @@ export interface ISystemUserDTO {
|
||||
export interface IEditUserDTO {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
active: boolean;
|
||||
email: string;
|
||||
roleId: number;
|
||||
@@ -44,7 +41,6 @@ export interface IEditUserDTO {
|
||||
export interface IInviteUserInput {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
}
|
||||
export interface IUserInvite {
|
||||
@@ -111,7 +107,6 @@ export interface ITenantUser {
|
||||
id?: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
active: boolean;
|
||||
email: string;
|
||||
roleId?: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
|
||||
export default class WelcomeSMSJob {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Container } from 'typedi';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||
|
||||
export default class WelcomeEmailJob {
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite';
|
||||
import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotification';
|
||||
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';
|
||||
@@ -113,6 +114,7 @@ export const susbcribers = () => {
|
||||
SyncTenantAcceptInvite,
|
||||
InviteSendMainNotification,
|
||||
SyncTenantUserMutate,
|
||||
SyncTenantUserDelete,
|
||||
OrgSyncTenantAdminUserSubscriber,
|
||||
OrgBuildSmsNotificationSubscriber,
|
||||
PurgeUserAbilityCache,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces';
|
||||
import { AuthSigninService } from './AuthSignin';
|
||||
import { AuthSignupService } from './AuthSignup';
|
||||
import { AuthSendResetPassword } from './AuthSendResetPassword';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationApplication {
|
||||
@Inject()
|
||||
private authSigninService: AuthSigninService;
|
||||
|
||||
@Inject()
|
||||
private authSignupService: AuthSignupService;
|
||||
|
||||
@Inject()
|
||||
private authResetPasswordService: AuthSendResetPassword;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} email - Email address.
|
||||
* @param {string} password - Password.
|
||||
* @return {Promise<{user: IUser, token: string}>}
|
||||
*/
|
||||
public async signIn(email: string, password: string) {
|
||||
return this.authSigninService.signIn(email, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signup a new user.
|
||||
* @param {IRegisterDTO} signupDTO
|
||||
* @returns {Promise<ISystemUser>}
|
||||
*/
|
||||
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||
return this.authSignupService.signUp(signupDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
* @return {<Promise<IPasswordReset>}
|
||||
*/
|
||||
public async sendResetPassword(email: string): Promise<IPasswordReset> {
|
||||
return this.authResetPasswordService.sendResetPassword(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
return this.authResetPasswordService.resetPassword(token, password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import uniqid from 'uniqid';
|
||||
import moment from 'moment';
|
||||
import config from '@/config';
|
||||
import {
|
||||
IAuthResetedPasswordEventPayload,
|
||||
IAuthSendedResetPassword,
|
||||
IAuthSendingResetPassword,
|
||||
IPasswordReset,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { PasswordReset } from '@/system/models';
|
||||
import { ERRORS } from './_constants';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
@Service()
|
||||
export class AuthSendResetPassword {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject('repositories')
|
||||
private sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
* @return {<Promise<IPasswordReset>}
|
||||
*/
|
||||
public async sendResetPassword(email: string): Promise<PasswordReset> {
|
||||
const user = await this.validateEmailExistance(email);
|
||||
|
||||
const token: string = uniqid();
|
||||
|
||||
// Triggers sending reset password event.
|
||||
await this.eventPublisher.emitAsync(events.auth.sendingResetPassword, {
|
||||
user,
|
||||
token,
|
||||
} as IAuthSendingResetPassword);
|
||||
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
this.deletePasswordResetToken(email);
|
||||
|
||||
// Creates a new password reset row with unique token.
|
||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||
|
||||
// Triggers sent reset password event.
|
||||
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
|
||||
user,
|
||||
token,
|
||||
} as IAuthSendedResetPassword);
|
||||
|
||||
return passwordReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
// Finds the password reset token.
|
||||
const tokenModel: IPasswordReset = await PasswordReset.query().findOne(
|
||||
'token',
|
||||
token
|
||||
);
|
||||
// In case the password reset token not found throw token invalid error..
|
||||
if (!tokenModel) {
|
||||
throw new ServiceError(ERRORS.TOKEN_INVALID);
|
||||
}
|
||||
// Different between tokne creation datetime and current time.
|
||||
if (
|
||||
moment().diff(tokenModel.createdAt, 'seconds') >
|
||||
config.resetPasswordSeconds
|
||||
) {
|
||||
// Deletes the expired token by expired token email.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
|
||||
}
|
||||
const user = await systemUserRepository.findOneByEmail(tokenModel.email);
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError(ERRORS.USER_NOT_FOUND);
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await systemUserRepository.update(
|
||||
{ password: hashedPassword },
|
||||
{ id: user.id }
|
||||
);
|
||||
// Deletes the used token.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
|
||||
// Triggers `onResetPassword` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.resetPassword, {
|
||||
user,
|
||||
token,
|
||||
password,
|
||||
} as IAuthResetedPasswordEventPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the password reset token by the given email.
|
||||
* @param {string} email
|
||||
* @returns {Promise}
|
||||
*/
|
||||
private async deletePasswordResetToken(email: string) {
|
||||
return PasswordReset.query().where('email', email).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given email existance on the storage.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} email - email address.
|
||||
*/
|
||||
private async validateEmailExistance(email: string): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const userByEmail = await systemUserRepository.findOneByEmail(email);
|
||||
|
||||
if (!userByEmail) {
|
||||
throw new ServiceError(ERRORS.EMAIL_NOT_FOUND);
|
||||
}
|
||||
return userByEmail;
|
||||
}
|
||||
}
|
||||
103
packages/server/src/services/Authentication/AuthSignin.ts
Normal file
103
packages/server/src/services/Authentication/AuthSignin.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Tenant } from '@/system/models';
|
||||
import {
|
||||
IAuthSignedInEventPayload,
|
||||
IAuthSigningInEventPayload,
|
||||
IAuthSignInPOJO,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import { generateToken } from './_utils';
|
||||
import { ERRORS } from './_constants';
|
||||
|
||||
@Inject()
|
||||
export class AuthSigninService {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject('repositories')
|
||||
private sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Validates the given email and password.
|
||||
* @param {ISystemUser} user
|
||||
* @param {string} email
|
||||
* @param {string} password
|
||||
*/
|
||||
public async validateSignIn(
|
||||
user: ISystemUser,
|
||||
email: string,
|
||||
password: string
|
||||
) {
|
||||
const loginThrottler = Container.get('rateLimiter.login');
|
||||
|
||||
// Validate if the user is not exist.
|
||||
if (!user) {
|
||||
await loginThrottler.hit(email);
|
||||
throw new ServiceError(ERRORS.INVALID_DETAILS);
|
||||
}
|
||||
// Validate if the given user's password is wrong.
|
||||
if (!user.verifyPassword(password)) {
|
||||
await loginThrottler.hit(email);
|
||||
throw new ServiceError(ERRORS.INVALID_DETAILS);
|
||||
}
|
||||
// Validate if the given user is inactive.
|
||||
if (!user.active) {
|
||||
throw new ServiceError(ERRORS.USER_INACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} email - Email address.
|
||||
* @param {string} password - Password.
|
||||
* @return {Promise<{user: IUser, token: string}>}
|
||||
*/
|
||||
public async signIn(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<IAuthSignInPOJO> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
// Finds the user of the given email address.
|
||||
const user = await systemUserRepository.findOneByEmail(email);
|
||||
|
||||
// Validate the given email and password.
|
||||
await this.validateSignIn(user, email, password);
|
||||
|
||||
// Triggers on signing-in event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signingIn, {
|
||||
email,
|
||||
password,
|
||||
user,
|
||||
} as IAuthSigningInEventPayload);
|
||||
|
||||
const token = generateToken(user);
|
||||
|
||||
// Update the last login at of the user.
|
||||
await systemUserRepository.patchLastLoginAt(user.id);
|
||||
|
||||
// Triggers `onSignIn` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signIn, {
|
||||
email,
|
||||
password,
|
||||
user,
|
||||
} as IAuthSignedInEventPayload);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(user.tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Keep the user object immutable.
|
||||
const outputUser = cloneDeep(user);
|
||||
|
||||
// Remove password property from user object.
|
||||
Reflect.deleteProperty(outputUser, 'password');
|
||||
|
||||
return { user: outputUser, token, tenant };
|
||||
}
|
||||
}
|
||||
77
packages/server/src/services/Authentication/AuthSignup.ts
Normal file
77
packages/server/src/services/Authentication/AuthSignup.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IAuthSignedUpEventPayload,
|
||||
IAuthSigningUpEventPayload,
|
||||
IRegisterDTO,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { ERRORS } from './_constants';
|
||||
import { Inject } from 'typedi';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import TenantsManagerService from '../Tenancy/TenantsManager';
|
||||
import events from '@/subscribers/events';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
export class AuthSignupService {
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject('repositories')
|
||||
private sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
private tenantsManager: TenantsManagerService;
|
||||
|
||||
/**
|
||||
* Registers a new tenant with user from user input.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IRegisterDTO} signupDTO
|
||||
* @returns {Promise<ISystemUser>}
|
||||
*/
|
||||
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
// Validates the given email uniqiness.
|
||||
await this.validateEmailUniqiness(signupDTO.email);
|
||||
|
||||
const hashedPassword = await hashPassword(signupDTO.password);
|
||||
|
||||
// Triggers signin up event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signingUp, {
|
||||
signupDTO,
|
||||
} as IAuthSigningUpEventPayload);
|
||||
|
||||
const tenant = await this.tenantsManager.createTenant();
|
||||
const registeredUser = await systemUserRepository.create({
|
||||
...omit(signupDTO, 'country'),
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenantId: tenant.id,
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
});
|
||||
// Triggers signed up event.
|
||||
await this.eventPublisher.emitAsync(events.auth.signUp, {
|
||||
signupDTO,
|
||||
tenant,
|
||||
user: registeredUser,
|
||||
} as IAuthSignedUpEventPayload);
|
||||
|
||||
return registeredUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email uniqiness on the storage.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {string} email - Email address
|
||||
*/
|
||||
private async validateEmailUniqiness(email: string) {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const isEmailExists = await systemUserRepository.findOneByEmail(email);
|
||||
|
||||
if (isEmailExists) {
|
||||
throw new ServiceError(ERRORS.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/server/src/services/Authentication/_constants.ts
Normal file
10
packages/server/src/services/Authentication/_constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const ERRORS = {
|
||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||
USER_INACTIVE: 'USER_INACTIVE',
|
||||
EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND',
|
||||
TOKEN_INVALID: 'TOKEN_INVALID',
|
||||
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
};
|
||||
22
packages/server/src/services/Authentication/_utils.ts
Normal file
22
packages/server/src/services/Authentication/_utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import JWT from 'jsonwebtoken';
|
||||
import { ISystemUser } from '@/interfaces';
|
||||
import config from '@/config';
|
||||
|
||||
/**
|
||||
* Generates JWT token for the given user.
|
||||
* @param {ISystemUser} user
|
||||
* @return {string} token
|
||||
*/
|
||||
export const generateToken = (user: ISystemUser): string => {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
return JWT.sign(
|
||||
{
|
||||
id: user.id, // We are gonna use this in the middleware 'isAuth'
|
||||
exp: exp.getTime() / 1000,
|
||||
},
|
||||
config.jwtSecret
|
||||
);
|
||||
};
|
||||
@@ -1,322 +0,0 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import JWT from 'jsonwebtoken';
|
||||
import uniqid from 'uniqid';
|
||||
import { omit, cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { PasswordReset, Tenant } from '@/system/models';
|
||||
import {
|
||||
IRegisterDTO,
|
||||
ITenant,
|
||||
ISystemUser,
|
||||
IPasswordReset,
|
||||
IAuthenticationService,
|
||||
} from '@/interfaces';
|
||||
import { hashPassword } from 'utils';
|
||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||
import config from '@/config';
|
||||
import events from '@/subscribers/events';
|
||||
import AuthenticationMailMessages from '@/services/Authentication/AuthenticationMailMessages';
|
||||
import TenantsManager from '@/services/Tenancy/TenantsManager';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
const ERRORS = {
|
||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||
USER_INACTIVE: 'USER_INACTIVE',
|
||||
EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND',
|
||||
TOKEN_INVALID: 'TOKEN_INVALID',
|
||||
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
EMAIL_EXISTS: 'EMAIL_EXISTS',
|
||||
};
|
||||
@Service()
|
||||
export default class AuthenticationService implements IAuthenticationService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
mailMessages: AuthenticationMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
|
||||
/**
|
||||
* Signin and generates JWT token.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} emailOrPhone - Email or phone number.
|
||||
* @param {string} password - Password.
|
||||
* @return {Promise<{user: IUser, token: string}>}
|
||||
*/
|
||||
public async signIn(
|
||||
emailOrPhone: string,
|
||||
password: string
|
||||
): Promise<{
|
||||
user: ISystemUser;
|
||||
token: string;
|
||||
tenant: ITenant;
|
||||
}> {
|
||||
this.logger.info('[login] Someone trying to login.', {
|
||||
emailOrPhone,
|
||||
password,
|
||||
});
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const loginThrottler = Container.get('rateLimiter.login');
|
||||
|
||||
// Finds the user of the given email or phone number.
|
||||
const user = await systemUserRepository.findByCrediential(emailOrPhone);
|
||||
|
||||
if (!user) {
|
||||
// Hits the loging throttler to the given crediential.
|
||||
await loginThrottler.hit(emailOrPhone);
|
||||
|
||||
this.logger.info('[login] invalid data');
|
||||
throw new ServiceError(ERRORS.INVALID_DETAILS);
|
||||
}
|
||||
|
||||
this.logger.info('[login] check password validation.', {
|
||||
emailOrPhone,
|
||||
password,
|
||||
});
|
||||
if (!user.verifyPassword(password)) {
|
||||
// Hits the loging throttler to the given crediential.
|
||||
await loginThrottler.hit(emailOrPhone);
|
||||
|
||||
throw new ServiceError(ERRORS.INVALID_DETAILS);
|
||||
}
|
||||
if (!user.active) {
|
||||
this.logger.info('[login] user inactive.', { userId: user.id });
|
||||
throw new ServiceError(ERRORS.USER_INACTIVE);
|
||||
}
|
||||
|
||||
this.logger.info('[login] generating JWT token.', { userId: user.id });
|
||||
const token = this.generateToken(user);
|
||||
|
||||
this.logger.info('[login] updating user last login at.', {
|
||||
userId: user.id,
|
||||
});
|
||||
await systemUserRepository.patchLastLoginAt(user.id);
|
||||
|
||||
this.logger.info('[login] Logging success.', { user, token });
|
||||
|
||||
// Triggers `onLogin` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.login, {
|
||||
emailOrPhone,
|
||||
password,
|
||||
user,
|
||||
});
|
||||
const tenant = await Tenant.query().findById(user.tenantId).withGraphFetched('metadata');
|
||||
|
||||
// Keep the user object immutable.
|
||||
const outputUser = cloneDeep(user);
|
||||
|
||||
// Remove password property from user object.
|
||||
Reflect.deleteProperty(outputUser, 'password');
|
||||
|
||||
return { user: outputUser, token, tenant };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email and phone number uniqiness on the storage.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IRegisterDTO} registerDTO - Register data object.
|
||||
*/
|
||||
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
const isEmailExists = await systemUserRepository.findOneByEmail(
|
||||
registerDTO.email
|
||||
);
|
||||
const isPhoneExists = await systemUserRepository.findOneByPhoneNumber(
|
||||
registerDTO.phoneNumber
|
||||
);
|
||||
const errorReasons: ServiceError[] = [];
|
||||
|
||||
if (isPhoneExists) {
|
||||
this.logger.info('[register] phone number exists on the storage.');
|
||||
errorReasons.push(new ServiceError(ERRORS.PHONE_NUMBER_EXISTS));
|
||||
}
|
||||
if (isEmailExists) {
|
||||
this.logger.info('[register] email exists on the storage.');
|
||||
errorReasons.push(new ServiceError(ERRORS.EMAIL_EXISTS));
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
throw new ServiceErrors(errorReasons);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new tenant with user from user input.
|
||||
* @throws {ServiceErrors}
|
||||
* @param {IUserDTO} user
|
||||
*/
|
||||
public async register(registerDTO: IRegisterDTO): Promise<ISystemUser> {
|
||||
this.logger.info('[register] Someone trying to register.');
|
||||
await this.validateEmailAndPhoneUniqiness(registerDTO);
|
||||
|
||||
this.logger.info('[register] Creating a new tenant organization.');
|
||||
const tenant = await this.newTenantOrganization();
|
||||
|
||||
this.logger.info('[register] Trying hashing the password.');
|
||||
const hashedPassword = await hashPassword(registerDTO.password);
|
||||
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const registeredUser = await systemUserRepository.create({
|
||||
...omit(registerDTO, 'country'),
|
||||
active: true,
|
||||
password: hashedPassword,
|
||||
tenantId: tenant.id,
|
||||
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
|
||||
});
|
||||
// Triggers `onRegister` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.register, {
|
||||
registerDTO,
|
||||
tenant,
|
||||
user: registeredUser,
|
||||
});
|
||||
return registeredUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and insert new tenant organization id.
|
||||
* @async
|
||||
* @return {Promise<ITenant>}
|
||||
*/
|
||||
private async newTenantOrganization(): Promise<ITenant> {
|
||||
return this.tenantsManager.createTenant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given email existance on the storage.
|
||||
* @throws {ServiceError}
|
||||
* @param {string} email - email address.
|
||||
*/
|
||||
private async validateEmailExistance(email: string): Promise<ISystemUser> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
const userByEmail = await systemUserRepository.findOneByEmail(email);
|
||||
|
||||
if (!userByEmail) {
|
||||
this.logger.info('[send_reset_password] The given email not found.');
|
||||
throw new ServiceError(ERRORS.EMAIL_NOT_FOUND);
|
||||
}
|
||||
return userByEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and retrieve password reset token for the given user email.
|
||||
* @param {string} email
|
||||
* @return {<Promise<IPasswordReset>}
|
||||
*/
|
||||
public async sendResetPassword(email: string): Promise<IPasswordReset> {
|
||||
this.logger.info('[send_reset_password] Trying to send reset password.');
|
||||
const user = await this.validateEmailExistance(email);
|
||||
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
this.logger.info(
|
||||
'[send_reset_password] trying to delete all tokens by email.'
|
||||
);
|
||||
this.deletePasswordResetToken(email);
|
||||
|
||||
const token: string = uniqid();
|
||||
|
||||
this.logger.info('[send_reset_password] insert the generated token.');
|
||||
const passwordReset = await PasswordReset.query().insert({ email, token });
|
||||
|
||||
// Triggers `onSendResetPassword` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
|
||||
user,
|
||||
token,
|
||||
});
|
||||
return passwordReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets a user password from given token.
|
||||
* @param {string} token - Password reset token.
|
||||
* @param {string} password - New Password.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async resetPassword(token: string, password: string): Promise<void> {
|
||||
const { systemUserRepository } = this.sysRepositories;
|
||||
|
||||
// Finds the password reset token.
|
||||
const tokenModel: IPasswordReset = await PasswordReset.query().findOne(
|
||||
'token',
|
||||
token
|
||||
);
|
||||
// In case the password reset token not found throw token invalid error..
|
||||
if (!tokenModel) {
|
||||
this.logger.info('[reset_password] token invalid.');
|
||||
throw new ServiceError(ERRORS.TOKEN_INVALID);
|
||||
}
|
||||
// Different between tokne creation datetime and current time.
|
||||
if (
|
||||
moment().diff(tokenModel.createdAt, 'seconds') >
|
||||
config.resetPasswordSeconds
|
||||
) {
|
||||
this.logger.info('[reset_password] token expired.');
|
||||
|
||||
// Deletes the expired token by expired token email.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
|
||||
}
|
||||
const user = await systemUserRepository.findOneByEmail(tokenModel.email);
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError(ERRORS.USER_NOT_FOUND);
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
this.logger.info('[reset_password] saving a new hashed password.');
|
||||
await systemUserRepository.update(
|
||||
{ password: hashedPassword },
|
||||
{ id: user.id }
|
||||
);
|
||||
|
||||
// Deletes the used token.
|
||||
await this.deletePasswordResetToken(tokenModel.email);
|
||||
|
||||
// Triggers `onResetPassword` event.
|
||||
await this.eventPublisher.emitAsync(events.auth.resetPassword, {
|
||||
user,
|
||||
token,
|
||||
password,
|
||||
});
|
||||
this.logger.info('[reset_password] reset password success.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the password reset token by the given email.
|
||||
* @param {string} email
|
||||
* @returns {Promise}
|
||||
*/
|
||||
private async deletePasswordResetToken(email: string) {
|
||||
this.logger.info('[reset_password] trying to delete all tokens by email.');
|
||||
return PasswordReset.query().where('email', email).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JWT token for the given user.
|
||||
* @param {ISystemUser} user
|
||||
* @return {string} token
|
||||
*/
|
||||
generateToken(user: ISystemUser): string {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
this.logger.silly(`Sign JWT for userId: ${user.id}`);
|
||||
return JWT.sign(
|
||||
{
|
||||
id: user.id, // We are gonna use this in the middleware 'isAuth'
|
||||
exp: exp.getTime() / 1000,
|
||||
},
|
||||
config.jwtSecret
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Invite, SystemUser, Tenant } from '@/system/models';
|
||||
import { hashPassword } from 'utils';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
IAcceptInviteEventPayload,
|
||||
@@ -12,29 +10,13 @@ import {
|
||||
ICheckInviteEventPayload,
|
||||
IUserInvite,
|
||||
} from '@/interfaces';
|
||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
||||
import { ERRORS } from './constants';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export default class AcceptInviteUserService {
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
mailMessages: InviteUsersMailMessages;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManagerService;
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Accept the received invite.
|
||||
@@ -50,9 +32,6 @@ export default class AcceptInviteUserService {
|
||||
// Retrieve the invite token or throw not found error.
|
||||
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||
|
||||
// Validates the user phone number.
|
||||
await this.validateUserPhoneNumberNotExists(inviteUserDTO.phoneNumber);
|
||||
|
||||
// Hash the given password.
|
||||
const hashedPassword = await hashPassword(inviteUserDTO.password);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Inject, Service } from 'typedi';
|
||||
@Service()
|
||||
export default class InviteSendMainNotificationSubscribe {
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
private agenda: any;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -99,8 +85,6 @@ export default class InviteTenantUserService implements IInviteUserService {
|
||||
): Promise<{
|
||||
user: ITenantUser;
|
||||
}> {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the user by id or throw not found service error.
|
||||
const user = await this.getUserByIdOrThrowError(tenantId, userId);
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ export const DATE_FORMATS = [
|
||||
'MMMM dd, YYYY',
|
||||
'EEE, MMMM dd, YYYY',
|
||||
];
|
||||
export const ACCEPTED_CURRENCIES = Object.keys(currencies);
|
||||
|
||||
export const MONTHS = [
|
||||
'january',
|
||||
'february',
|
||||
|
||||
@@ -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
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
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,23 +14,22 @@ 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('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
repositories: any;
|
||||
@Inject()
|
||||
private rolesService: RolesService;
|
||||
|
||||
@Inject()
|
||||
rolesService: RolesService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
@@ -46,7 +45,7 @@ export default class UsersService {
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<any> {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
const { email, phoneNumber } = editUserDTO;
|
||||
const { email } = editUserDTO;
|
||||
|
||||
// Retrieve the tenant user or throw not found service error.
|
||||
const oldTenantUser = await this.getTenantUserOrThrowError(
|
||||
@@ -62,9 +61,6 @@ export default class UsersService {
|
||||
// Validate user email should be unique.
|
||||
await this.validateUserEmailUniquiness(tenantId, email, userId);
|
||||
|
||||
// Validate user phone number should be unique.
|
||||
await this.validateUserPhoneNumberUniqiness(tenantId, phoneNumber, userId);
|
||||
|
||||
// Retrieve the given role or throw not found service error.
|
||||
const role = await this.rolesService.getRoleOrThrowError(
|
||||
tenantId,
|
||||
@@ -97,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();
|
||||
|
||||
@@ -189,7 +186,7 @@ export default class UsersService {
|
||||
|
||||
const users = await User.query().withGraphFetched('role');
|
||||
|
||||
return users;
|
||||
return this.transformer.transform(tenantId, users, new UserTransformer());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,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);
|
||||
}
|
||||
}
|
||||
@@ -295,32 +294,11 @@ export default class UsersService {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate user phone number should be unique.
|
||||
* @param {string} phoneNumber -
|
||||
* @param {number} userId -
|
||||
*/
|
||||
private validateUserPhoneNumberUniqiness = async (
|
||||
tenantId: number,
|
||||
phoneNumber: string,
|
||||
userId: number
|
||||
) => {
|
||||
const { User } = this.tenancy.models(tenantId);
|
||||
|
||||
const userByPhoneNumber = await User.query()
|
||||
.findOne('phone_number', phoneNumber)
|
||||
.whereNot('id', userId);
|
||||
|
||||
if (userByPhoneNumber) {
|
||||
throw new ServiceError(ERRORS.PHONE_NUMBER_ALREADY_EXIST);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -334,5 +312,4 @@ export default class UsersService {
|
||||
throw new ServiceError(ERRORS.CANNOT_AUTHORIZED_USER_MUTATE_ROLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { IAuthSignedInEventPayload } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class ResetLoginThrottleSubscriber {
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
* @param bus
|
||||
* @param bus
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.auth.login, this.resetLoginThrottleOnceSuccessLogin);
|
||||
bus.subscribe(events.auth.signIn, this.resetLoginThrottleOnceSuccessLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the login throttle once the login success.
|
||||
* @param {IAuthSignedInEventPayload} payload -
|
||||
*/
|
||||
private async resetLoginThrottleOnceSuccessLogin(payload) {
|
||||
const { emailOrPhone, password, user } = payload;
|
||||
|
||||
private async resetLoginThrottleOnceSuccessLogin(
|
||||
payload: IAuthSignedInEventPayload
|
||||
) {
|
||||
const { email, user } = payload;
|
||||
const loginThrottler = Container.get('rateLimiter.login');
|
||||
|
||||
// Reset the login throttle by the given email and phone number.
|
||||
await loginThrottler.reset(user.email);
|
||||
await loginThrottler.reset(user.phoneNumber);
|
||||
await loginThrottler.reset(emailOrPhone);
|
||||
await loginThrottler.reset(email);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ export default class AuthSendWelcomeMailSubscriber {
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(events.auth.register, this.sendWelcomeEmailOnceUserRegister);
|
||||
bus.subscribe(events.auth.signUp, this.sendWelcomeEmailOnceUserRegister);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends welcome email once the user register.
|
||||
*/
|
||||
private sendWelcomeEmailOnceUserRegister = async (payload) => {
|
||||
const { registerDTO, tenant, user } = payload;
|
||||
const { tenant, user } = payload;
|
||||
|
||||
// Send welcome mail to the user.
|
||||
await this.agenda.now('welcome-email', {
|
||||
|
||||
@@ -3,10 +3,17 @@ export default {
|
||||
* Authentication service.
|
||||
*/
|
||||
auth: {
|
||||
login: 'onLogin',
|
||||
register: 'onRegister',
|
||||
signIn: 'onSignIn',
|
||||
signingIn: 'onSigningIn',
|
||||
|
||||
signUp: 'onSignUp',
|
||||
signingUp: 'onSigningUp',
|
||||
|
||||
sendingResetPassword: 'onSendingResetPassword',
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
|
||||
resetPassword: 'onResetPassword',
|
||||
resetingPassword: 'onResetingPassword'
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('users', (table) => {
|
||||
table.dropColumn('phone_number');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('users', (table) => {});
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import AppIntlLoader from './AppIntlLoader';
|
||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||
import Authentication from '@/components/Authentication';
|
||||
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||
|
||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||
import { queryConfig } from '../hooks/query/base';
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import authenticationRoutes from '@/routes/authentication';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
import '@/style/pages/Authentication/Auth.scss';
|
||||
|
||||
function PageFade(props) {
|
||||
return <CSSTransition {...props} classNames="authTransition" timeout={500} />;
|
||||
}
|
||||
|
||||
export default function AuthenticationWrapper({ ...rest }) {
|
||||
const to = { pathname: '/' };
|
||||
const location = useLocation();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const locationKey = location.pathname;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated ? (
|
||||
<Redirect to={to} />
|
||||
) : (
|
||||
<BodyClassName className={'authentication'}>
|
||||
<div class="authentication-page">
|
||||
<a
|
||||
href={'http://bigcapital.ly'}
|
||||
className={'authentication-page__goto-bigcapital'}
|
||||
>
|
||||
<T id={'go_to_bigcapital_com'} />
|
||||
</a>
|
||||
|
||||
<div class="authentication-page__form-wrapper">
|
||||
<div class="authentication-insider">
|
||||
<div className={'authentication-insider__logo-section'}>
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</div>
|
||||
|
||||
<TransitionGroup>
|
||||
<PageFade key={locationKey}>
|
||||
<Switch>
|
||||
{authenticationRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</PageFade>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BodyClassName>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
|
||||
import { If, Icon } from '@/components';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
|
||||
2269
packages/webapp/src/constants/countries.ts
Normal file
2269
packages/webapp/src/constants/countries.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,5 +3,4 @@ import intl from 'react-intl-universal';
|
||||
|
||||
export const getLanguages = () => [
|
||||
{ name: intl.get('english'), value: 'en' },
|
||||
{ name: intl.get('arabic'), value: 'ar' },
|
||||
];
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Icon } from '@/components/Icon';
|
||||
|
||||
export default function AuthCopyright() {
|
||||
return (
|
||||
<div class="auth-copyright">
|
||||
<div class="auth-copyright__text">
|
||||
{intl.get('all_rights_reserved', {
|
||||
pre: moment().subtract(1, 'years').year(),
|
||||
current: moment().get('year'),
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Icon width={122} height={22} icon={'bigcapital'} />
|
||||
</div>
|
||||
);
|
||||
return <Icon width={122} height={22} icon={'bigcapital'} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AuthCopyright from './AuthCopyright';
|
||||
import { AuthInsiderContent, AuthInsiderCopyright } from './_components';
|
||||
|
||||
/**
|
||||
* Authentication insider page.
|
||||
@@ -9,16 +11,21 @@ export default function AuthInsider({
|
||||
logo = true,
|
||||
copyright = true,
|
||||
children,
|
||||
classNames,
|
||||
}) {
|
||||
return (
|
||||
<div class="authentication-insider__content">
|
||||
<div class="authentication-insider__form">
|
||||
{ children }
|
||||
</div>
|
||||
<AuthInsiderContent>
|
||||
<AuthInsiderContentWrap className={classNames?.content}>
|
||||
{children}
|
||||
</AuthInsiderContentWrap>
|
||||
|
||||
<div class="authentication-insider__footer">
|
||||
<AuthCopyright />
|
||||
</div>
|
||||
</div>
|
||||
{copyright && (
|
||||
<AuthInsiderCopyright className={classNames?.copyrightWrap}>
|
||||
<AuthCopyright />
|
||||
</AuthInsiderCopyright>
|
||||
)}
|
||||
</AuthInsiderContent>
|
||||
);
|
||||
}
|
||||
|
||||
const AuthInsiderContentWrap = styled.div``;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import styled from 'styled-components';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
|
||||
import authenticationRoutes from '@/routes/authentication';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
import '@/style/pages/Authentication/Auth.scss';
|
||||
|
||||
export function Authentication() {
|
||||
const to = { pathname: '/' };
|
||||
const location = useLocation();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const locationKey = location.pathname;
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect to={to} />;
|
||||
}
|
||||
return (
|
||||
<BodyClassName className={'authentication'}>
|
||||
<AuthPage>
|
||||
<AuthInsider>
|
||||
<AuthLogo>
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</AuthLogo>
|
||||
|
||||
<TransitionGroup>
|
||||
<CSSTransition
|
||||
timeout={500}
|
||||
key={locationKey}
|
||||
classNames="authTransition"
|
||||
>
|
||||
<Switch>
|
||||
{authenticationRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</CSSTransition>
|
||||
</TransitionGroup>
|
||||
</AuthInsider>
|
||||
</AuthPage>
|
||||
</BodyClassName>
|
||||
);
|
||||
}
|
||||
|
||||
const AuthPage = styled.div``;
|
||||
const AuthInsider = styled.div`
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 40px;
|
||||
padding-top: 80px;
|
||||
`;
|
||||
|
||||
const AuthLogo = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
@@ -4,13 +4,21 @@ import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Intent, Position } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||
import { AppToaster } from '@/components';
|
||||
import { InviteAcceptSchema } from './utils';
|
||||
import InviteAcceptFormContent from './InviteAcceptFormContent';
|
||||
import { AuthInsiderCard } from './_components';
|
||||
|
||||
const initialValues = {
|
||||
organization_name: '',
|
||||
invited_email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
export default function InviteAcceptForm() {
|
||||
const history = useHistory();
|
||||
@@ -19,9 +27,8 @@ export default function InviteAcceptForm() {
|
||||
const { inviteAcceptMutate, inviteMeta, token } = useInviteAcceptContext();
|
||||
|
||||
// Invite value.
|
||||
const inviteValue = {
|
||||
organization_name: '',
|
||||
invited_email: '',
|
||||
const inviteFormValue = {
|
||||
...initialValues,
|
||||
...(!isEmpty(inviteMeta)
|
||||
? {
|
||||
invited_email: inviteMeta.email,
|
||||
@@ -33,19 +40,17 @@ export default function InviteAcceptForm() {
|
||||
// Handle form submitting.
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
inviteAcceptMutate([values, token])
|
||||
.then((response) => {
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: intl.getHTML(
|
||||
'congrats_your_account_has_been_created_and_invited',
|
||||
{
|
||||
organization_name: inviteValue.organization_name,
|
||||
organization_name: inviteMeta.organizationName,
|
||||
},
|
||||
),
|
||||
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
@@ -80,23 +85,13 @@ export default function InviteAcceptForm() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'invite-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'welcome_to_bigcapital'} />
|
||||
</h3>
|
||||
<p>
|
||||
<T id={'enter_your_personal_information'} />{' '}
|
||||
<b>{inviteValue.organization_name}</b> <T id={'organization'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AuthInsiderCard>
|
||||
<Formik
|
||||
validationSchema={InviteAcceptSchema}
|
||||
initialValues={inviteValue}
|
||||
initialValues={inviteFormValue}
|
||||
onSubmit={handleSubmit}
|
||||
component={InviteAcceptFormContent}
|
||||
/>
|
||||
</div>
|
||||
</AuthInsiderCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,73 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField, useFormikContext } from 'formik';
|
||||
import { Button, InputGroup, Intent } from '@blueprintjs/core';
|
||||
import { Form, useFormikContext } from 'formik';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Col, Row, FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
Col,
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
Row,
|
||||
FormattedMessage as T,
|
||||
} from '@/components';
|
||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||
import { PasswordRevealer } from './components';
|
||||
import { AuthSubmitButton } from './_components';
|
||||
|
||||
/**
|
||||
* Invite user form.
|
||||
*/
|
||||
export default function InviteUserFormContent() {
|
||||
// Invite accept context.
|
||||
const { inviteMeta } = useInviteAcceptContext();
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
|
||||
// Formik context.
|
||||
const { inviteMeta } = useInviteAcceptContext();
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
const handleLockClick = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
const lockButton = (
|
||||
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||
<Button
|
||||
icon={showPassword ? 'unlock' : 'lock'}
|
||||
intent={Intent.WARNING}
|
||||
minimal={true}
|
||||
onClick={handleLockClick}
|
||||
small={true}
|
||||
/>
|
||||
</Tooltip2>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<FastField name={'first_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'first_name'} />}
|
||||
className={'form-group--first_name'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'first_name'} />}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'first_name'} label={<T id={'first_name'} />}>
|
||||
<FInputGroup name={'first_name'} large={true} />
|
||||
</FFormGroup>
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<FastField name={'last_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'last_name'} />}
|
||||
className={'form-group--last_name'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'last_name'} />}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'last_name'} label={<T id={'last_name'} />}>
|
||||
<FInputGroup name={'last_name'} large={true} />
|
||||
</FFormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<FastField name={'phone_number'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'phone_number'} />}
|
||||
className={'form-group--phone_number'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'phone_number'} />}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||
<FInputGroup
|
||||
name={'password'}
|
||||
large={true}
|
||||
rightElement={lockButton}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<FastField name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={passwordType}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<div className={'invite-form__statement-section'}>
|
||||
<InviteAcceptFooterParagraphs>
|
||||
<p>
|
||||
<T id={'you_email_address_is'} /> <b>{inviteMeta.email},</b> <br />
|
||||
<T id={'you_will_use_this_address_to_sign_in_to_bigcapital'} />
|
||||
@@ -115,18 +78,25 @@ export default function InviteUserFormContent() {
|
||||
privacy: (msg) => <Link>{msg}</Link>,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</InviteAcceptFooterParagraphs>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'create_account'} />
|
||||
</Button>
|
||||
</div>
|
||||
<InviteAuthSubmitButton
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
large={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'create_account'} />
|
||||
</InviteAuthSubmitButton>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const InviteAcceptFooterParagraphs = styled.div`
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
const InviteAuthSubmitButton = styled(AuthSubmitButton)`
|
||||
margin-top: 1.6rem;
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useInviteMetaByToken, useAuthInviteAccept } from '@/hooks/query';
|
||||
import { InviteAcceptLoading } from './components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const InviteAcceptContext = createContext();
|
||||
|
||||
@@ -22,11 +22,10 @@ function InviteAcceptProvider({ token, ...props }) {
|
||||
const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (inviteMetaError) { history.push('/auth/login'); }
|
||||
}, [history, inviteMetaError]);
|
||||
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Formik } from 'formik';
|
||||
import { AppToaster as Toaster, FormattedMessage as T } from '@/components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { AppToaster as Toaster, FormattedMessage as T } from '@/components';
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
import { useAuthLogin } from '@/hooks/query';
|
||||
|
||||
import LoginForm from './LoginForm';
|
||||
import { LoginSchema, transformLoginErrorsToToasts } from './utils';
|
||||
import {
|
||||
AuthFooterLinks,
|
||||
AuthFooterLink,
|
||||
AuthInsiderCard,
|
||||
} from './_components';
|
||||
|
||||
const initialValues = {
|
||||
crediential: '',
|
||||
password: '',
|
||||
keepLoggedIn: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Login page.
|
||||
@@ -38,34 +49,32 @@ export default function Login() {
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className="login-form">
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'log_in'} />
|
||||
</h3>
|
||||
{/* <T id={'need_bigcapital_account'} />
|
||||
<Link to="/auth/register">
|
||||
{' '}
|
||||
<T id={'create_an_account'} />
|
||||
</Link> */}
|
||||
</div>
|
||||
|
||||
<AuthInsiderCard>
|
||||
<Formik
|
||||
initialValues={{
|
||||
crediential: '',
|
||||
password: '',
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
validationSchema={LoginSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={LoginForm}
|
||||
/>
|
||||
</AuthInsiderCard>
|
||||
|
||||
<div class="authentication-page__footer-links">
|
||||
<Link to={'/auth/send_reset_password'}>
|
||||
<T id={'forget_my_password'} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<LoginFooterLinks />
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginFooterLinks() {
|
||||
return (
|
||||
<AuthFooterLinks>
|
||||
<AuthFooterLink>
|
||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
||||
</AuthFooterLink>
|
||||
|
||||
<AuthFooterLink>
|
||||
<Link to={'/auth/send_reset_password'}>
|
||||
<T id={'forget_my_password'} />
|
||||
</Link>
|
||||
</AuthFooterLink>
|
||||
</AuthFooterLinks>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,63 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
InputGroup,
|
||||
Intent,
|
||||
FormGroup,
|
||||
Checkbox,
|
||||
} from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, Field } from 'formik';
|
||||
import { T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { PasswordRevealer } from './components';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { Form } from 'formik';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
|
||||
import { FFormGroup, FInputGroup, FCheckbox, T } from '@/components';
|
||||
import { AuthSubmitButton } from './_components';
|
||||
|
||||
/**
|
||||
* Login form.
|
||||
*/
|
||||
export default function LoginForm({ isSubmitting }) {
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
const handleLockClick = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
const lockButton = (
|
||||
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||
<Button
|
||||
icon={showPassword ? 'unlock' : 'lock'}
|
||||
intent={Intent.WARNING}
|
||||
minimal={true}
|
||||
onClick={handleLockClick}
|
||||
small={true}
|
||||
/>
|
||||
</Tooltip2>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form className={'authentication-page__form'}>
|
||||
<Field name={'crediential'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email_or_phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'crediential'} />}
|
||||
className={'form-group--crediential'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
large={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<Form>
|
||||
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
||||
<FInputGroup name={'crediential'} large={true} />
|
||||
</FFormGroup>
|
||||
|
||||
<Field name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
>
|
||||
<InputGroup
|
||||
large={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
type={passwordType}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||
<FInputGroup
|
||||
name={'password'}
|
||||
large={true}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
rightElement={lockButton}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<div className={'login-form__checkbox-section'}>
|
||||
<Checkbox large={true} className={'checkbox--remember-me'}>
|
||||
<T id={'keep_me_logged_in'} />
|
||||
</Checkbox>
|
||||
</div>
|
||||
<FCheckbox name={'keepLoggedIn'}>
|
||||
<T id={'keep_me_logged_in'} />
|
||||
</FCheckbox>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
lang={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'log_in'} />
|
||||
</Button>
|
||||
</div>
|
||||
<AuthSubmitButton
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
large={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'log_in'} />
|
||||
</AuthSubmitButton>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication';
|
||||
|
||||
import RegisterForm from './RegisterForm';
|
||||
import { RegisterSchema, transformRegisterErrorsToForm } from './utils';
|
||||
import {
|
||||
AuthFooterLinks,
|
||||
AuthFooterLink,
|
||||
AuthInsiderCard,
|
||||
} from './_components';
|
||||
|
||||
const initialValues = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Register form.
|
||||
@@ -19,18 +31,6 @@ export default function RegisterUserForm() {
|
||||
const { mutateAsync: authLoginMutate } = useAuthLogin();
|
||||
const { mutateAsync: authRegisterMutate } = useAuthRegister();
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
phone_number: '',
|
||||
password: '',
|
||||
country: 'LY',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
authRegisterMutate(values)
|
||||
.then((response) => {
|
||||
@@ -66,24 +66,32 @@ export default function RegisterUserForm() {
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className={'register-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'register_a_new_organization'} />
|
||||
</h3>
|
||||
<T id={'you_have_a_bigcapital_account'} />
|
||||
<Link to="/auth/login">
|
||||
<T id={'login'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AuthInsiderCard>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={RegisterSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={RegisterForm}
|
||||
/>
|
||||
</div>
|
||||
</AuthInsiderCard>
|
||||
|
||||
<RegisterFooterLinks />
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisterFooterLinks() {
|
||||
return (
|
||||
<AuthFooterLinks>
|
||||
<AuthFooterLink>
|
||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
||||
</AuthFooterLink>
|
||||
|
||||
<AuthFooterLink>
|
||||
<Link to={'/auth/send_reset_password'}>
|
||||
<T id={'forget_my_password'} />
|
||||
</Link>
|
||||
</AuthFooterLink>
|
||||
</AuthFooterLinks>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,148 +1,101 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import intl from 'react-intl-universal';
|
||||
import {
|
||||
Button,
|
||||
InputGroup,
|
||||
Intent,
|
||||
FormGroup,
|
||||
Spinner,
|
||||
} from '@blueprintjs/core';
|
||||
import { ErrorMessage, Field, Form } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { Intent, Button } from '@blueprintjs/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Row, Col, If } from '@/components';
|
||||
import { PasswordRevealer } from './components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
Row,
|
||||
Col,
|
||||
FormattedMessage as T,
|
||||
} from '@/components';
|
||||
import { AuthSubmitButton, AuthenticationLoadingOverlay } from './_components';
|
||||
|
||||
/**
|
||||
* Register form.
|
||||
*/
|
||||
export default function RegisterForm({ isSubmitting }) {
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
const [showPassword, setShowPassword] = React.useState<boolean>(false);
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
const handleLockClick = () => {
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
const lockButton = (
|
||||
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||
<Button
|
||||
icon={showPassword ? 'unlock' : 'lock'}
|
||||
intent={Intent.WARNING}
|
||||
minimal={true}
|
||||
onClick={handleLockClick}
|
||||
small={true}
|
||||
/>
|
||||
</Tooltip2>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form className={'authentication-page__form'}>
|
||||
<RegisterFormRoot>
|
||||
<Row className={'name-section'}>
|
||||
<Col md={6}>
|
||||
<Field name={'first_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'first_name'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'first_name'} />}
|
||||
className={'form-group--first-name'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<FFormGroup name={'first_name'} label={<T id={'first_name'} />}>
|
||||
<FInputGroup name={'first_name'} large={true} />
|
||||
</FFormGroup>
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<Field name={'last_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'last_name'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'last_name'} />}
|
||||
className={'form-group--last-name'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<FFormGroup name={'last_name'} label={<T id={'last_name'} />}>
|
||||
<FInputGroup name={'last_name'} large={true} />
|
||||
</FFormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Field name={'phone_number'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'phone_number'} />}
|
||||
className={'form-group--phone-number'}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<FFormGroup name={'email'} label={<T id={'email'} />}>
|
||||
<FInputGroup name={'email'} large={true} />
|
||||
</FFormGroup>
|
||||
|
||||
<Field name={'email'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'email'} />}
|
||||
className={'form-group--email'}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||
<FInputGroup
|
||||
name={'password'}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
rightElement={lockButton}
|
||||
large={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
<Field name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={passwordType}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
<TermsConditionsText>
|
||||
{intl.getHTML('signing_in_or_creating', {
|
||||
terms: (msg) => <Link>{msg}</Link>,
|
||||
privacy: (msg) => <Link>{msg}</Link>,
|
||||
})}
|
||||
</TermsConditionsText>
|
||||
|
||||
<div className={'register-form__agreement-section'}>
|
||||
<p>
|
||||
{intl.getHTML('signing_in_or_creating', {
|
||||
terms: (msg) => <Link>{msg}</Link>,
|
||||
privacy: (msg) => <Link>{msg}</Link>,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<AuthSubmitButton
|
||||
className={'btn-register'}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
large={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'register'} />
|
||||
</AuthSubmitButton>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
className={'btn-register'}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'register'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<If condition={isSubmitting}>
|
||||
<div class="authentication-page__loading-overlay">
|
||||
<Spinner size={50} />
|
||||
</div>
|
||||
</If>
|
||||
</Form>
|
||||
{isSubmitting && <AuthenticationLoadingOverlay />}
|
||||
</RegisterFormRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const TermsConditionsText = styled.p`
|
||||
opacity: 0.8;
|
||||
margin-bottom: 1.4rem;
|
||||
`;
|
||||
|
||||
const RegisterFormRoot = styled(Form)`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
@@ -4,14 +4,23 @@ import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent, Position } from '@blueprintjs/core';
|
||||
import { Link, useParams, useHistory } from 'react-router-dom';
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { useAuthResetPassword } from '@/hooks/query';
|
||||
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
|
||||
import {
|
||||
AuthFooterLink,
|
||||
AuthFooterLinks,
|
||||
AuthInsiderCard,
|
||||
} from './_components';
|
||||
import ResetPasswordForm from './ResetPasswordForm';
|
||||
import { ResetPasswordSchema } from './utils';
|
||||
|
||||
const initialValues = {
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
};
|
||||
/**
|
||||
* Reset password page.
|
||||
*/
|
||||
@@ -22,22 +31,13 @@ export default function ResetPassword() {
|
||||
// Authentication reset password.
|
||||
const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword();
|
||||
|
||||
// Initial values of the form.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values, { setSubmitting }) => {
|
||||
authResetPasswordMutate([token, values])
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get('password_successfully_updated'),
|
||||
intent: Intent.DANGER,
|
||||
intent: Intent.SUCCESS,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
@@ -64,24 +64,30 @@ export default function ResetPassword() {
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className={'submit-np-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'choose_a_new_password'} />
|
||||
</h3>
|
||||
<T id={'you_remembered_your_password'} />{' '}
|
||||
<Link to="/auth/login">
|
||||
<T id={'login'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AuthInsiderCard>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={ResetPasswordSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={ResetPasswordForm}
|
||||
/>
|
||||
</div>
|
||||
</AuthInsiderCard>
|
||||
|
||||
<ResetPasswordFooterLinks />
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetPasswordFooterLinks() {
|
||||
return (
|
||||
<AuthFooterLinks>
|
||||
<AuthFooterLink>
|
||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
||||
</AuthFooterLink>
|
||||
|
||||
<AuthFooterLink>
|
||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
||||
</AuthFooterLink>
|
||||
</AuthFooterLinks>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Form } from 'formik';
|
||||
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||
import { AuthSubmitButton } from './_components';
|
||||
|
||||
/**
|
||||
* Reset password form.
|
||||
@@ -11,54 +11,23 @@ import { inputIntent } from '@/utils';
|
||||
export default function ResetPasswordForm({ isSubmitting }) {
|
||||
return (
|
||||
<Form>
|
||||
<FastField name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'new_password'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={'password'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'password'} label={<T id={'new_password'} />}>
|
||||
<FInputGroup name={'password'} type={'password'} large={true} />
|
||||
</FFormGroup>
|
||||
|
||||
<FastField name={'confirm_password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'new_password'} />}
|
||||
labelInfo={'(again):'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'confirm_password'} />}
|
||||
className={'form-group--confirm-password'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={'password'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'confirm_password'} label={<T id={'new_password'} />}>
|
||||
<FInputGroup name={'confirm_password'} type={'password'} large={true} />
|
||||
</FFormGroup>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
fill={true}
|
||||
className={'btn-new'}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'submit'} />
|
||||
</Button>
|
||||
</div>
|
||||
<AuthSubmitButton
|
||||
fill={true}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
large={true}
|
||||
>
|
||||
<T id={'submit'} />
|
||||
</AuthSubmitButton>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,33 +5,32 @@ import { Formik } from 'formik';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useAuthSendResetPassword } from '@/hooks/query';
|
||||
|
||||
import SendResetPasswordForm from './SendResetPasswordForm';
|
||||
import {
|
||||
AuthFooterLink,
|
||||
AuthFooterLinks,
|
||||
AuthInsiderCard,
|
||||
} from './_components';
|
||||
import {
|
||||
SendResetPasswordSchema,
|
||||
transformSendResetPassErrorsToToasts,
|
||||
} from './utils';
|
||||
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
|
||||
const initialValues = {
|
||||
crediential: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Send reset password page.
|
||||
*/
|
||||
export default function SendResetPassword({ requestSendResetPassword }) {
|
||||
const history = useHistory();
|
||||
|
||||
const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword();
|
||||
|
||||
// Initial values.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
crediential: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Handle form submitting.
|
||||
const handleSubmit = (values, { setSubmitting }) => {
|
||||
sendResetPasswordMutate({ email: values.crediential })
|
||||
@@ -61,28 +60,30 @@ export default function SendResetPassword({ requestSendResetPassword }) {
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className="reset-form">
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'you_can_t_login'} />
|
||||
</h3>
|
||||
<p>
|
||||
<T id={'we_ll_send_a_recovery_link_to_your_email'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AuthInsiderCard>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={SendResetPasswordSchema}
|
||||
component={SendResetPasswordForm}
|
||||
/>
|
||||
<div class="authentication-page__footer-links">
|
||||
<Link to="/auth/login">
|
||||
<T id={'return_to_log_in'} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</AuthInsiderCard>
|
||||
|
||||
<SendResetPasswordFooterLinks />
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
|
||||
function SendResetPasswordFooterLinks() {
|
||||
return (
|
||||
<AuthFooterLinks>
|
||||
<AuthFooterLink>
|
||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
||||
</AuthFooterLink>
|
||||
|
||||
<AuthFooterLink>
|
||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
||||
</AuthFooterLink>
|
||||
</AuthFooterLinks>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Form } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FInputGroup, FFormGroup, FormattedMessage as T } from '@/components';
|
||||
import { AuthSubmitButton } from './_components';
|
||||
|
||||
/**
|
||||
* Send reset password form.
|
||||
*/
|
||||
export default function SendResetPasswordForm({ isSubmitting }) {
|
||||
return (
|
||||
<Form className={'send-reset-password'}>
|
||||
<FastField name={'crediential'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email_or_phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'crediential'} />}
|
||||
className={'form-group--crediential'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
large={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<Form>
|
||||
<TopParagraph>
|
||||
Enter the email address associated with your account and we'll send you
|
||||
a link to reset your password.
|
||||
</TopParagraph>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'send_reset_password_mail'} />
|
||||
</Button>
|
||||
</div>
|
||||
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
||||
<FInputGroup name={'crediential'} large={true} />
|
||||
</FFormGroup>
|
||||
|
||||
<AuthSubmitButton
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
large={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Reset Password
|
||||
</AuthSubmitButton>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const TopParagraph = styled.p`
|
||||
margin-bottom: 1.6rem;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
|
||||
export function AuthenticationLoadingOverlay() {
|
||||
return (
|
||||
<AuthOverlayRoot>
|
||||
<Spinner size={50} />
|
||||
</AuthOverlayRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const AuthOverlayRoot = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(252, 253, 255, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const AuthInsiderContent = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
export const AuthInsiderCard = styled.div`
|
||||
border: 1px solid #d5d5d5;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
padding: 26px 22px;
|
||||
background: #ffff;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
export const AuthInsiderCopyright = styled.div`
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 1.2rem;
|
||||
|
||||
.bp3-icon-bigcapital {
|
||||
svg {
|
||||
path {
|
||||
fill: #a3a3a3;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AuthFooterLinks = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-left: 1.2rem;
|
||||
padding-right: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
export const AuthFooterLink = styled.p`
|
||||
color: #666;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const AuthSubmitButton = styled(Button)`
|
||||
margin-top: 20px;
|
||||
|
||||
&.bp3-intent-primary {
|
||||
background-color: #0052cc;
|
||||
|
||||
&:disabled,
|
||||
&.bp3-disabled {
|
||||
background-color: rgba(0, 82, 204, 0.4);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,57 +1,42 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import { If, Icon, FormattedMessage as T } from '@/components';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
export function PasswordRevealer({ defaultShown = false, onChange }) {
|
||||
const [shown, setShown] = React.useState(defaultShown);
|
||||
|
||||
const handleClick = () => {
|
||||
setShown(!shown);
|
||||
saveInvoke(onChange, !shown);
|
||||
};
|
||||
|
||||
return (
|
||||
<span class="password-revealer" onClick={handleClick}>
|
||||
<If condition={shown}>
|
||||
<Icon icon="eye-slash" />{' '}
|
||||
<span class="text">
|
||||
<T id={'hide'} />
|
||||
</span>
|
||||
</If>
|
||||
<If condition={!shown}>
|
||||
<Icon icon="eye" />{' '}
|
||||
<span class="text">
|
||||
<T id={'show'} />
|
||||
</span>
|
||||
</If>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
import styled from 'styled-components';
|
||||
import { AuthInsiderCard } from './_components';
|
||||
import { Skeleton } from '@/components';
|
||||
|
||||
/**
|
||||
* Invite accept loading space.
|
||||
*/
|
||||
export function InviteAcceptLoading({ isLoading, children, ...props }) {
|
||||
export function InviteAcceptLoading({ isLoading, children }) {
|
||||
return isLoading ? (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={400}
|
||||
height={280}
|
||||
viewBox="0 0 400 280"
|
||||
backgroundColor="#f3f3f3"
|
||||
foregroundColor="#e6e6e6"
|
||||
{...props}
|
||||
>
|
||||
<rect x="0" y="80" rx="2" ry="2" width="200" height="20" />
|
||||
<rect x="0" y="0" rx="2" ry="2" width="250" height="30" />
|
||||
<rect x="0" y="38" rx="2" ry="2" width="300" height="15" />
|
||||
<rect x="0" y="175" rx="2" ry="2" width="200" height="20" />
|
||||
<rect x="1" y="205" rx="2" ry="2" width="385" height="38" />
|
||||
<rect x="0" y="110" rx="2" ry="2" width="385" height="38" />
|
||||
</ContentLoader>
|
||||
<AuthInsiderCard>
|
||||
<Fields>
|
||||
<SkeletonField />
|
||||
<SkeletonField />
|
||||
<SkeletonField />
|
||||
</Fields>
|
||||
</AuthInsiderCard>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonField() {
|
||||
return (
|
||||
<SkeletonFieldRoot>
|
||||
<Skeleton>XXXX XXXX</Skeleton>
|
||||
<Skeleton minWidth={100}>XXXX XXXX XXXX XXXX</Skeleton>
|
||||
</SkeletonFieldRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const Fields = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
const SkeletonFieldRoot = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
@@ -15,42 +15,19 @@ const REGISTER_ERRORS = {
|
||||
};
|
||||
|
||||
export const LoginSchema = Yup.object().shape({
|
||||
crediential: Yup.string()
|
||||
.required()
|
||||
.email()
|
||||
.label(intl.get('email')),
|
||||
password: Yup.string()
|
||||
.required()
|
||||
.min(4)
|
||||
.label(intl.get('password')),
|
||||
crediential: Yup.string().required().email().label(intl.get('email')),
|
||||
password: Yup.string().required().min(4).label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const RegisterSchema = Yup.object().shape({
|
||||
first_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('first_name_')),
|
||||
last_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('last_name_')),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required()
|
||||
.label(intl.get('email')),
|
||||
phone_number: Yup.string()
|
||||
.matches()
|
||||
.required()
|
||||
.label(intl.get('phone_number_')),
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||
last_name: Yup.string().required().label(intl.get('last_name_')),
|
||||
email: Yup.string().email().required().label(intl.get('email')),
|
||||
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const ResetPasswordSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||
confirm_password: Yup.string()
|
||||
.oneOf([Yup.ref('password'), null])
|
||||
.required()
|
||||
@@ -59,27 +36,13 @@ export const ResetPasswordSchema = Yup.object().shape({
|
||||
|
||||
// Validation schema.
|
||||
export const SendResetPasswordSchema = Yup.object().shape({
|
||||
crediential: Yup.string()
|
||||
.required()
|
||||
.email()
|
||||
.label(intl.get('email')),
|
||||
crediential: Yup.string().required().email().label(intl.get('email')),
|
||||
});
|
||||
|
||||
export const InviteAcceptSchema = Yup.object().shape({
|
||||
first_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('first_name_')),
|
||||
last_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('last_name_')),
|
||||
phone_number: Yup.string()
|
||||
.matches()
|
||||
.required()
|
||||
.label(intl.get('phone_number')),
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||
last_name: Yup.string().required().label(intl.get('last_name_')),
|
||||
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const transformSendResetPassErrorsToToasts = (errors) => {
|
||||
@@ -92,7 +55,7 @@ export const transformSendResetPassErrorsToToasts = (errors) => {
|
||||
});
|
||||
}
|
||||
return toastBuilders;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformLoginErrorsToToasts = (errors) => {
|
||||
const toastBuilders = [];
|
||||
@@ -109,25 +72,25 @@ export const transformLoginErrorsToToasts = (errors) => {
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (
|
||||
errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)
|
||||
) {
|
||||
if (errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)) {
|
||||
toastBuilders.push({
|
||||
message: intl.get('your_account_has_been_locked'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
return toastBuilders;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformRegisterErrorsToForm = (errors) => {
|
||||
const formErrors = {};
|
||||
|
||||
if (errors.some((e) => e.type === REGISTER_ERRORS.PHONE_NUMBER_EXISTS)) {
|
||||
formErrors.phone_number = intl.get('the_phone_number_already_used_in_another_account');
|
||||
formErrors.phone_number = intl.get(
|
||||
'the_phone_number_already_used_in_another_account',
|
||||
);
|
||||
}
|
||||
if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) {
|
||||
formErrors.email = intl.get('the_email_already_used_in_another_account');
|
||||
}
|
||||
return formErrors;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,11 @@ import { transformApiErrors } from './utils';
|
||||
|
||||
import { compose, objectKeysTransform } from '@/utils';
|
||||
|
||||
const initialValues = {
|
||||
email: '',
|
||||
role_id: ''
|
||||
}
|
||||
|
||||
function InviteUserForm({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
@@ -23,7 +28,8 @@ function InviteUserForm({
|
||||
const { dialogName, isEditMode, inviteUserMutate, userId } =
|
||||
useInviteUserFormContext();
|
||||
|
||||
const initialValues = {
|
||||
const initialFormValues = {
|
||||
...initialValues,
|
||||
status: 1,
|
||||
...(isEditMode &&
|
||||
pick(
|
||||
@@ -66,7 +72,7 @@ function InviteUserForm({
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={InviteUserFormSchema}
|
||||
initialValues={initialValues}
|
||||
initialValues={initialFormValues}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<InviteUserFormContent />
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ListSelect,
|
||||
FieldRequiredHint,
|
||||
FormattedMessage as T,
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
} from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import classNames from 'classnames';
|
||||
@@ -32,19 +34,13 @@ function InviteUserFormContent({
|
||||
<T id={'your_access_to_your_team'} />
|
||||
</p>
|
||||
{/* ----------- Email ----------- */}
|
||||
<FastField name={'email'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--email', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="email" />}
|
||||
>
|
||||
<InputGroup medium={true} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup
|
||||
name={'email'}
|
||||
label={<T id={'invite_user.label.email'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<FInputGroup name={'email'} />
|
||||
</FFormGroup>
|
||||
{/* ----------- Role name ----------- */}
|
||||
<FastField name={'role_id'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
@@ -78,7 +74,13 @@ function InviteUserFormContent({
|
||||
<T id={'cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '95px' }}
|
||||
>
|
||||
{isEditMode ? <T id={'edit'} /> : <T id={'invite'} />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,6 @@ const Schema = Yup.object().shape({
|
||||
email: Yup.string().email().required().label(intl.get('email')),
|
||||
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||
last_name: Yup.string().required().label(intl.get('last_name_')),
|
||||
phone_number: Yup.string()
|
||||
.matches()
|
||||
.required()
|
||||
.label(intl.get('phone_number_')),
|
||||
role_id: Yup.string().required().label(intl.get('roles.label.role_name_')),
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,14 @@ import UserFormContent from './UserFormContent';
|
||||
import { useUserFormContext } from './UserFormProvider';
|
||||
import { transformErrors } from './utils';
|
||||
|
||||
import { compose, objectKeysTransform } from '@/utils';
|
||||
import { compose, objectKeysTransform, transformToForm } from '@/utils';
|
||||
|
||||
const initialValues = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
role_id: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* User form.
|
||||
@@ -27,12 +34,9 @@ function UserForm({
|
||||
const { dialogName, user, userId, isEditMode, EditUserMutate } =
|
||||
useUserFormContext();
|
||||
|
||||
const initialValues = {
|
||||
...(isEditMode &&
|
||||
pick(
|
||||
objectKeysTransform(user, snakeCase),
|
||||
Object.keys(UserFormSchema.fields),
|
||||
)),
|
||||
const initialFormValues = {
|
||||
...initialValues,
|
||||
...(isEditMode && transformToForm(user, initialValues)),
|
||||
};
|
||||
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
@@ -68,7 +72,7 @@ function UserForm({
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={UserFormSchema}
|
||||
initialValues={initialValues}
|
||||
initialValues={initialFormValues}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<UserFormContent calloutCode={calloutCode} />
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { FastField, Form, useFormikContext, ErrorMessage } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { ListSelect, FieldRequiredHint } from '@/components';
|
||||
import { useUserFormContext } from './UserFormProvider';
|
||||
@@ -23,6 +24,7 @@ import { UserFormCalloutAlerts } from './components';
|
||||
*/
|
||||
function UserFormContent({
|
||||
calloutCode,
|
||||
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
@@ -39,60 +41,32 @@ function UserFormContent({
|
||||
<UserFormCalloutAlerts calloutCodes={calloutCode} />
|
||||
|
||||
{/* ----------- Email ----------- */}
|
||||
<FastField name={'email'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--email', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="email" />}
|
||||
>
|
||||
<InputGroup medium={true} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup
|
||||
name={'email'}
|
||||
label={<T id={'email'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<FInputGroup name={'email'} />
|
||||
</FFormGroup>
|
||||
|
||||
{/* ----------- First name ----------- */}
|
||||
<FastField name={'first_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'first_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'first_name'} />}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup
|
||||
name={'first_name'}
|
||||
label={<T id={'first_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<FInputGroup name={'first_name'} />
|
||||
</FFormGroup>
|
||||
|
||||
{/* ----------- Last name ----------- */}
|
||||
<FastField name={'last_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'last_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'last_name'} />}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
{/* ----------- Phone name ----------- */}
|
||||
<FastField name={'phone_number'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'phone_number'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'phone_number'} />}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup
|
||||
name={'last_name'}
|
||||
label={<T id={'last_name'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<FInputGroup name={'last_name'} />
|
||||
</FFormGroup>
|
||||
|
||||
{/* ----------- Role name ----------- */}
|
||||
<FastField name={'role_id'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
@@ -127,7 +101,13 @@ function UserFormContent({
|
||||
<T id={'cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '85px' }}
|
||||
>
|
||||
<T id={'edit'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,23 +4,24 @@ import intl from 'react-intl-universal';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { TableStyle } from '@/constants';
|
||||
import { Card, DataTable, If } from '@/components';
|
||||
import { AccountDrawerTableOptionsProvider } from './AccountDrawerTableOptionsProvider';
|
||||
import { AccountDrawerTableHeader } from './AccountDrawerTableHeader';
|
||||
|
||||
import { useAccountReadEntriesColumns } from './utils';
|
||||
import { useAppIntlContext } from '@/components/AppIntlProvider';
|
||||
import { useAccountDrawerContext } from './AccountDrawerProvider';
|
||||
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* account drawer table.
|
||||
*/
|
||||
function AccountDrawerTable({ closeDrawer }) {
|
||||
const { account, accounts, drawerName } = useAccountDrawerContext();
|
||||
|
||||
// Account read-only entries table columns.
|
||||
const columns = useAccountReadEntriesColumns();
|
||||
const { accounts, drawerName } = useAccountDrawerContext();
|
||||
|
||||
// Handle view more link click.
|
||||
const handleLinkClick = () => {
|
||||
@@ -31,27 +32,41 @@ function AccountDrawerTable({ closeDrawer }) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={accounts}
|
||||
payload={{ account }}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
<AccountDrawerTableOptionsProvider>
|
||||
<AccountDrawerTableHeader />
|
||||
<AccountDrawerDataTable />
|
||||
|
||||
<If condition={accounts.length > 0}>
|
||||
<TableFooter>
|
||||
<Link
|
||||
to={`/financial-reports/general-ledger`}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{isRTL ? '→' : '←'} {intl.get('view_more_transactions')}
|
||||
</Link>
|
||||
</TableFooter>
|
||||
</If>
|
||||
<If condition={accounts.length > 0}>
|
||||
<TableFooter>
|
||||
<Link
|
||||
to={`/financial-reports/general-ledger`}
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{isRTL ? '→' : '←'} {intl.get('view_more_transactions')}
|
||||
</Link>
|
||||
</TableFooter>
|
||||
</If>
|
||||
</AccountDrawerTableOptionsProvider>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountDrawerDataTable() {
|
||||
const { account, accounts } = useAccountDrawerContext();
|
||||
|
||||
// Account read-only entries table columns.
|
||||
const columns = useAccountReadEntriesColumns();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={accounts}
|
||||
payload={{ account }}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDrawerActions)(AccountDrawerTable);
|
||||
|
||||
const TableFooter = styled.div`
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Button, ButtonGroup } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { useAccountDrawerTableOptionsContext } from './AccountDrawerTableOptionsProvider';
|
||||
|
||||
export function AccountDrawerTableHeader() {
|
||||
const {
|
||||
setBCYCurrencyType,
|
||||
setFYCCurrencyType,
|
||||
isBCYCurrencyType,
|
||||
isFCYCurrencyType,
|
||||
} = useAccountDrawerTableOptionsContext();
|
||||
|
||||
const handleBCYBtnClick = () => {
|
||||
setBCYCurrencyType();
|
||||
};
|
||||
const handleFCYBtnClick = () => {
|
||||
setFYCCurrencyType();
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHeaderRoot>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
small
|
||||
outlined
|
||||
onClick={handleFCYBtnClick}
|
||||
active={isFCYCurrencyType}
|
||||
>
|
||||
FCY
|
||||
</Button>
|
||||
<Button
|
||||
small
|
||||
outlined
|
||||
onClick={handleBCYBtnClick}
|
||||
active={isBCYCurrencyType}
|
||||
>
|
||||
BCY
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</TableHeaderRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const TableHeaderRoot = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
interface AccountDrawerTableOptionsContextValue {
|
||||
setFYCCurrencyType: () => void;
|
||||
setBCYCurrencyType: () => void;
|
||||
isFCYCurrencyType: boolean;
|
||||
isBCYCurrencyType: boolean;
|
||||
currencyType: ForeignCurrencyType;
|
||||
}
|
||||
|
||||
const AccountDrawerTableOptionsContext = React.createContext(
|
||||
{} as AccountDrawerTableOptionsContextValue,
|
||||
);
|
||||
|
||||
enum ForeignCurrencyTypes {
|
||||
FCY = 'FCY',
|
||||
BCY = 'BCY',
|
||||
}
|
||||
type ForeignCurrencyType = ForeignCurrencyTypes.FCY | ForeignCurrencyTypes.BCY;
|
||||
|
||||
function AccountDrawerTableOptionsProvider({
|
||||
initialCurrencyType = ForeignCurrencyTypes.FCY,
|
||||
...props
|
||||
}) {
|
||||
const [currencyType, setCurrentType] =
|
||||
useState<ForeignCurrencyType>(initialCurrencyType);
|
||||
|
||||
const setFYCCurrencyType = useCallback(
|
||||
() => setCurrentType(ForeignCurrencyTypes.FCY),
|
||||
[setCurrentType],
|
||||
);
|
||||
const setBCYCurrencyType = useCallback(
|
||||
() => setCurrentType(ForeignCurrencyTypes.BCY),
|
||||
[setCurrentType],
|
||||
);
|
||||
|
||||
// Provider.
|
||||
const provider = {
|
||||
setFYCCurrencyType,
|
||||
setBCYCurrencyType,
|
||||
isFCYCurrencyType: currencyType === ForeignCurrencyTypes.FCY,
|
||||
isBCYCurrencyType: currencyType === ForeignCurrencyTypes.BCY,
|
||||
currencyType,
|
||||
};
|
||||
|
||||
return (
|
||||
<AccountDrawerTableOptionsContext.Provider value={provider} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const useAccountDrawerTableOptionsContext = () =>
|
||||
React.useContext(AccountDrawerTableOptionsContext);
|
||||
|
||||
export {
|
||||
AccountDrawerTableOptionsProvider,
|
||||
useAccountDrawerTableOptionsContext,
|
||||
};
|
||||
@@ -3,27 +3,15 @@ import intl from 'react-intl-universal';
|
||||
import React from 'react';
|
||||
|
||||
import { FormatDateCell } from '@/components';
|
||||
import { isBlank } from '@/utils';
|
||||
|
||||
/**
|
||||
* Debit/credit table cell.
|
||||
*/
|
||||
function DebitCreditTableCell({ value, payload: { account } }) {
|
||||
return !isBlank(value) && value !== 0 ? account.formatted_amount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Running balance table cell.
|
||||
*/
|
||||
function RunningBalanceTableCell({ value, payload: { account } }) {
|
||||
return account.formatted_amount;
|
||||
}
|
||||
import { useAccountDrawerTableOptionsContext } from './AccountDrawerTableOptionsProvider';
|
||||
|
||||
/**
|
||||
* Retrieve entries columns of read-only account view.
|
||||
*/
|
||||
export const useAccountReadEntriesColumns = () =>
|
||||
React.useMemo(
|
||||
export const useAccountReadEntriesColumns = () => {
|
||||
const { isFCYCurrencyType } = useAccountDrawerTableOptionsContext();
|
||||
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: intl.get('transaction_date'),
|
||||
@@ -34,14 +22,15 @@ export const useAccountReadEntriesColumns = () =>
|
||||
},
|
||||
{
|
||||
Header: intl.get('transaction_type'),
|
||||
accessor: 'reference_type_formatted',
|
||||
accessor: 'transaction_type_formatted',
|
||||
width: 100,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
Header: intl.get('credit'),
|
||||
accessor: 'credit',
|
||||
Cell: DebitCreditTableCell,
|
||||
accessor: isFCYCurrencyType
|
||||
? 'formatted_fc_credit'
|
||||
: 'formatted_credit',
|
||||
width: 80,
|
||||
className: 'credit',
|
||||
align: 'right',
|
||||
@@ -49,22 +38,13 @@ export const useAccountReadEntriesColumns = () =>
|
||||
},
|
||||
{
|
||||
Header: intl.get('debit'),
|
||||
accessor: 'debit',
|
||||
Cell: DebitCreditTableCell,
|
||||
accessor: isFCYCurrencyType ? 'formatted_fc_debit' : 'formatted_debit',
|
||||
width: 80,
|
||||
className: 'debit',
|
||||
align: 'right',
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
Header: intl.get('running_balance'),
|
||||
Cell: RunningBalanceTableCell,
|
||||
accessor: 'running_balance',
|
||||
width: 110,
|
||||
className: 'running_balance',
|
||||
align: 'right',
|
||||
textOverview: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[isFCYCurrencyType],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Menu,
|
||||
Popover,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Intent,
|
||||
Tag,
|
||||
} from '@blueprintjs/core';
|
||||
import { Icon } from '@/components';
|
||||
import { safeCallback } from '@/utils';
|
||||
@@ -52,12 +54,25 @@ export const ActionsCell = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CurrencyNameAccessor = (value) => {
|
||||
return (
|
||||
<CurrencyNameRoot>
|
||||
{value.currency_name} {value.is_base_currency && <Tag>Base Currency</Tag>}
|
||||
</CurrencyNameRoot>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrencyNameRoot = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export function useCurrenciesTableColumns() {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: intl.get('currency_name'),
|
||||
accessor: 'currency_name',
|
||||
accessor: CurrencyNameAccessor,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,14 +15,15 @@ import {
|
||||
} from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { getCountries } from '@/constants/countries';
|
||||
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
||||
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
||||
import { getLanguages } from '@/constants/languagesOptions';
|
||||
import { useGeneralFormContext } from './GeneralFormProvider';
|
||||
import { getAllCountries } from '@/utils/countries';
|
||||
|
||||
import { shouldBaseCurrencyUpdate } from './utils';
|
||||
|
||||
const Countries = getAllCountries();
|
||||
/**
|
||||
* Preferences general form.
|
||||
*/
|
||||
@@ -30,7 +31,6 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
|
||||
const history = useHistory();
|
||||
|
||||
const FiscalYear = getFiscalYear();
|
||||
const Countries = getCountries();
|
||||
const Languages = getLanguages();
|
||||
const Currencies = getAllCurrenciesOptions();
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ function UsersDataTable({
|
||||
// #withAlertActions
|
||||
openAlert,
|
||||
}) {
|
||||
const { mutateAsync: resendInviation } = useResendInvitation();
|
||||
|
||||
// Users list columns.
|
||||
const columns = useUsersListColumns();
|
||||
|
||||
// Users list context.
|
||||
const { users, isUsersLoading, isUsersFetching } = useUsersListContext();
|
||||
|
||||
// Handle edit user action.
|
||||
const handleEditUserAction = useCallback(
|
||||
(user) => {
|
||||
@@ -50,9 +58,6 @@ function UsersDataTable({
|
||||
},
|
||||
[openAlert],
|
||||
);
|
||||
|
||||
const { mutateAsync: resendInviation } = useResendInvitation();
|
||||
|
||||
const handleResendInvitation = useCallback((user) => {
|
||||
resendInviation(user.id)
|
||||
.then(() => {
|
||||
@@ -71,17 +76,12 @@ function UsersDataTable({
|
||||
AppToaster.show({
|
||||
message:
|
||||
'This person was recently invited. No need to invite them again just yet.',
|
||||
intent: Intent.DANGER,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
// Users list columns.
|
||||
const columns = useUsersListColumns();
|
||||
|
||||
// Users list context.
|
||||
const { users, isUsersLoading, isUsersFetching } = useUsersListContext();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
|
||||
@@ -132,15 +132,9 @@ export const useUsersListColumns = () => {
|
||||
{
|
||||
id: 'role_name',
|
||||
Header: intl.get('users.column.role_name'),
|
||||
accessor: 'role.name',
|
||||
accessor: 'role_name',
|
||||
width: 120,
|
||||
},
|
||||
// {
|
||||
// id: 'phone_number',
|
||||
// Header: intl.get('phone_number'),
|
||||
// accessor: 'phone_number',
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
id: 'status',
|
||||
Header: intl.get('status'),
|
||||
|
||||
@@ -5,15 +5,12 @@ import {
|
||||
Button,
|
||||
Intent,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
MenuItem,
|
||||
Classes,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { TimezonePicker } from '@blueprintjs/timezone';
|
||||
import useAutofocus from '@/hooks/useAutofocus'
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { getCountries } from '@/constants/countries';
|
||||
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||
|
||||
import { Col, Row, ListSelect } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
@@ -21,6 +18,9 @@ import { inputIntent } from '@/utils';
|
||||
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
||||
import { getLanguages } from '@/constants/languagesOptions';
|
||||
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
||||
import { getAllCountries } from '@/utils/countries';
|
||||
|
||||
const countries = getAllCountries();
|
||||
|
||||
/**
|
||||
* Setup organization form.
|
||||
@@ -29,9 +29,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
||||
const FiscalYear = getFiscalYear();
|
||||
const Languages = getLanguages();
|
||||
const currencies = getAllCurrenciesOptions();
|
||||
const countries = getCountries();
|
||||
|
||||
const accountRef = useAutofocus();
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@@ -40,22 +37,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
||||
</h3>
|
||||
|
||||
{/* ---------- Organization name ---------- */}
|
||||
<FastField name={'name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'legal_organization_name'} />}
|
||||
className={'form-group--name'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'name'} />}
|
||||
>
|
||||
<InputGroup
|
||||
{...field}
|
||||
intent={inputIntent({ error, touched })}
|
||||
inputRef={accountRef}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup name={'name'} label={<T id={'legal_organization_name'} />}>
|
||||
<FInputGroup name={'name'} />
|
||||
</FFormGroup>
|
||||
|
||||
{/* ---------- Location ---------- */}
|
||||
<FastField name={'location'}>
|
||||
@@ -71,11 +55,11 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
||||
>
|
||||
<ListSelect
|
||||
items={countries}
|
||||
onItemSelect={({ value }) => {
|
||||
form.setFieldValue('location', value);
|
||||
onItemSelect={({ countryCode }) => {
|
||||
form.setFieldValue('location', countryCode);
|
||||
}}
|
||||
selectedItem={value}
|
||||
selectedItemProp={'value'}
|
||||
selectedItemProp={'countryCode'}
|
||||
defaultText={<T id={'select_business_location'} />}
|
||||
textProp={'name'}
|
||||
popoverProps={{ minimal: true }}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { getSetupOrganizationValidation } from './SetupOrganization.schema';
|
||||
// Initial values.
|
||||
const defaultValues = {
|
||||
name: '',
|
||||
location: 'libya',
|
||||
location: '',
|
||||
baseCurrency: '',
|
||||
language: 'en',
|
||||
fiscalYear: '',
|
||||
|
||||
@@ -31,13 +31,14 @@
|
||||
"phone_number": "Phone Number",
|
||||
"you_email_address_is": "You email address is",
|
||||
"you_will_use_this_address_to_sign_in_to_bigcapital": "You will use this address to sign in to Bigcapital.",
|
||||
"signing_in_or_creating": "By signing in or creating an account, you agree with our <br/> <a>Terms & Conditions </a> and <a> Privacy Statement </a> ",
|
||||
"signing_in_or_creating": "By signing in or creating an account, you agree with our <a>Terms & Conditions </a> and <a> Privacy Statement </a> ",
|
||||
"and": "And",
|
||||
"create_account": "Create Account",
|
||||
"success": "Success",
|
||||
"register_a_new_organization": "Register a New Organization.",
|
||||
"organization_name": "Organization Name",
|
||||
"email": "Email",
|
||||
"email_address": "Email Address",
|
||||
"register": "Register",
|
||||
"password_successfully_updated": "The Password for your account was successfully updated.",
|
||||
"choose_a_new_password": "Choose a new password",
|
||||
@@ -2227,5 +2228,8 @@
|
||||
"project_billable_entries.dialog.show": "Show",
|
||||
"project_billable_entries.alert.there_is_no_billable_entries": "There is no billable entries for that project.",
|
||||
"project_billable_entries.billable_type": "Billable {value}",
|
||||
"add_billable_entries": "Add Billable Entries"
|
||||
"add_billable_entries": "Add Billable Entries",
|
||||
|
||||
"invite_user.label.email": "Email",
|
||||
"invite_user.label.role_name": "Role name"
|
||||
}
|
||||
@@ -1,224 +1,32 @@
|
||||
|
||||
body.authentication {
|
||||
background-color: #fcfdff;
|
||||
}
|
||||
|
||||
.authentication-insider {
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 40px;
|
||||
padding-top: 80px;
|
||||
|
||||
&__logo-section {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
.auth-copyright {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
.bp3-icon-bigcapital {
|
||||
margin-top: 9px;
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: #a3a3a3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.authTransition{
|
||||
|
||||
.authTransition {
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
&-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
&-enter-done {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0.5;
|
||||
transition: opacity 250ms ease-in-out;
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.authentication-page {
|
||||
&__goto-bigcapital {
|
||||
position: fixed;
|
||||
margin-top: 30px;
|
||||
margin-left: 30px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.bp3-input {
|
||||
min-height: 40px;
|
||||
}
|
||||
.bp3-form-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.bp3-form-group.has-password-revealer {
|
||||
.bp3-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.password-revealer {
|
||||
.text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-button.bp3-fill.bp3-intent-primary {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__label-section {
|
||||
margin-bottom: 30px;
|
||||
color: #555;
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
color: #2d2b43;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: #0040bd;
|
||||
}
|
||||
}
|
||||
|
||||
&__form-wrapper {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__footer-links {
|
||||
padding: 9px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: center;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
a {
|
||||
color: #0052cc;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(252, 253, 255, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__submit-button-wrap {
|
||||
margin: 0px 0px 24px 0px;
|
||||
|
||||
.bp3-button {
|
||||
background-color: #0052cc;
|
||||
min-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
// Login Form
|
||||
// ------------------------------
|
||||
.login-form {
|
||||
// width: 690px;
|
||||
// margin: 0px auto;
|
||||
// padding: 85px 50px;
|
||||
|
||||
.checkbox {
|
||||
&--remember-me {
|
||||
margin: -6px 0 26px 0px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register form
|
||||
// ----------------------------
|
||||
.register-form {
|
||||
|
||||
&__agreement-section {
|
||||
margin-top: -10px;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
&__submit-button-wrap {
|
||||
margin: 25px 0px 25px 0px;
|
||||
|
||||
.bp3-button {
|
||||
min-height: 45px;
|
||||
background-color: #0052cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send reset password
|
||||
// ----------------------------
|
||||
.send-reset-password {
|
||||
.form-group--crediential {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
// Invite form.
|
||||
// ----------------
|
||||
.invite-form {
|
||||
|
||||
&__statement-section {
|
||||
margin-top: -10px;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
.authentication-page__loading-overlay {
|
||||
background: rgba(252, 253, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,11 @@
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
|
||||
&:disabled,
|
||||
&.bp3-loading{
|
||||
background-color: rgba(28, 36, 72, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
packages/webapp/src/utils/countries.tsx
Normal file
10
packages/webapp/src/utils/countries.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Countries } from '@/constants/countries';
|
||||
|
||||
export const getAllCountries = () => {
|
||||
return Object.keys(Countries).map((countryCode) => {
|
||||
return {
|
||||
...Countries[countryCode],
|
||||
countryCode,
|
||||
}
|
||||
});
|
||||
};
|
||||
38
playwright.config.ts
Normal file
38
playwright.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import path from 'path';
|
||||
import { PlaywrightTestConfig, devices } from '@playwright/test';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Reference: https://playwright.dev/docs/test-configuration
|
||||
const config: PlaywrightTestConfig = {
|
||||
// Timeout per test
|
||||
timeout: 60 * 1000,
|
||||
workers: 1,
|
||||
// Test directory
|
||||
testDir: path.join(__dirname, 'e2e'),
|
||||
// If a test fails, retry it additional 2 times
|
||||
retries: 0,
|
||||
// Artifacts folder where screenshots, videos, and traces are stored.
|
||||
outputDir: 'test-results/',
|
||||
use: {
|
||||
// Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc.
|
||||
// More information: https://playwright.dev/docs/trace-viewer
|
||||
trace: 'retain-on-failure',
|
||||
|
||||
// All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context
|
||||
// contextOptions: {
|
||||
// ignoreHTTPSErrors: true,
|
||||
// },
|
||||
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:4000',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'Desktop Chrome',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
export default config;
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"installCommand": "npm install && npm run bootstrap",
|
||||
"buildCommand": "CI='' npm run build:webapp",
|
||||
"outputDirectory": "packages/webapp/build"
|
||||
}
|
||||
"outputDirectory": "packages/webapp/build",
|
||||
"rewrites": [{
|
||||
"source": "/(.*)",
|
||||
"destination": "/"
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user