Compare commits

...

17 Commits

Author SHA1 Message Date
a.bouhuolia
012b13ad4a add comments 2023-04-07 18:50:45 +02:00
a.bouhuolia
ad8770f12c fix: docker-compose enviroment values 2023-04-07 18:41:03 +02:00
a.bouhuolia
c6cdbe11e6 fix(server): initalize root dirs on CLI 2023-04-06 05:11:20 +02:00
a.bouhuolia
308980604a chore: dump v0.8.3 CHANGELOG 2023-04-06 04:35:37 +02:00
a.bouhuolia
32148a3207 fix(webapp): change intent type of reset password success toast 2023-04-06 04:32:26 +02:00
Ahmed Bouhuolia
fe270b3703 Merge pull request #104 from bigcapitalhq/auth-pages-tweaks
Fix issues in authentication process.
2023-04-06 03:13:03 +02:00
a.bouhuolia
950b5407c3 feat(server): remove the phone number from users management 2023-04-06 03:08:51 +02:00
a.bouhuolia
e4a647376c feat(server): remove the phone number from users service 2023-04-06 03:08:06 +02:00
a.bouhuolia
85b24c7a4f feat(server): remove phone number from authentication endpoints 2023-04-05 23:57:26 +02:00
a.bouhuolia
4a22576d88 Merge branch 'develop' into auth-pages-tweaks 2023-04-05 23:44:41 +02:00
Ahmed Bouhuolia
d1ab64e9bd Merge pull request #102 from bigcapitalhq/named-docker-volumes
feat: configure named docker volumes
2023-04-05 04:39:48 +02:00
Ahmed Bouhuolia
110fdbaa4e Merge pull request #103 from bigcapitalhq/agpl
chore: switch to AGPL to protect networks
2023-04-05 04:39:18 +02:00
a.bouhuolia
961ff74880 feat(server): remove phone number from authentication process 2023-04-05 04:18:12 +02:00
a.bouhuolia
da20b7c837 feat(webapp): add all countries to the setup organization page 2023-04-05 01:30:36 +02:00
a.bouhuolia
a5c190e094 feat(webapp): from phone number fields from authentication pages 2023-04-04 23:51:36 +02:00
a.bouhuolia
7177276b12 feat(webapp): style tweaks to authentication pages 2023-04-04 23:38:04 +02:00
a.bouhuolia
65bb3a1cb8 feat: configure named docker volumes 2023-04-04 00:31:41 +02:00
62 changed files with 3579 additions and 1518 deletions

View File

@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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
View File

@@ -0,0 +1,2 @@
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
FLUSH PRIVILEGES;

View File

@@ -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);

View File

@@ -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({

View File

@@ -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),

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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) => {});
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 {
/** /**

View File

@@ -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 {
/** /**

View File

@@ -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 {
/** /**

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View 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 };
}
}

View 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);
}
}
}

View 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',
};

View 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
);
};

View File

@@ -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
);
}
}

View File

@@ -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);

View File

@@ -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',

View File

@@ -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

View File

@@ -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);
} }
} }

View File

@@ -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', {

View File

@@ -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'
}, },
/** /**

View File

@@ -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) => {});
};

View File

@@ -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';

View File

@@ -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>
)}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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' },
]; ];

View File

@@ -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>
);
} }

View File

@@ -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``;

View File

@@ -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;
`;

View File

@@ -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>
); );
} }

View File

@@ -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;
`;

View File

@@ -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]);

View File

@@ -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>
);
}

View File

@@ -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>
); );
} }

View File

@@ -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>
);
}

View File

@@ -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;
`;

View File

@@ -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>
);
}

View File

@@ -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>
); );
} }

View File

@@ -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>
);
}

View File

@@ -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;
`;

View File

@@ -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);
}
}
`;

View File

@@ -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;
`;

View File

@@ -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;
} };

View File

@@ -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_')),
}); });

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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();

View File

@@ -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 }}

View File

@@ -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: '',

View File

@@ -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",

View File

@@ -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);
}
}
} }

View File

@@ -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);
}
} }
} }
} }

View File

@@ -0,0 +1,10 @@
import { Countries } from '@/constants/countries';
export const getAllCountries = () => {
return Object.keys(Countries).map((countryCode) => {
return {
...Countries[countryCode],
countryCode,
}
});
};