mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
Compare commits
17 Commits
agpl
...
docker-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012b13ad4a | ||
|
|
ad8770f12c | ||
|
|
c6cdbe11e6 | ||
|
|
308980604a | ||
|
|
32148a3207 | ||
|
|
fe270b3703 | ||
|
|
950b5407c3 | ||
|
|
e4a647376c | ||
|
|
85b24c7a4f | ||
|
|
4a22576d88 | ||
|
|
d1ab64e9bd | ||
|
|
110fdbaa4e | ||
|
|
961ff74880 | ||
|
|
da20b7c837 | ||
|
|
a5c190e094 | ||
|
|
7177276b12 | ||
|
|
65bb3a1cb8 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
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
|
## [0.8.1] - 26-03-2023
|
||||||
|
|
||||||
`@bigcaptial/monorepo`
|
`@bigcaptial/monorepo`
|
||||||
|
|||||||
@@ -77,24 +77,26 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: docker/migration/Dockerfile
|
dockerfile: docker/migration/Dockerfile
|
||||||
args:
|
environment:
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_USER=${DB_USER}
|
- DB_USER=${DB_USER}
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- DB_CHARSET=${DB_CHARSET}
|
- DB_CHARSET=${DB_CHARSET}
|
||||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
container_name: bigcapital-mysql
|
container_name: bigcapital-mysql
|
||||||
build:
|
build:
|
||||||
context: ./docker/mysql
|
context: ./docker/mysql
|
||||||
args:
|
environment:
|
||||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||||
- MYSQL_USER=${DB_NAME}
|
- MYSQL_USER=${DB_USER}
|
||||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mysql/:/var/lib/mysql
|
- mysql:/var/lib/mysql
|
||||||
expose:
|
expose:
|
||||||
- '3306'
|
- '3306'
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- '27017'
|
- '27017'
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mongo/:/var/lib/mongodb
|
- mongo:/var/lib/mongodb
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: bigcapital-redis
|
container_name: bigcapital-redis
|
||||||
@@ -113,4 +115,18 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "6379"
|
- "6379"
|
||||||
volumes:
|
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:
|
mysql:
|
||||||
build:
|
build:
|
||||||
context: ./docker/mysql
|
context: ./docker/mysql
|
||||||
args:
|
environment:
|
||||||
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
- MYSQL_DATABASE=${SYSTEM_DB_NAME}
|
||||||
- MYSQL_USER=${DB_NAME}
|
- MYSQL_USER=${DB_USER}
|
||||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mysql/:/var/lib/mysql
|
- mysql:/var/lib/mysql
|
||||||
expose:
|
expose:
|
||||||
- '3306'
|
- '3306'
|
||||||
ports:
|
ports:
|
||||||
@@ -26,7 +26,7 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- '27017'
|
- '27017'
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/mongo/:/var/lib/mongodb
|
- mongo:/var/lib/mongodb
|
||||||
ports:
|
ports:
|
||||||
- '27017:27017'
|
- '27017:27017'
|
||||||
|
|
||||||
@@ -36,4 +36,18 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "6379"
|
- "6379"
|
||||||
volumes:
|
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
|
FROM mysql:5.7
|
||||||
|
|
||||||
|
USER root
|
||||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
||||||
|
|
||||||
RUN chown -R mysql:root /var/lib/mysql/
|
|
||||||
|
|
||||||
ARG MYSQL_DATABASE=default_database
|
ARG MYSQL_DATABASE=default_database
|
||||||
ARG MYSQL_USER=default_user
|
ARG MYSQL_USER=default_user
|
||||||
ARG MYSQL_PASSWORD=secret
|
ARG MYSQL_PASSWORD=secret
|
||||||
@@ -14,5 +13,14 @@ ENV MYSQL_USER=$MYSQL_USER
|
|||||||
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
|
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
|
||||||
ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_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"]
|
CMD ["mysqld"]
|
||||||
EXPOSE 3306
|
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;
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
import { Request, Response, Router } from 'express';
|
import { Request, Response, Router } from 'express';
|
||||||
import { check, ValidationChain } from 'express-validator';
|
import { check, ValidationChain } from 'express-validator';
|
||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import countries from 'country-codes-list';
|
|
||||||
import parsePhoneNumber from 'libphonenumber-js';
|
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import AuthenticationService from '@/services/Authentication';
|
|
||||||
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
||||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||||
import config from '@/config';
|
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AuthenticationController extends BaseController {
|
export default class AuthenticationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
authService: AuthenticationService;
|
private authApplication: AuthenticationApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
*/
|
*/
|
||||||
router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post(
|
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 [
|
return [
|
||||||
check('crediential').exists().isEmail(),
|
check('crediential').exists().isEmail(),
|
||||||
check('password').exists().isLength({ min: 5 }),
|
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 [
|
return [
|
||||||
check('first_name')
|
check('first_name')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -89,71 +88,20 @@ export default class AuthenticationController extends BaseController {
|
|||||||
.trim()
|
.trim()
|
||||||
.escape()
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('phone_number')
|
|
||||||
.exists()
|
|
||||||
.isString()
|
|
||||||
.trim()
|
|
||||||
.escape()
|
|
||||||
.custom(this.phoneNumberValidator)
|
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
|
||||||
check('password')
|
check('password')
|
||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
.escape()
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.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.
|
* Reset password schema.
|
||||||
|
* @returns {ValidationChain[]}
|
||||||
*/
|
*/
|
||||||
get resetPasswordSchema(): ValidationChain[] {
|
private get resetPasswordSchema(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
check('password')
|
check('password')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -170,8 +118,9 @@ export default class AuthenticationController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send reset password validation schema.
|
* Send reset password validation schema.
|
||||||
|
* @returns {ValidationChain[]}
|
||||||
*/
|
*/
|
||||||
get sendResetPasswordSchema(): ValidationChain[] {
|
private get sendResetPasswordSchema(): ValidationChain[] {
|
||||||
return [check('email').exists().isEmail().trim().escape()];
|
return [check('email').exists().isEmail().trim().escape()];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +129,11 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @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);
|
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { token, user, tenant } = await this.authService.signIn(
|
const { token, user, tenant } = await this.authApplication.signIn(
|
||||||
userDTO.crediential,
|
userDTO.crediential,
|
||||||
userDTO.password
|
userDTO.password
|
||||||
);
|
);
|
||||||
@@ -199,13 +148,11 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @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);
|
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registeredUser: ISystemUser = await this.authService.register(
|
await this.authApplication.signUp(registerDTO);
|
||||||
registerDTO
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -222,11 +169,11 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @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);
|
const { email } = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.authService.sendResetPassword(email);
|
await this.authApplication.sendResetPassword(email);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||||
@@ -244,12 +191,12 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @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 { token } = req.params;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.authService.resetPassword(token, password);
|
await this.authApplication.resetPassword(token, password);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'RESET_PASSWORD_SUCCESS',
|
type: 'RESET_PASSWORD_SUCCESS',
|
||||||
@@ -263,7 +210,7 @@ export default class AuthenticationController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* Handles the service errors.
|
* 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 (error instanceof ServiceError) {
|
||||||
if (
|
if (
|
||||||
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
|
['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 }],
|
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
if (error.errorType === 'EMAIL_EXISTS') {
|
||||||
if (error instanceof ServiceErrors) {
|
return res.status(400).send({
|
||||||
const errorReasons = [];
|
errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
|
||||||
|
});
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import AcceptInviteUserService from '@/services/InviteUsers/AcceptInviteUser';
|
|||||||
@Service()
|
@Service()
|
||||||
export default class InviteUsersController extends BaseController {
|
export default class InviteUsersController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
inviteUsersService: InviteTenantUserService;
|
private inviteUsersService: InviteTenantUserService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
acceptInviteService: AcceptInviteUserService;
|
private acceptInviteService: AcceptInviteUserService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes that require authentication.
|
* Routes that require authentication.
|
||||||
@@ -68,13 +68,13 @@ export default class InviteUsersController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite DTO schema validation.
|
* Invite DTO schema validation.
|
||||||
|
* @returns {ValidationChain[]}
|
||||||
*/
|
*/
|
||||||
get inviteUserDTO() {
|
private get inviteUserDTO() {
|
||||||
return [
|
return [
|
||||||
check('first_name').exists().trim().escape(),
|
check('first_name').exists().trim().escape(),
|
||||||
check('last_name').exists().trim().escape(),
|
check('last_name').exists().trim().escape(),
|
||||||
check('phone_number').exists().trim().escape(),
|
check('password').exists().trim().escape().isLength({ min: 5 }),
|
||||||
check('password').exists().trim().escape(),
|
|
||||||
param('token').exists().trim().escape(),
|
param('token').exists().trim().escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -85,17 +85,13 @@ export default class InviteUsersController extends BaseController {
|
|||||||
* @param {Response} res - Response object.
|
* @param {Response} res - Response object.
|
||||||
* @param {NextFunction} next - Next function.
|
* @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 sendInviteDTO = this.matchedBodyData(req);
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { invite } = await this.inviteUsersService.sendInvite(
|
await this.inviteUsersService.sendInvite(tenantId, sendInviteDTO, user);
|
||||||
tenantId,
|
|
||||||
sendInviteDTO,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||||
@@ -112,7 +108,7 @@ export default class InviteUsersController extends BaseController {
|
|||||||
* @param {Response} res - Response object.
|
* @param {Response} res - Response object.
|
||||||
* @param {NextFunction} next - Next function.
|
* @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 { tenantId, user } = req;
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
|
|
||||||
@@ -135,7 +131,7 @@ export default class InviteUsersController extends BaseController {
|
|||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
* @param {NextFunction} next -
|
* @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, {
|
const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, {
|
||||||
locations: ['body'],
|
locations: ['body'],
|
||||||
includeOptionals: true,
|
includeOptionals: true,
|
||||||
@@ -161,7 +157,7 @@ export default class InviteUsersController extends BaseController {
|
|||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
* @param {NextFunction} next -
|
* @param {NextFunction} next -
|
||||||
*/
|
*/
|
||||||
async invited(req: Request, res: Response, next: Function) {
|
private async invited(req: Request, res: Response, next: Function) {
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -181,7 +177,12 @@ export default class InviteUsersController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* Handles the service error.
|
* 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 instanceof ServiceError) {
|
||||||
if (error.errorType === 'EMAIL_EXISTS') {
|
if (error.errorType === 'EMAIL_EXISTS') {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
|
|||||||
@@ -8,18 +8,12 @@ import JWTAuth from '@/api/middleware/jwtAuth';
|
|||||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||||
import {
|
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
|
||||||
ACCEPTED_CURRENCIES,
|
|
||||||
MONTHS,
|
|
||||||
ACCEPTED_LOCALES,
|
|
||||||
} from '@/services/Organization/constants';
|
|
||||||
import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants';
|
import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants';
|
||||||
|
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
|
|
||||||
const ACCEPTED_LOCATIONS = ['libya'];
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class OrganizationController extends BaseController {
|
export default class OrganizationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -65,8 +59,8 @@ export default class OrganizationController extends BaseController {
|
|||||||
return [
|
return [
|
||||||
check('name').exists().trim(),
|
check('name').exists().trim(),
|
||||||
check('industry').optional().isString(),
|
check('industry').optional().isString(),
|
||||||
check('location').exists().isString().isIn(ACCEPTED_LOCATIONS),
|
check('location').exists().isString().isISO31661Alpha2(),
|
||||||
check('base_currency').exists().isIn(ACCEPTED_CURRENCIES),
|
check('base_currency').exists().isISO4217(),
|
||||||
check('timezone').exists().isIn(moment.tz.names()),
|
check('timezone').exists().isIn(moment.tz.names()),
|
||||||
check('fiscal_year').exists().isIn(MONTHS),
|
check('fiscal_year').exists().isIn(MONTHS),
|
||||||
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
|
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export default class UsersController extends BaseController {
|
|||||||
check('first_name').exists(),
|
check('first_name').exists(),
|
||||||
check('last_name').exists(),
|
check('last_name').exists(),
|
||||||
check('email').exists().isEmail(),
|
check('email').exists().isEmail(),
|
||||||
check('phone_number').optional().isMobilePhone(),
|
|
||||||
check('role_id').exists().isNumeric().toInt(),
|
check('role_id').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import color from 'colorette';
|
|||||||
import argv from 'getopts';
|
import argv from 'getopts';
|
||||||
import Knex from 'knex';
|
import Knex from 'knex';
|
||||||
import { knexSnakeCaseMappers } from 'objection';
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
|
import '../before';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
function initSystemKnex() {
|
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) => {});
|
||||||
|
};
|
||||||
@@ -1,29 +1,77 @@
|
|||||||
import { ISystemUser } from './User';
|
import { ISystemUser } from './User';
|
||||||
import { ITenant } from './Tenancy';
|
import { ITenant } from './Tenancy';
|
||||||
|
import { SystemUser } from '@/system/models';
|
||||||
|
|
||||||
export interface IRegisterDTO {
|
export interface IRegisterDTO {
|
||||||
firstName: string,
|
firstName: string;
|
||||||
lastName: string,
|
lastName: string;
|
||||||
email: string,
|
email: string;
|
||||||
password: string,
|
password: string;
|
||||||
organizationName: string,
|
organizationName: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface ILoginDTO {
|
export interface ILoginDTO {
|
||||||
crediential: string,
|
crediential: string;
|
||||||
password: string,
|
password: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface IPasswordReset {
|
export interface IPasswordReset {
|
||||||
id: number,
|
id: number;
|
||||||
email: string,
|
email: string;
|
||||||
token: string,
|
token: string;
|
||||||
createdAt: Date,
|
createdAt: Date;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface IAuthenticationService {
|
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>;
|
register(registerDTO: IRegisterDTO): Promise<ISystemUser>;
|
||||||
sendResetPassword(email: string): Promise<IPasswordReset>;
|
sendResetPassword(email: string): Promise<IPasswordReset>;
|
||||||
resetPassword(token: string, password: string): Promise<void>;
|
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;
|
active: boolean;
|
||||||
password: string;
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
phoneNumber: string;
|
|
||||||
|
|
||||||
roleId: number;
|
roleId: number;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
@@ -26,7 +25,6 @@ export interface ISystemUserDTO {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
password: string;
|
password: string;
|
||||||
phoneNumber: string;
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
roleId?: number;
|
roleId?: number;
|
||||||
@@ -35,7 +33,6 @@ export interface ISystemUserDTO {
|
|||||||
export interface IEditUserDTO {
|
export interface IEditUserDTO {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
phoneNumber: string;
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
roleId: number;
|
roleId: number;
|
||||||
@@ -44,7 +41,6 @@ export interface IEditUserDTO {
|
|||||||
export interface IInviteUserInput {
|
export interface IInviteUserInput {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
phoneNumber: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
export interface IUserInvite {
|
export interface IUserInvite {
|
||||||
@@ -111,7 +107,6 @@ export interface ITenantUser {
|
|||||||
id?: number;
|
id?: number;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
phoneNumber: string;
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
email: string;
|
email: string;
|
||||||
roleId?: number;
|
roleId?: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Container, Inject } from 'typedi';
|
import { Container, Inject } from 'typedi';
|
||||||
import AuthenticationService from '@/services/Authentication';
|
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
export default class WelcomeEmailJob {
|
export default class WelcomeEmailJob {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Container, Inject } from 'typedi';
|
import { Container, Inject } from 'typedi';
|
||||||
import AuthenticationService from '@/services/Authentication';
|
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
export default class WelcomeSMSJob {
|
export default class WelcomeSMSJob {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import AuthenticationService from '@/services/Authentication';
|
import AuthenticationService from '@/services/Authentication/AuthApplication';
|
||||||
|
|
||||||
export default class WelcomeEmailJob {
|
export default class WelcomeEmailJob {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ import moment from 'moment';
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { Invite, SystemUser, Tenant } from '@/system/models';
|
import { Invite, SystemUser, Tenant } from '@/system/models';
|
||||||
import { hashPassword } from 'utils';
|
import { hashPassword } from 'utils';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages';
|
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import {
|
import {
|
||||||
IAcceptInviteEventPayload,
|
IAcceptInviteEventPayload,
|
||||||
@@ -12,29 +10,13 @@ import {
|
|||||||
ICheckInviteEventPayload,
|
ICheckInviteEventPayload,
|
||||||
IUserInvite,
|
IUserInvite,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
|
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AcceptInviteUserService {
|
export default class AcceptInviteUserService {
|
||||||
@Inject()
|
@Inject()
|
||||||
eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
tenancy: TenancyService;
|
|
||||||
|
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
mailMessages: InviteUsersMailMessages;
|
|
||||||
|
|
||||||
@Inject('repositories')
|
|
||||||
sysRepositories: any;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
tenantsManager: TenantsManagerService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept the received invite.
|
* Accept the received invite.
|
||||||
@@ -50,9 +32,6 @@ export default class AcceptInviteUserService {
|
|||||||
// Retrieve the invite token or throw not found error.
|
// Retrieve the invite token or throw not found error.
|
||||||
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
const inviteToken = await this.getInviteTokenOrThrowError(token);
|
||||||
|
|
||||||
// Validates the user phone number.
|
|
||||||
await this.validateUserPhoneNumberNotExists(inviteUserDTO.phoneNumber);
|
|
||||||
|
|
||||||
// Hash the given password.
|
// Hash the given password.
|
||||||
const hashedPassword = await hashPassword(inviteUserDTO.password);
|
const hashedPassword = await hashPassword(inviteUserDTO.password);
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export const DATE_FORMATS = [
|
|||||||
'MMMM dd, YYYY',
|
'MMMM dd, YYYY',
|
||||||
'EEE, MMMM dd, YYYY',
|
'EEE, MMMM dd, YYYY',
|
||||||
];
|
];
|
||||||
export const ACCEPTED_CURRENCIES = Object.keys(currencies);
|
|
||||||
|
|
||||||
export const MONTHS = [
|
export const MONTHS = [
|
||||||
'january',
|
'january',
|
||||||
'february',
|
'february',
|
||||||
|
|||||||
@@ -17,20 +17,17 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
|||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class UsersService {
|
export default class UsersService {
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
@Inject('repositories')
|
@Inject('repositories')
|
||||||
repositories: any;
|
private repositories: any;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
rolesService: RolesService;
|
private rolesService: RolesService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new user.
|
* Creates a new user.
|
||||||
@@ -46,7 +43,7 @@ export default class UsersService {
|
|||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { User } = this.tenancy.models(tenantId);
|
const { User } = this.tenancy.models(tenantId);
|
||||||
const { email, phoneNumber } = editUserDTO;
|
const { email } = editUserDTO;
|
||||||
|
|
||||||
// Retrieve the tenant user or throw not found service error.
|
// Retrieve the tenant user or throw not found service error.
|
||||||
const oldTenantUser = await this.getTenantUserOrThrowError(
|
const oldTenantUser = await this.getTenantUserOrThrowError(
|
||||||
@@ -62,9 +59,6 @@ export default class UsersService {
|
|||||||
// Validate user email should be unique.
|
// Validate user email should be unique.
|
||||||
await this.validateUserEmailUniquiness(tenantId, email, userId);
|
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.
|
// Retrieve the given role or throw not found service error.
|
||||||
const role = await this.rolesService.getRoleOrThrowError(
|
const role = await this.rolesService.getRoleOrThrowError(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -295,27 +289,6 @@ 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.
|
* Validate the authorized user cannot mutate its role.
|
||||||
* @param {ITenantUser} oldTenantUser
|
* @param {ITenantUser} oldTenantUser
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
import { IAuthSignedInEventPayload } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ResetLoginThrottleSubscriber {
|
export default class ResetLoginThrottleSubscriber {
|
||||||
@@ -8,20 +9,21 @@ export default class ResetLoginThrottleSubscriber {
|
|||||||
* @param bus
|
* @param bus
|
||||||
*/
|
*/
|
||||||
public attach(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.
|
* Resets the login throttle once the login success.
|
||||||
|
* @param {IAuthSignedInEventPayload} payload -
|
||||||
*/
|
*/
|
||||||
private async resetLoginThrottleOnceSuccessLogin(payload) {
|
private async resetLoginThrottleOnceSuccessLogin(
|
||||||
const { emailOrPhone, password, user } = payload;
|
payload: IAuthSignedInEventPayload
|
||||||
|
) {
|
||||||
|
const { email, user } = payload;
|
||||||
const loginThrottler = Container.get('rateLimiter.login');
|
const loginThrottler = Container.get('rateLimiter.login');
|
||||||
|
|
||||||
// Reset the login throttle by the given email and phone number.
|
// Reset the login throttle by the given email and phone number.
|
||||||
await loginThrottler.reset(user.email);
|
await loginThrottler.reset(user.email);
|
||||||
await loginThrottler.reset(user.phoneNumber);
|
await loginThrottler.reset(email);
|
||||||
await loginThrottler.reset(emailOrPhone);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ export default class AuthSendWelcomeMailSubscriber {
|
|||||||
* Attaches events with handlers.
|
* Attaches events with handlers.
|
||||||
*/
|
*/
|
||||||
public attach(bus) {
|
public attach(bus) {
|
||||||
bus.subscribe(events.auth.register, this.sendWelcomeEmailOnceUserRegister);
|
bus.subscribe(events.auth.signUp, this.sendWelcomeEmailOnceUserRegister);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends welcome email once the user register.
|
* Sends welcome email once the user register.
|
||||||
*/
|
*/
|
||||||
private sendWelcomeEmailOnceUserRegister = async (payload) => {
|
private sendWelcomeEmailOnceUserRegister = async (payload) => {
|
||||||
const { registerDTO, tenant, user } = payload;
|
const { tenant, user } = payload;
|
||||||
|
|
||||||
// Send welcome mail to the user.
|
// Send welcome mail to the user.
|
||||||
await this.agenda.now('welcome-email', {
|
await this.agenda.now('welcome-email', {
|
||||||
|
|||||||
@@ -3,10 +3,17 @@ export default {
|
|||||||
* Authentication service.
|
* Authentication service.
|
||||||
*/
|
*/
|
||||||
auth: {
|
auth: {
|
||||||
login: 'onLogin',
|
signIn: 'onSignIn',
|
||||||
register: 'onRegister',
|
signingIn: 'onSigningIn',
|
||||||
|
|
||||||
|
signUp: 'onSignUp',
|
||||||
|
signingUp: 'onSigningUp',
|
||||||
|
|
||||||
|
sendingResetPassword: 'onSendingResetPassword',
|
||||||
sendResetPassword: 'onSendResetPassword',
|
sendResetPassword: 'onSendResetPassword',
|
||||||
|
|
||||||
resetPassword: 'onResetPassword',
|
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 PrivateRoute from '@/components/Guards/PrivateRoute';
|
||||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||||
import Authentication from '@/components/Authentication';
|
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||||
|
|
||||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||||
import { queryConfig } from '../hooks/query/base';
|
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 = () => [
|
export const getLanguages = () => [
|
||||||
{ name: intl.get('english'), value: 'en' },
|
{ name: intl.get('english'), value: 'en' },
|
||||||
{ name: intl.get('arabic'), value: 'ar' },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { Icon } from '@/components/Icon';
|
import { Icon } from '@/components/Icon';
|
||||||
|
|
||||||
export default function AuthCopyright() {
|
export default function AuthCopyright() {
|
||||||
return (
|
return <Icon width={122} height={22} icon={'bigcapital'} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import AuthCopyright from './AuthCopyright';
|
import AuthCopyright from './AuthCopyright';
|
||||||
|
import { AuthInsiderContent, AuthInsiderCopyright } from './_components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication insider page.
|
* Authentication insider page.
|
||||||
@@ -9,16 +11,21 @@ export default function AuthInsider({
|
|||||||
logo = true,
|
logo = true,
|
||||||
copyright = true,
|
copyright = true,
|
||||||
children,
|
children,
|
||||||
|
classNames,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div class="authentication-insider__content">
|
<AuthInsiderContent>
|
||||||
<div class="authentication-insider__form">
|
<AuthInsiderContentWrap className={classNames?.content}>
|
||||||
{ children }
|
{children}
|
||||||
</div>
|
</AuthInsiderContentWrap>
|
||||||
|
|
||||||
<div class="authentication-insider__footer">
|
{copyright && (
|
||||||
<AuthCopyright />
|
<AuthInsiderCopyright className={classNames?.copyrightWrap}>
|
||||||
</div>
|
<AuthCopyright />
|
||||||
</div>
|
</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 { Formik } from 'formik';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Intent, Position } from '@blueprintjs/core';
|
import { Intent, Position } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { InviteAcceptSchema } from './utils';
|
import { InviteAcceptSchema } from './utils';
|
||||||
import InviteAcceptFormContent from './InviteAcceptFormContent';
|
import InviteAcceptFormContent from './InviteAcceptFormContent';
|
||||||
|
import { AuthInsiderCard } from './_components';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
organization_name: '',
|
||||||
|
invited_email: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
password: '',
|
||||||
|
};
|
||||||
|
|
||||||
export default function InviteAcceptForm() {
|
export default function InviteAcceptForm() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -19,9 +27,8 @@ export default function InviteAcceptForm() {
|
|||||||
const { inviteAcceptMutate, inviteMeta, token } = useInviteAcceptContext();
|
const { inviteAcceptMutate, inviteMeta, token } = useInviteAcceptContext();
|
||||||
|
|
||||||
// Invite value.
|
// Invite value.
|
||||||
const inviteValue = {
|
const inviteFormValue = {
|
||||||
organization_name: '',
|
...initialValues,
|
||||||
invited_email: '',
|
|
||||||
...(!isEmpty(inviteMeta)
|
...(!isEmpty(inviteMeta)
|
||||||
? {
|
? {
|
||||||
invited_email: inviteMeta.email,
|
invited_email: inviteMeta.email,
|
||||||
@@ -33,19 +40,17 @@ export default function InviteAcceptForm() {
|
|||||||
// Handle form submitting.
|
// Handle form submitting.
|
||||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
inviteAcceptMutate([values, token])
|
inviteAcceptMutate([values, token])
|
||||||
.then((response) => {
|
.then(() => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.getHTML(
|
message: intl.getHTML(
|
||||||
'congrats_your_account_has_been_created_and_invited',
|
'congrats_your_account_has_been_created_and_invited',
|
||||||
{
|
{
|
||||||
organization_name: inviteValue.organization_name,
|
organization_name: inviteMeta.organizationName,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
});
|
});
|
||||||
history.push('/auth/login');
|
history.push('/auth/login');
|
||||||
setSubmitting(false);
|
|
||||||
})
|
})
|
||||||
.catch(
|
.catch(
|
||||||
({
|
({
|
||||||
@@ -80,23 +85,13 @@ export default function InviteAcceptForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'invite-form'}>
|
<AuthInsiderCard>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={InviteAcceptSchema}
|
validationSchema={InviteAcceptSchema}
|
||||||
initialValues={inviteValue}
|
initialValues={inviteFormValue}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
component={InviteAcceptFormContent}
|
component={InviteAcceptFormContent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AuthInsiderCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +1,73 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
import { Button, InputGroup, Intent } from '@blueprintjs/core';
|
||||||
import { Form, ErrorMessage, FastField, useFormikContext } from 'formik';
|
import { Form, useFormikContext } from 'formik';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Col, Row, FormattedMessage as T } from '@/components';
|
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||||
import { inputIntent } from '@/utils';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
Row,
|
||||||
|
FormattedMessage as T,
|
||||||
|
} from '@/components';
|
||||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||||
import { PasswordRevealer } from './components';
|
import { AuthSubmitButton } from './_components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite user form.
|
* Invite user form.
|
||||||
*/
|
*/
|
||||||
export default function InviteUserFormContent() {
|
export default function InviteUserFormContent() {
|
||||||
// Invite accept context.
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
const { inviteMeta } = useInviteAcceptContext();
|
|
||||||
|
|
||||||
// Formik context.
|
const { inviteMeta } = useInviteAcceptContext();
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
const [passwordType, setPasswordType] = React.useState('password');
|
|
||||||
|
|
||||||
// Handle password revealer changing.
|
// Handle password revealer changing.
|
||||||
const handlePasswordRevealerChange = React.useCallback(
|
const handleLockClick = () => {
|
||||||
(shown) => {
|
setShowPassword(!showPassword);
|
||||||
const type = shown ? 'text' : 'password';
|
};
|
||||||
setPasswordType(type);
|
const lockButton = (
|
||||||
},
|
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||||
[setPasswordType],
|
<Button
|
||||||
|
icon={showPassword ? 'unlock' : 'lock'}
|
||||||
|
intent={Intent.WARNING}
|
||||||
|
minimal={true}
|
||||||
|
onClick={handleLockClick}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
</Tooltip2>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Row>
|
<Row>
|
||||||
<Col md={6}>
|
<Col md={6}>
|
||||||
<FastField name={'first_name'}>
|
<FFormGroup name={'first_name'} label={<T id={'first_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'first_name'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col md={6}>
|
<Col md={6}>
|
||||||
<FastField name={'last_name'}>
|
<FFormGroup name={'last_name'} label={<T id={'last_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'last_name'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<FastField name={'phone_number'}>
|
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup
|
||||||
<FormGroup
|
name={'password'}
|
||||||
label={<T id={'phone_number'} />}
|
large={true}
|
||||||
className={'form-group--phone_number'}
|
rightElement={lockButton}
|
||||||
intent={inputIntent({ error, touched })}
|
type={showPassword ? 'text' : 'password'}
|
||||||
helperText={<ErrorMessage name={'phone_number'} />}
|
/>
|
||||||
>
|
</FFormGroup>
|
||||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
|
|
||||||
<FastField name={'password'}>
|
<InviteAcceptFooterParagraphs>
|
||||||
{({ 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'}>
|
|
||||||
<p>
|
<p>
|
||||||
<T id={'you_email_address_is'} /> <b>{inviteMeta.email},</b> <br />
|
<T id={'you_email_address_is'} /> <b>{inviteMeta.email},</b> <br />
|
||||||
<T id={'you_will_use_this_address_to_sign_in_to_bigcapital'} />
|
<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>,
|
privacy: (msg) => <Link>{msg}</Link>,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</InviteAcceptFooterParagraphs>
|
||||||
|
|
||||||
<div className={'authentication-page__submit-button-wrap'}>
|
<InviteAuthSubmitButton
|
||||||
<Button
|
intent={Intent.PRIMARY}
|
||||||
intent={Intent.PRIMARY}
|
type="submit"
|
||||||
type="submit"
|
fill={true}
|
||||||
fill={true}
|
large={true}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
<T id={'create_account'} />
|
<T id={'create_account'} />
|
||||||
</Button>
|
</InviteAuthSubmitButton>
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const InviteAcceptFooterParagraphs = styled.div`
|
||||||
|
opacity: 0.8;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteAuthSubmitButton = styled(AuthSubmitButton)`
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @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 { useInviteMetaByToken, useAuthInviteAccept } from '@/hooks/query';
|
||||||
import { InviteAcceptLoading } from './components';
|
import { InviteAcceptLoading } from './components';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
const InviteAcceptContext = createContext();
|
const InviteAcceptContext = createContext();
|
||||||
|
|
||||||
@@ -22,11 +22,10 @@ function InviteAcceptProvider({ token, ...props }) {
|
|||||||
const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({
|
const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// History context.
|
// History context.
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (inviteMetaError) { history.push('/auth/login'); }
|
if (inviteMetaError) { history.push('/auth/login'); }
|
||||||
}, [history, inviteMetaError]);
|
}, [history, inviteMetaError]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Formik } from 'formik';
|
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 AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||||
import { useAuthLogin } from '@/hooks/query';
|
import { useAuthLogin } from '@/hooks/query';
|
||||||
|
|
||||||
import LoginForm from './LoginForm';
|
import LoginForm from './LoginForm';
|
||||||
import { LoginSchema, transformLoginErrorsToToasts } from './utils';
|
import { LoginSchema, transformLoginErrorsToToasts } from './utils';
|
||||||
|
import {
|
||||||
|
AuthFooterLinks,
|
||||||
|
AuthFooterLink,
|
||||||
|
AuthInsiderCard,
|
||||||
|
} from './_components';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
crediential: '',
|
||||||
|
password: '',
|
||||||
|
keepLoggedIn: false
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login page.
|
* Login page.
|
||||||
@@ -38,34 +49,32 @@ export default function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthInsider>
|
<AuthInsider>
|
||||||
<div className="login-form">
|
<AuthInsiderCard>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
crediential: '',
|
|
||||||
password: '',
|
|
||||||
}}
|
|
||||||
validationSchema={LoginSchema}
|
validationSchema={LoginSchema}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
component={LoginForm}
|
component={LoginForm}
|
||||||
/>
|
/>
|
||||||
|
</AuthInsiderCard>
|
||||||
|
|
||||||
<div class="authentication-page__footer-links">
|
<LoginFooterLinks />
|
||||||
<Link to={'/auth/send_reset_password'}>
|
|
||||||
<T id={'forget_my_password'} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AuthInsider>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
Button,
|
import { Form } from 'formik';
|
||||||
InputGroup,
|
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||||
Intent,
|
|
||||||
FormGroup,
|
import { FFormGroup, FInputGroup, FCheckbox, T } from '@/components';
|
||||||
Checkbox,
|
import { AuthSubmitButton } from './_components';
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { Form, ErrorMessage, Field } from 'formik';
|
|
||||||
import { T } from '@/components';
|
|
||||||
import { inputIntent } from '@/utils';
|
|
||||||
import { PasswordRevealer } from './components';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login form.
|
* Login form.
|
||||||
*/
|
*/
|
||||||
export default function LoginForm({ isSubmitting }) {
|
export default function LoginForm({ isSubmitting }) {
|
||||||
const [passwordType, setPasswordType] = React.useState('password');
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
// Handle password revealer changing.
|
// Handle password revealer changing.
|
||||||
const handlePasswordRevealerChange = React.useCallback(
|
const handleLockClick = () => {
|
||||||
(shown) => {
|
setShowPassword(!showPassword);
|
||||||
const type = shown ? 'text' : 'password';
|
};
|
||||||
setPasswordType(type);
|
|
||||||
},
|
const lockButton = (
|
||||||
[setPasswordType],
|
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||||
|
<Button
|
||||||
|
icon={showPassword ? 'unlock' : 'lock'}
|
||||||
|
intent={Intent.WARNING}
|
||||||
|
minimal={true}
|
||||||
|
onClick={handleLockClick}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
</Tooltip2>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className={'authentication-page__form'}>
|
<Form>
|
||||||
<Field name={'crediential'}>
|
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'crediential'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
<Field name={'password'}>
|
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup
|
||||||
<FormGroup
|
name={'password'}
|
||||||
label={<T id={'password'} />}
|
large={true}
|
||||||
labelInfo={
|
type={showPassword ? 'text' : 'password'}
|
||||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
rightElement={lockButton}
|
||||||
}
|
/>
|
||||||
intent={inputIntent({ error, touched })}
|
</FFormGroup>
|
||||||
helperText={<ErrorMessage name={'password'} />}
|
|
||||||
className={'form-group--password has-password-revealer'}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
large={true}
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
type={passwordType}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<div className={'login-form__checkbox-section'}>
|
<FCheckbox name={'keepLoggedIn'}>
|
||||||
<Checkbox large={true} className={'checkbox--remember-me'}>
|
<T id={'keep_me_logged_in'} />
|
||||||
<T id={'keep_me_logged_in'} />
|
</FCheckbox>
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'authentication-page__submit-button-wrap'}>
|
<AuthSubmitButton
|
||||||
<Button
|
type={'submit'}
|
||||||
type={'submit'}
|
intent={Intent.PRIMARY}
|
||||||
intent={Intent.PRIMARY}
|
fill={true}
|
||||||
fill={true}
|
large={true}
|
||||||
lang={true}
|
loading={isSubmitting}
|
||||||
loading={isSubmitting}
|
>
|
||||||
>
|
<T id={'log_in'} />
|
||||||
<T id={'log_in'} />
|
</AuthSubmitButton>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication';
|
|||||||
|
|
||||||
import RegisterForm from './RegisterForm';
|
import RegisterForm from './RegisterForm';
|
||||||
import { RegisterSchema, transformRegisterErrorsToForm } from './utils';
|
import { RegisterSchema, transformRegisterErrorsToForm } from './utils';
|
||||||
|
import {
|
||||||
|
AuthFooterLinks,
|
||||||
|
AuthFooterLink,
|
||||||
|
AuthInsiderCard,
|
||||||
|
} from './_components';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register form.
|
* Register form.
|
||||||
@@ -19,18 +31,6 @@ export default function RegisterUserForm() {
|
|||||||
const { mutateAsync: authLoginMutate } = useAuthLogin();
|
const { mutateAsync: authLoginMutate } = useAuthLogin();
|
||||||
const { mutateAsync: authRegisterMutate } = useAuthRegister();
|
const { mutateAsync: authRegisterMutate } = useAuthRegister();
|
||||||
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() => ({
|
|
||||||
first_name: '',
|
|
||||||
last_name: '',
|
|
||||||
email: '',
|
|
||||||
phone_number: '',
|
|
||||||
password: '',
|
|
||||||
country: 'LY',
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
authRegisterMutate(values)
|
authRegisterMutate(values)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -66,24 +66,32 @@ export default function RegisterUserForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthInsider>
|
<AuthInsider>
|
||||||
<div className={'register-form'}>
|
<AuthInsiderCard>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={RegisterSchema}
|
validationSchema={RegisterSchema}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
component={RegisterForm}
|
component={RegisterForm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AuthInsiderCard>
|
||||||
|
|
||||||
|
<RegisterFooterLinks />
|
||||||
</AuthInsider>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import {
|
import { Intent, Button } from '@blueprintjs/core';
|
||||||
Button,
|
|
||||||
InputGroup,
|
|
||||||
Intent,
|
|
||||||
FormGroup,
|
|
||||||
Spinner,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { ErrorMessage, Field, Form } from 'formik';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Row, Col, If } from '@/components';
|
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||||
import { PasswordRevealer } from './components';
|
import styled from 'styled-components';
|
||||||
import { inputIntent } from '@/utils';
|
|
||||||
|
import {
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
FormattedMessage as T,
|
||||||
|
} from '@/components';
|
||||||
|
import { AuthSubmitButton, AuthenticationLoadingOverlay } from './_components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register form.
|
* Register form.
|
||||||
*/
|
*/
|
||||||
export default function RegisterForm({ isSubmitting }) {
|
export default function RegisterForm({ isSubmitting }) {
|
||||||
const [passwordType, setPasswordType] = React.useState('password');
|
const [showPassword, setShowPassword] = React.useState<boolean>(false);
|
||||||
|
|
||||||
// Handle password revealer changing.
|
// Handle password revealer changing.
|
||||||
const handlePasswordRevealerChange = React.useCallback(
|
const handleLockClick = () => {
|
||||||
(shown) => {
|
setShowPassword(!showPassword);
|
||||||
const type = shown ? 'text' : 'password';
|
};
|
||||||
setPasswordType(type);
|
|
||||||
},
|
const lockButton = (
|
||||||
[setPasswordType],
|
<Tooltip2 content={`${showPassword ? 'Hide' : 'Show'} Password`}>
|
||||||
|
<Button
|
||||||
|
icon={showPassword ? 'unlock' : 'lock'}
|
||||||
|
intent={Intent.WARNING}
|
||||||
|
minimal={true}
|
||||||
|
onClick={handleLockClick}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
</Tooltip2>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className={'authentication-page__form'}>
|
<RegisterFormRoot>
|
||||||
<Row className={'name-section'}>
|
<Row className={'name-section'}>
|
||||||
<Col md={6}>
|
<Col md={6}>
|
||||||
<Field name={'first_name'}>
|
<FFormGroup name={'first_name'} label={<T id={'first_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'first_name'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col md={6}>
|
<Col md={6}>
|
||||||
<Field name={'last_name'}>
|
<FFormGroup name={'last_name'} label={<T id={'last_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'last_name'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Field name={'phone_number'}>
|
<FFormGroup name={'email'} label={<T id={'email'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'email'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
<Field name={'email'}>
|
<FFormGroup name={'password'} label={<T id={'password'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup
|
||||||
<FormGroup
|
name={'password'}
|
||||||
label={<T id={'email'} />}
|
type={showPassword ? 'text' : 'password'}
|
||||||
intent={inputIntent({ error, touched })}
|
rightElement={lockButton}
|
||||||
helperText={<ErrorMessage name={'email'} />}
|
large={true}
|
||||||
className={'form-group--email'}
|
/>
|
||||||
>
|
</FFormGroup>
|
||||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field name={'password'}>
|
<TermsConditionsText>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
{intl.getHTML('signing_in_or_creating', {
|
||||||
<FormGroup
|
terms: (msg) => <Link>{msg}</Link>,
|
||||||
label={<T id={'password'} />}
|
privacy: (msg) => <Link>{msg}</Link>,
|
||||||
labelInfo={
|
})}
|
||||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
</TermsConditionsText>
|
||||||
}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<div className={'register-form__agreement-section'}>
|
<AuthSubmitButton
|
||||||
<p>
|
className={'btn-register'}
|
||||||
{intl.getHTML('signing_in_or_creating', {
|
intent={Intent.PRIMARY}
|
||||||
terms: (msg) => <Link>{msg}</Link>,
|
type="submit"
|
||||||
privacy: (msg) => <Link>{msg}</Link>,
|
fill={true}
|
||||||
})}
|
large={true}
|
||||||
</p>
|
loading={isSubmitting}
|
||||||
</div>
|
>
|
||||||
|
<T id={'register'} />
|
||||||
|
</AuthSubmitButton>
|
||||||
|
|
||||||
<div className={'authentication-page__submit-button-wrap'}>
|
{isSubmitting && <AuthenticationLoadingOverlay />}
|
||||||
<Button
|
</RegisterFormRoot>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { Formik } from 'formik';
|
||||||
import { Intent, Position } from '@blueprintjs/core';
|
import { Intent, Position } from '@blueprintjs/core';
|
||||||
import { Link, useParams, useHistory } from 'react-router-dom';
|
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 { useAuthResetPassword } from '@/hooks/query';
|
||||||
|
|
||||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthFooterLink,
|
||||||
|
AuthFooterLinks,
|
||||||
|
AuthInsiderCard,
|
||||||
|
} from './_components';
|
||||||
import ResetPasswordForm from './ResetPasswordForm';
|
import ResetPasswordForm from './ResetPasswordForm';
|
||||||
import { ResetPasswordSchema } from './utils';
|
import { ResetPasswordSchema } from './utils';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Reset password page.
|
* Reset password page.
|
||||||
*/
|
*/
|
||||||
@@ -22,22 +31,13 @@ export default function ResetPassword() {
|
|||||||
// Authentication reset password.
|
// Authentication reset password.
|
||||||
const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword();
|
const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword();
|
||||||
|
|
||||||
// Initial values of the form.
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() => ({
|
|
||||||
password: '',
|
|
||||||
confirm_password: '',
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle the form submitting.
|
// Handle the form submitting.
|
||||||
const handleSubmit = (values, { setSubmitting }) => {
|
const handleSubmit = (values, { setSubmitting }) => {
|
||||||
authResetPasswordMutate([token, values])
|
authResetPasswordMutate([token, values])
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('password_successfully_updated'),
|
message: intl.get('password_successfully_updated'),
|
||||||
intent: Intent.DANGER,
|
intent: Intent.SUCCESS,
|
||||||
position: Position.BOTTOM,
|
position: Position.BOTTOM,
|
||||||
});
|
});
|
||||||
history.push('/auth/login');
|
history.push('/auth/login');
|
||||||
@@ -64,24 +64,30 @@ export default function ResetPassword() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthInsider>
|
<AuthInsider>
|
||||||
<div className={'submit-np-form'}>
|
<AuthInsiderCard>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={ResetPasswordSchema}
|
validationSchema={ResetPasswordSchema}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
component={ResetPasswordForm}
|
component={ResetPasswordForm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AuthInsiderCard>
|
||||||
|
|
||||||
|
<ResetPasswordFooterLinks />
|
||||||
</AuthInsider>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { Form, ErrorMessage, FastField } from 'formik';
|
import { Form } from 'formik';
|
||||||
import { FormattedMessage as T } from '@/components';
|
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||||
import { inputIntent } from '@/utils';
|
import { AuthSubmitButton } from './_components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset password form.
|
* Reset password form.
|
||||||
@@ -11,54 +11,23 @@ import { inputIntent } from '@/utils';
|
|||||||
export default function ResetPasswordForm({ isSubmitting }) {
|
export default function ResetPasswordForm({ isSubmitting }) {
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<FastField name={'password'}>
|
<FFormGroup name={'password'} label={<T id={'new_password'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'password'} type={'password'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
<FastField name={'confirm_password'}>
|
<FFormGroup name={'confirm_password'} label={<T id={'new_password'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'confirm_password'} type={'password'} large={true} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
<div className={'authentication-page__submit-button-wrap'}>
|
<AuthSubmitButton
|
||||||
<Button
|
fill={true}
|
||||||
fill={true}
|
intent={Intent.PRIMARY}
|
||||||
className={'btn-new'}
|
type="submit"
|
||||||
intent={Intent.PRIMARY}
|
loading={isSubmitting}
|
||||||
type="submit"
|
large={true}
|
||||||
loading={isSubmitting}
|
>
|
||||||
>
|
<T id={'submit'} />
|
||||||
<T id={'submit'} />
|
</AuthSubmitButton>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,32 @@ import { Formik } from 'formik';
|
|||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { useAuthSendResetPassword } from '@/hooks/query';
|
import { useAuthSendResetPassword } from '@/hooks/query';
|
||||||
|
|
||||||
import SendResetPasswordForm from './SendResetPasswordForm';
|
import SendResetPasswordForm from './SendResetPasswordForm';
|
||||||
|
import {
|
||||||
|
AuthFooterLink,
|
||||||
|
AuthFooterLinks,
|
||||||
|
AuthInsiderCard,
|
||||||
|
} from './_components';
|
||||||
import {
|
import {
|
||||||
SendResetPasswordSchema,
|
SendResetPasswordSchema,
|
||||||
transformSendResetPassErrorsToToasts,
|
transformSendResetPassErrorsToToasts,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
crediential: '',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send reset password page.
|
* Send reset password page.
|
||||||
*/
|
*/
|
||||||
export default function SendResetPassword({ requestSendResetPassword }) {
|
export default function SendResetPassword({ requestSendResetPassword }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword();
|
const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword();
|
||||||
|
|
||||||
// Initial values.
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() => ({
|
|
||||||
crediential: '',
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle form submitting.
|
// Handle form submitting.
|
||||||
const handleSubmit = (values, { setSubmitting }) => {
|
const handleSubmit = (values, { setSubmitting }) => {
|
||||||
sendResetPasswordMutate({ email: values.crediential })
|
sendResetPasswordMutate({ email: values.crediential })
|
||||||
@@ -61,28 +60,30 @@ export default function SendResetPassword({ requestSendResetPassword }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthInsider>
|
<AuthInsider>
|
||||||
<div className="reset-form">
|
<AuthInsiderCard>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
validationSchema={SendResetPasswordSchema}
|
validationSchema={SendResetPasswordSchema}
|
||||||
component={SendResetPasswordForm}
|
component={SendResetPasswordForm}
|
||||||
/>
|
/>
|
||||||
<div class="authentication-page__footer-links">
|
</AuthInsiderCard>
|
||||||
<Link to="/auth/login">
|
|
||||||
<T id={'return_to_log_in'} />
|
<SendResetPasswordFooterLinks />
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AuthInsider>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { Form, ErrorMessage, FastField } from 'formik';
|
import { Form } from 'formik';
|
||||||
import { FormattedMessage as T } from '@/components';
|
import styled from 'styled-components';
|
||||||
import { inputIntent } from '@/utils';
|
|
||||||
|
import { FInputGroup, FFormGroup, FormattedMessage as T } from '@/components';
|
||||||
|
import { AuthSubmitButton } from './_components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send reset password form.
|
* Send reset password form.
|
||||||
*/
|
*/
|
||||||
export default function SendResetPasswordForm({ isSubmitting }) {
|
export default function SendResetPasswordForm({ isSubmitting }) {
|
||||||
return (
|
return (
|
||||||
<Form className={'send-reset-password'}>
|
<Form>
|
||||||
<FastField name={'crediential'}>
|
<TopParagraph>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
Enter the email address associated with your account and we'll send you
|
||||||
<FormGroup
|
a link to reset your password.
|
||||||
label={<T id={'email_or_phone_number'} />}
|
</TopParagraph>
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
helperText={<ErrorMessage name={'crediential'} />}
|
|
||||||
className={'form-group--crediential'}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
large={true}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
|
|
||||||
<div className={'authentication-page__submit-button-wrap'}>
|
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
||||||
<Button
|
<FInputGroup name={'crediential'} large={true} />
|
||||||
type={'submit'}
|
</FFormGroup>
|
||||||
intent={Intent.PRIMARY}
|
|
||||||
fill={true}
|
<AuthSubmitButton
|
||||||
loading={isSubmitting}
|
type={'submit'}
|
||||||
>
|
intent={Intent.PRIMARY}
|
||||||
<T id={'send_reset_password_mail'} />
|
fill={true}
|
||||||
</Button>
|
large={true}
|
||||||
</div>
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</AuthSubmitButton>
|
||||||
</Form>
|
</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
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ContentLoader from 'react-content-loader';
|
import styled from 'styled-components';
|
||||||
import { If, Icon, FormattedMessage as T } from '@/components';
|
import { AuthInsiderCard } from './_components';
|
||||||
import { saveInvoke } from '@/utils';
|
import { Skeleton } from '@/components';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite accept loading space.
|
* Invite accept loading space.
|
||||||
*/
|
*/
|
||||||
export function InviteAcceptLoading({ isLoading, children, ...props }) {
|
export function InviteAcceptLoading({ isLoading, children }) {
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<ContentLoader
|
<AuthInsiderCard>
|
||||||
speed={2}
|
<Fields>
|
||||||
width={400}
|
<SkeletonField />
|
||||||
height={280}
|
<SkeletonField />
|
||||||
viewBox="0 0 400 280"
|
<SkeletonField />
|
||||||
backgroundColor="#f3f3f3"
|
</Fields>
|
||||||
foregroundColor="#e6e6e6"
|
</AuthInsiderCard>
|
||||||
{...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>
|
|
||||||
) : (
|
) : (
|
||||||
children
|
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({
|
export const LoginSchema = Yup.object().shape({
|
||||||
crediential: Yup.string()
|
crediential: Yup.string().required().email().label(intl.get('email')),
|
||||||
.required()
|
password: Yup.string().required().min(4).label(intl.get('password')),
|
||||||
.email()
|
|
||||||
.label(intl.get('email')),
|
|
||||||
password: Yup.string()
|
|
||||||
.required()
|
|
||||||
.min(4)
|
|
||||||
.label(intl.get('password')),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RegisterSchema = Yup.object().shape({
|
export const RegisterSchema = Yup.object().shape({
|
||||||
first_name: Yup.string()
|
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||||
.required()
|
last_name: Yup.string().required().label(intl.get('last_name_')),
|
||||||
.label(intl.get('first_name_')),
|
email: Yup.string().email().required().label(intl.get('email')),
|
||||||
last_name: Yup.string()
|
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||||
.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')),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ResetPasswordSchema = Yup.object().shape({
|
export const ResetPasswordSchema = Yup.object().shape({
|
||||||
password: Yup.string()
|
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||||
.min(4)
|
|
||||||
.required()
|
|
||||||
.label(intl.get('password')),
|
|
||||||
confirm_password: Yup.string()
|
confirm_password: Yup.string()
|
||||||
.oneOf([Yup.ref('password'), null])
|
.oneOf([Yup.ref('password'), null])
|
||||||
.required()
|
.required()
|
||||||
@@ -59,27 +36,13 @@ export const ResetPasswordSchema = Yup.object().shape({
|
|||||||
|
|
||||||
// Validation schema.
|
// Validation schema.
|
||||||
export const SendResetPasswordSchema = Yup.object().shape({
|
export const SendResetPasswordSchema = Yup.object().shape({
|
||||||
crediential: Yup.string()
|
crediential: Yup.string().required().email().label(intl.get('email')),
|
||||||
.required()
|
|
||||||
.email()
|
|
||||||
.label(intl.get('email')),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const InviteAcceptSchema = Yup.object().shape({
|
export const InviteAcceptSchema = Yup.object().shape({
|
||||||
first_name: Yup.string()
|
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||||
.required()
|
last_name: Yup.string().required().label(intl.get('last_name_')),
|
||||||
.label(intl.get('first_name_')),
|
password: Yup.string().min(4).required().label(intl.get('password')),
|
||||||
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')),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const transformSendResetPassErrorsToToasts = (errors) => {
|
export const transformSendResetPassErrorsToToasts = (errors) => {
|
||||||
@@ -92,7 +55,7 @@ export const transformSendResetPassErrorsToToasts = (errors) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return toastBuilders;
|
return toastBuilders;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const transformLoginErrorsToToasts = (errors) => {
|
export const transformLoginErrorsToToasts = (errors) => {
|
||||||
const toastBuilders = [];
|
const toastBuilders = [];
|
||||||
@@ -109,25 +72,25 @@ export const transformLoginErrorsToToasts = (errors) => {
|
|||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)) {
|
||||||
errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)
|
|
||||||
) {
|
|
||||||
toastBuilders.push({
|
toastBuilders.push({
|
||||||
message: intl.get('your_account_has_been_locked'),
|
message: intl.get('your_account_has_been_locked'),
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return toastBuilders;
|
return toastBuilders;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const transformRegisterErrorsToForm = (errors) => {
|
export const transformRegisterErrorsToForm = (errors) => {
|
||||||
const formErrors = {};
|
const formErrors = {};
|
||||||
|
|
||||||
if (errors.some((e) => e.type === REGISTER_ERRORS.PHONE_NUMBER_EXISTS)) {
|
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)) {
|
if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) {
|
||||||
formErrors.email = intl.get('the_email_already_used_in_another_account');
|
formErrors.email = intl.get('the_email_already_used_in_another_account');
|
||||||
}
|
}
|
||||||
return formErrors;
|
return formErrors;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ const Schema = Yup.object().shape({
|
|||||||
email: Yup.string().email().required().label(intl.get('email')),
|
email: Yup.string().email().required().label(intl.get('email')),
|
||||||
first_name: Yup.string().required().label(intl.get('first_name_')),
|
first_name: Yup.string().required().label(intl.get('first_name_')),
|
||||||
last_name: Yup.string().required().label(intl.get('last_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_')),
|
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 { useUserFormContext } from './UserFormProvider';
|
||||||
import { transformErrors } from './utils';
|
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.
|
* User form.
|
||||||
@@ -27,12 +34,9 @@ function UserForm({
|
|||||||
const { dialogName, user, userId, isEditMode, EditUserMutate } =
|
const { dialogName, user, userId, isEditMode, EditUserMutate } =
|
||||||
useUserFormContext();
|
useUserFormContext();
|
||||||
|
|
||||||
const initialValues = {
|
const initialFormValues = {
|
||||||
...(isEditMode &&
|
...initialValues,
|
||||||
pick(
|
...(isEditMode && transformToForm(user, initialValues)),
|
||||||
objectKeysTransform(user, snakeCase),
|
|
||||||
Object.keys(UserFormSchema.fields),
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
@@ -68,7 +72,7 @@ function UserForm({
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={UserFormSchema}
|
validationSchema={UserFormSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialFormValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<UserFormContent calloutCode={calloutCode} />
|
<UserFormContent calloutCode={calloutCode} />
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { FastField, Form, useFormikContext, ErrorMessage } from 'formik';
|
import { FastField, Form, useFormikContext, ErrorMessage } from 'formik';
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { inputIntent } from '@/utils';
|
import { inputIntent } from '@/utils';
|
||||||
import { ListSelect, FieldRequiredHint } from '@/components';
|
import { ListSelect, FieldRequiredHint } from '@/components';
|
||||||
import { useUserFormContext } from './UserFormProvider';
|
import { useUserFormContext } from './UserFormProvider';
|
||||||
@@ -23,6 +24,7 @@ import { UserFormCalloutAlerts } from './components';
|
|||||||
*/
|
*/
|
||||||
function UserFormContent({
|
function UserFormContent({
|
||||||
calloutCode,
|
calloutCode,
|
||||||
|
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
@@ -39,60 +41,20 @@ function UserFormContent({
|
|||||||
<UserFormCalloutAlerts calloutCodes={calloutCode} />
|
<UserFormCalloutAlerts calloutCodes={calloutCode} />
|
||||||
|
|
||||||
{/* ----------- Email ----------- */}
|
{/* ----------- Email ----------- */}
|
||||||
<FastField name={'email'}>
|
<FFormGroup name={'email'} label={<T id={'email'} />}>
|
||||||
{({ field, meta: { error, touched } }) => (
|
<FInputGroup name={'email'} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* ----------- First name ----------- */}
|
{/* ----------- First name ----------- */}
|
||||||
<FastField name={'first_name'}>
|
<FFormGroup name={'first_name'} label={<T id={'first_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'first_name'} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
label={<T id={'first_name'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
helperText={<ErrorMessage name={'first_name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
|
|
||||||
{/* ----------- Last name ----------- */}
|
{/* ----------- Last name ----------- */}
|
||||||
<FastField name={'last_name'}>
|
<FFormGroup name={'last_name'} label={<T id={'last_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'last_name'} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
{/* ----------- Role name ----------- */}
|
{/* ----------- Role name ----------- */}
|
||||||
<FastField name={'role_id'}>
|
<FastField name={'role_id'}>
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||||
@@ -127,7 +89,12 @@ function UserFormContent({
|
|||||||
<T id={'cancel'} />
|
<T id={'cancel'} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
<T id={'edit'} />
|
<T id={'edit'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { inputIntent } from '@/utils';
|
import { inputIntent } from '@/utils';
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { getCountries } from '@/constants/countries';
|
|
||||||
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
||||||
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
||||||
import { getLanguages } from '@/constants/languagesOptions';
|
import { getLanguages } from '@/constants/languagesOptions';
|
||||||
import { useGeneralFormContext } from './GeneralFormProvider';
|
import { useGeneralFormContext } from './GeneralFormProvider';
|
||||||
|
import { getAllCountries } from '@/utils/countries';
|
||||||
|
|
||||||
import { shouldBaseCurrencyUpdate } from './utils';
|
import { shouldBaseCurrencyUpdate } from './utils';
|
||||||
|
|
||||||
|
const Countries = getAllCountries();
|
||||||
/**
|
/**
|
||||||
* Preferences general form.
|
* Preferences general form.
|
||||||
*/
|
*/
|
||||||
@@ -30,7 +31,6 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const FiscalYear = getFiscalYear();
|
const FiscalYear = getFiscalYear();
|
||||||
const Countries = getCountries();
|
|
||||||
const Languages = getLanguages();
|
const Languages = getLanguages();
|
||||||
const Currencies = getAllCurrenciesOptions();
|
const Currencies = getAllCurrenciesOptions();
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Intent,
|
Intent,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
InputGroup,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Classes,
|
Classes,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { TimezonePicker } from '@blueprintjs/timezone';
|
import { TimezonePicker } from '@blueprintjs/timezone';
|
||||||
import useAutofocus from '@/hooks/useAutofocus'
|
import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components';
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { getCountries } from '@/constants/countries';
|
|
||||||
|
|
||||||
import { Col, Row, ListSelect } from '@/components';
|
import { Col, Row, ListSelect } from '@/components';
|
||||||
import { inputIntent } from '@/utils';
|
import { inputIntent } from '@/utils';
|
||||||
@@ -21,6 +18,9 @@ import { inputIntent } from '@/utils';
|
|||||||
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
||||||
import { getLanguages } from '@/constants/languagesOptions';
|
import { getLanguages } from '@/constants/languagesOptions';
|
||||||
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
import { getAllCurrenciesOptions } from '@/constants/currencies';
|
||||||
|
import { getAllCountries } from '@/utils/countries';
|
||||||
|
|
||||||
|
const countries = getAllCountries();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup organization form.
|
* Setup organization form.
|
||||||
@@ -29,9 +29,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
|||||||
const FiscalYear = getFiscalYear();
|
const FiscalYear = getFiscalYear();
|
||||||
const Languages = getLanguages();
|
const Languages = getLanguages();
|
||||||
const currencies = getAllCurrenciesOptions();
|
const currencies = getAllCurrenciesOptions();
|
||||||
const countries = getCountries();
|
|
||||||
|
|
||||||
const accountRef = useAutofocus();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
@@ -40,22 +37,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* ---------- Organization name ---------- */}
|
{/* ---------- Organization name ---------- */}
|
||||||
<FastField name={'name'}>
|
<FFormGroup name={'name'} label={<T id={'legal_organization_name'} />}>
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
<FInputGroup name={'name'} />
|
||||||
<FormGroup
|
</FFormGroup>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* ---------- Location ---------- */}
|
{/* ---------- Location ---------- */}
|
||||||
<FastField name={'location'}>
|
<FastField name={'location'}>
|
||||||
@@ -71,11 +55,11 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
|||||||
>
|
>
|
||||||
<ListSelect
|
<ListSelect
|
||||||
items={countries}
|
items={countries}
|
||||||
onItemSelect={({ value }) => {
|
onItemSelect={({ countryCode }) => {
|
||||||
form.setFieldValue('location', value);
|
form.setFieldValue('location', countryCode);
|
||||||
}}
|
}}
|
||||||
selectedItem={value}
|
selectedItem={value}
|
||||||
selectedItemProp={'value'}
|
selectedItemProp={'countryCode'}
|
||||||
defaultText={<T id={'select_business_location'} />}
|
defaultText={<T id={'select_business_location'} />}
|
||||||
textProp={'name'}
|
textProp={'name'}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { getSetupOrganizationValidation } from './SetupOrganization.schema';
|
|||||||
// Initial values.
|
// Initial values.
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: '',
|
name: '',
|
||||||
location: 'libya',
|
location: '',
|
||||||
baseCurrency: '',
|
baseCurrency: '',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
fiscalYear: '',
|
fiscalYear: '',
|
||||||
|
|||||||
@@ -31,13 +31,14 @@
|
|||||||
"phone_number": "Phone Number",
|
"phone_number": "Phone Number",
|
||||||
"you_email_address_is": "You email address is",
|
"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.",
|
"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",
|
"and": "And",
|
||||||
"create_account": "Create Account",
|
"create_account": "Create Account",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"register_a_new_organization": "Register a New Organization.",
|
"register_a_new_organization": "Register a New Organization.",
|
||||||
"organization_name": "Organization Name",
|
"organization_name": "Organization Name",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email_address": "Email Address",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"password_successfully_updated": "The Password for your account was successfully updated.",
|
"password_successfully_updated": "The Password for your account was successfully updated.",
|
||||||
"choose_a_new_password": "Choose a new password",
|
"choose_a_new_password": "Choose a new password",
|
||||||
|
|||||||
@@ -1,45 +1,8 @@
|
|||||||
|
|
||||||
body.authentication {
|
body.authentication {
|
||||||
background-color: #fcfdff;
|
background-color: #fcfdff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authentication-insider {
|
.authTransition {
|
||||||
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{
|
|
||||||
|
|
||||||
&-enter {
|
&-enter {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -61,164 +24,9 @@ body.authentication {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 250ms ease-in-out;
|
transition: opacity 250ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-exit-active {
|
&-exit-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
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;
|
height: 40px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
width: 100%;
|
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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user