diff --git a/packages/server/src/api/controllers/Authentication.ts b/packages/server/src/api/controllers/Authentication.ts index dbe5b62a1..e0f9073ea 100644 --- a/packages/server/src/api/controllers/Authentication.ts +++ b/packages/server/src/api/controllers/Authentication.ts @@ -1,26 +1,23 @@ import { Request, Response, Router } from 'express'; import { check, ValidationChain } from 'express-validator'; import { Service, Inject } from 'typedi'; -import countries from 'country-codes-list'; -import parsePhoneNumber from 'libphonenumber-js'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; -import AuthenticationService from '@/services/Authentication'; import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces'; import { ServiceError, ServiceErrors } from '@/exceptions'; import { DATATYPES_LENGTH } from '@/data/DataTypes'; import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware'; -import config from '@/config'; +import AuthenticationApplication from '@/services/Authentication/AuthApplication'; @Service() export default class AuthenticationController extends BaseController { @Inject() - authService: AuthenticationService; + private authApplication: AuthenticationApplication; /** * Constructor method. */ - router() { + public router() { const router = Router(); router.post( @@ -56,9 +53,10 @@ export default class AuthenticationController extends BaseController { } /** - * Login schema. + * Login validation schema. + * @returns {ValidationChain[]} */ - get loginSchema(): ValidationChain[] { + private get loginSchema(): ValidationChain[] { return [ check('crediential').exists().isEmail(), check('password').exists().isLength({ min: 5 }), @@ -66,9 +64,10 @@ export default class AuthenticationController extends BaseController { } /** - * Register schema. + * Register validation schema. + * @returns {ValidationChain[]} */ - get registerSchema(): ValidationChain[] { + private get registerSchema(): ValidationChain[] { return [ check('first_name') .exists() @@ -89,71 +88,20 @@ export default class AuthenticationController extends BaseController { .trim() .escape() .isLength({ max: DATATYPES_LENGTH.STRING }), - check('phone_number') - .exists() - .isString() - .trim() - .escape() - .custom(this.phoneNumberValidator) - .isLength({ max: DATATYPES_LENGTH.STRING }), check('password') .exists() .isString() .trim() .escape() .isLength({ max: DATATYPES_LENGTH.STRING }), - check('country') - .exists() - .isString() - .trim() - .escape() - .custom(this.countryValidator) - .isLength({ max: DATATYPES_LENGTH.STRING }), ]; } - /** - * Country validator. - */ - countryValidator(value, { req }) { - const { - countries: { whitelist, blacklist }, - } = config.registration; - const foundCountry = countries.findOne('countryCode', value); - - if (!foundCountry) { - throw new Error('The country code is invalid.'); - } - if ( - // Focus with me! In case whitelist is not empty and the given coutry is not - // in whitelist throw the error. - // - // Or in case the blacklist is not empty and the given country exists - // in the blacklist throw the goddamn error. - (whitelist.length > 0 && whitelist.indexOf(value) === -1) || - (blacklist.length > 0 && blacklist.indexOf(value) !== -1) - ) { - throw new Error('The country code is not supported yet.'); - } - return true; - } - - /** - * Phone number validator. - */ - phoneNumberValidator(value, { req }) { - const phoneNumber = parsePhoneNumber(value, req.body.country); - - if (!phoneNumber || !phoneNumber.isValid()) { - throw new Error('Phone number is invalid with the given country code.'); - } - return true; - } - /** * Reset password schema. + * @returns {ValidationChain[]} */ - get resetPasswordSchema(): ValidationChain[] { + private get resetPasswordSchema(): ValidationChain[] { return [ check('password') .exists() @@ -170,8 +118,9 @@ export default class AuthenticationController extends BaseController { /** * Send reset password validation schema. + * @returns {ValidationChain[]} */ - get sendResetPasswordSchema(): ValidationChain[] { + private get sendResetPasswordSchema(): ValidationChain[] { return [check('email').exists().isEmail().trim().escape()]; } @@ -180,11 +129,11 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async login(req: Request, res: Response, next: Function): Response { + private async login(req: Request, res: Response, next: Function): Response { const userDTO: ILoginDTO = this.matchedBodyData(req); try { - const { token, user, tenant } = await this.authService.signIn( + const { token, user, tenant } = await this.authApplication.signIn( userDTO.crediential, userDTO.password ); @@ -199,13 +148,11 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async register(req: Request, res: Response, next: Function) { + private async register(req: Request, res: Response, next: Function) { const registerDTO: IRegisterDTO = this.matchedBodyData(req); try { - const registeredUser: ISystemUser = await this.authService.register( - registerDTO - ); + await this.authApplication.signUp(registerDTO); return res.status(200).send({ type: 'success', @@ -222,11 +169,11 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async sendResetPassword(req: Request, res: Response, next: Function) { + private async sendResetPassword(req: Request, res: Response, next: Function) { const { email } = this.matchedBodyData(req); try { - await this.authService.sendResetPassword(email); + await this.authApplication.sendResetPassword(email); return res.status(200).send({ code: 'SEND_RESET_PASSWORD_SUCCESS', @@ -244,12 +191,12 @@ export default class AuthenticationController extends BaseController { * @param {Request} req * @param {Response} res */ - async resetPassword(req: Request, res: Response, next: Function) { + private async resetPassword(req: Request, res: Response, next: Function) { const { token } = req.params; const { password } = req.body; try { - await this.authService.resetPassword(token, password); + await this.authApplication.resetPassword(token, password); return res.status(200).send({ type: 'RESET_PASSWORD_SUCCESS', @@ -263,7 +210,7 @@ export default class AuthenticationController extends BaseController { /** * Handles the service errors. */ - handlerErrors(error, req: Request, res: Response, next: Function) { + private handlerErrors(error, req: Request, res: Response, next: Function) { if (error instanceof ServiceError) { if ( ['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1 @@ -295,18 +242,10 @@ export default class AuthenticationController extends BaseController { errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }], }); } - } - if (error instanceof ServiceErrors) { - const errorReasons = []; - - if (error.hasType('PHONE_NUMBER_EXISTS')) { - errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 }); - } - if (error.hasType('EMAIL_EXISTS')) { - errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 }); - } - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); + if (error.errorType === 'EMAIL_EXISTS') { + return res.status(400).send({ + errors: [{ type: 'EMAIL.EXISTS', code: 600 }], + }); } } next(error); diff --git a/packages/server/src/api/controllers/InviteUsers.ts b/packages/server/src/api/controllers/InviteUsers.ts index 090f33882..af0cefb42 100644 --- a/packages/server/src/api/controllers/InviteUsers.ts +++ b/packages/server/src/api/controllers/InviteUsers.ts @@ -11,10 +11,10 @@ import AcceptInviteUserService from '@/services/InviteUsers/AcceptInviteUser'; @Service() export default class InviteUsersController extends BaseController { @Inject() - inviteUsersService: InviteTenantUserService; + private inviteUsersService: InviteTenantUserService; @Inject() - acceptInviteService: AcceptInviteUserService; + private acceptInviteService: AcceptInviteUserService; /** * Routes that require authentication. @@ -68,13 +68,13 @@ export default class InviteUsersController extends BaseController { /** * Invite DTO schema validation. + * @returns {ValidationChain[]} */ - get inviteUserDTO() { + private get inviteUserDTO() { return [ check('first_name').exists().trim().escape(), check('last_name').exists().trim().escape(), - check('phone_number').exists().trim().escape(), - check('password').exists().trim().escape(), + check('password').exists().trim().escape().isLength({ min: 5 }), param('token').exists().trim().escape(), ]; } @@ -85,17 +85,13 @@ export default class InviteUsersController extends BaseController { * @param {Response} res - Response object. * @param {NextFunction} next - Next function. */ - async sendInvite(req: Request, res: Response, next: Function) { + private async sendInvite(req: Request, res: Response, next: Function) { const sendInviteDTO = this.matchedBodyData(req); const { tenantId } = req; const { user } = req; try { - const { invite } = await this.inviteUsersService.sendInvite( - tenantId, - sendInviteDTO, - user - ); + await this.inviteUsersService.sendInvite(tenantId, sendInviteDTO, user); return res.status(200).send({ type: 'success', code: 'INVITE.SENT.SUCCESSFULLY', @@ -112,7 +108,7 @@ export default class InviteUsersController extends BaseController { * @param {Response} res - Response object. * @param {NextFunction} next - Next function. */ - async resendInvite(req: Request, res: Response, next: NextFunction) { + private async resendInvite(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const { userId } = req.params; @@ -135,7 +131,7 @@ export default class InviteUsersController extends BaseController { * @param {Response} res - * @param {NextFunction} next - */ - async accept(req: Request, res: Response, next: Function) { + private async accept(req: Request, res: Response, next: Function) { const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, { locations: ['body'], includeOptionals: true, @@ -161,7 +157,7 @@ export default class InviteUsersController extends BaseController { * @param {Response} res - * @param {NextFunction} next - */ - async invited(req: Request, res: Response, next: Function) { + private async invited(req: Request, res: Response, next: Function) { const { token } = req.params; try { @@ -181,7 +177,12 @@ export default class InviteUsersController extends BaseController { /** * Handles the service error. */ - handleServicesError(error, req: Request, res: Response, next: Function) { + private handleServicesError( + error, + req: Request, + res: Response, + next: Function + ) { if (error instanceof ServiceError) { if (error.errorType === 'EMAIL_EXISTS') { return res.status(400).send({ diff --git a/packages/server/src/api/controllers/Organization.ts b/packages/server/src/api/controllers/Organization.ts index 76c8e72bb..6eda6d627 100644 --- a/packages/server/src/api/controllers/Organization.ts +++ b/packages/server/src/api/controllers/Organization.ts @@ -8,18 +8,12 @@ import JWTAuth from '@/api/middleware/jwtAuth'; import TenancyMiddleware from '@/api/middleware/TenancyMiddleware'; import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser'; import OrganizationService from '@/services/Organization/OrganizationService'; -import { - ACCEPTED_CURRENCIES, - MONTHS, - ACCEPTED_LOCALES, -} from '@/services/Organization/constants'; +import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants'; import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants'; import { ServiceError } from '@/exceptions'; import BaseController from '@/api/controllers/BaseController'; -const ACCEPTED_LOCATIONS = ['libya']; - @Service() export default class OrganizationController extends BaseController { @Inject() @@ -65,8 +59,8 @@ export default class OrganizationController extends BaseController { return [ check('name').exists().trim(), check('industry').optional().isString(), - check('location').exists().isString().isIn(ACCEPTED_LOCATIONS), - check('base_currency').exists().isIn(ACCEPTED_CURRENCIES), + check('location').exists().isString().isISO31661Alpha2(), + check('base_currency').exists().isISO4217(), check('timezone').exists().isIn(moment.tz.names()), check('fiscal_year').exists().isIn(MONTHS), check('language').exists().isString().isIn(ACCEPTED_LOCALES), diff --git a/packages/server/src/api/controllers/Users.ts b/packages/server/src/api/controllers/Users.ts index 543a7080d..f888b90c0 100644 --- a/packages/server/src/api/controllers/Users.ts +++ b/packages/server/src/api/controllers/Users.ts @@ -47,7 +47,6 @@ export default class UsersController extends BaseController { check('first_name').exists(), check('last_name').exists(), check('email').exists().isEmail(), - check('phone_number').optional().isMobilePhone(), check('role_id').exists().isNumeric().toInt(), ], this.validationResult, diff --git a/packages/server/src/database/migrations/20230405232607_drop_phone_number_from_users.js b/packages/server/src/database/migrations/20230405232607_drop_phone_number_from_users.js new file mode 100644 index 000000000..9ab142779 --- /dev/null +++ b/packages/server/src/database/migrations/20230405232607_drop_phone_number_from_users.js @@ -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) => {}); +}; diff --git a/packages/server/src/interfaces/Authentication.ts b/packages/server/src/interfaces/Authentication.ts index be86dfdd3..fb26c4fa6 100644 --- a/packages/server/src/interfaces/Authentication.ts +++ b/packages/server/src/interfaces/Authentication.ts @@ -1,29 +1,77 @@ import { ISystemUser } from './User'; import { ITenant } from './Tenancy'; +import { SystemUser } from '@/system/models'; export interface IRegisterDTO { - firstName: string, - lastName: string, - email: string, - password: string, - organizationName: string, -}; + firstName: string; + lastName: string; + email: string; + password: string; + organizationName: string; +} export interface ILoginDTO { - crediential: string, - password: string, -}; + crediential: string; + password: string; +} export interface IPasswordReset { - id: number, - email: string, - token: string, - createdAt: Date, -}; + id: number; + email: string; + token: string; + createdAt: Date; +} export interface IAuthenticationService { - signIn(emailOrPhone: string, password: string): Promise<{ user: ISystemUser, token: string, tenant: ITenant }>; + signIn( + email: string, + password: string + ): Promise<{ user: ISystemUser; token: string; tenant: ITenant }>; register(registerDTO: IRegisterDTO): Promise; sendResetPassword(email: string): Promise; resetPassword(token: string, password: string): Promise; +} + +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; } \ No newline at end of file diff --git a/packages/server/src/interfaces/User.ts b/packages/server/src/interfaces/User.ts index 9782f1a91..25543e394 100644 --- a/packages/server/src/interfaces/User.ts +++ b/packages/server/src/interfaces/User.ts @@ -9,7 +9,6 @@ export interface ISystemUser extends Model { active: boolean; password: string; email: string; - phoneNumber: string; roleId: number; tenantId: number; @@ -26,7 +25,6 @@ export interface ISystemUserDTO { firstName: string; lastName: string; password: string; - phoneNumber: string; active: boolean; email: string; roleId?: number; @@ -35,7 +33,6 @@ export interface ISystemUserDTO { export interface IEditUserDTO { firstName: string; lastName: string; - phoneNumber: string; active: boolean; email: string; roleId: number; @@ -44,7 +41,6 @@ export interface IEditUserDTO { export interface IInviteUserInput { firstName: string; lastName: string; - phoneNumber: string; password: string; } export interface IUserInvite { @@ -111,7 +107,6 @@ export interface ITenantUser { id?: number; firstName: string; lastName: string; - phoneNumber: string; active: boolean; email: string; roleId?: number; diff --git a/packages/server/src/jobs/ResetPasswordMail.ts b/packages/server/src/jobs/ResetPasswordMail.ts index cd33e49b3..f9c3c15ff 100644 --- a/packages/server/src/jobs/ResetPasswordMail.ts +++ b/packages/server/src/jobs/ResetPasswordMail.ts @@ -1,5 +1,5 @@ import { Container, Inject } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeEmailJob { /** diff --git a/packages/server/src/jobs/WelcomeSMS.ts b/packages/server/src/jobs/WelcomeSMS.ts index 4dc135db7..67a72c368 100644 --- a/packages/server/src/jobs/WelcomeSMS.ts +++ b/packages/server/src/jobs/WelcomeSMS.ts @@ -1,5 +1,5 @@ import { Container, Inject } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeSMSJob { /** diff --git a/packages/server/src/jobs/welcomeEmail.ts b/packages/server/src/jobs/welcomeEmail.ts index b9c8bbde9..6b076556a 100644 --- a/packages/server/src/jobs/welcomeEmail.ts +++ b/packages/server/src/jobs/welcomeEmail.ts @@ -1,5 +1,5 @@ import { Container } from 'typedi'; -import AuthenticationService from '@/services/Authentication'; +import AuthenticationService from '@/services/Authentication/AuthApplication'; export default class WelcomeEmailJob { /** diff --git a/packages/server/src/services/Authentication/AuthApplication.ts b/packages/server/src/services/Authentication/AuthApplication.ts new file mode 100644 index 000000000..5c077530d --- /dev/null +++ b/packages/server/src/services/Authentication/AuthApplication.ts @@ -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} + */ + public async signUp(signupDTO: IRegisterDTO): Promise { + return this.authSignupService.signUp(signupDTO); + } + + /** + * Generates and retrieve password reset token for the given user email. + * @param {string} email + * @return {} + */ + public async sendResetPassword(email: string): Promise { + 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} + */ + public async resetPassword(token: string, password: string): Promise { + return this.authResetPasswordService.resetPassword(token, password); + } +} diff --git a/packages/server/src/services/Authentication/AuthSendResetPassword.ts b/packages/server/src/services/Authentication/AuthSendResetPassword.ts new file mode 100644 index 000000000..c3a12b47b --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSendResetPassword.ts @@ -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 {} + */ + public async sendResetPassword(email: string): Promise { + 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} + */ + public async resetPassword(token: string, password: string): Promise { + 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 { + const { systemUserRepository } = this.sysRepositories; + const userByEmail = await systemUserRepository.findOneByEmail(email); + + if (!userByEmail) { + throw new ServiceError(ERRORS.EMAIL_NOT_FOUND); + } + return userByEmail; + } +} diff --git a/packages/server/src/services/Authentication/AuthSignin.ts b/packages/server/src/services/Authentication/AuthSignin.ts new file mode 100644 index 000000000..1331eefd5 --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSignin.ts @@ -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 { + 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 }; + } +} diff --git a/packages/server/src/services/Authentication/AuthSignup.ts b/packages/server/src/services/Authentication/AuthSignup.ts new file mode 100644 index 000000000..b2d11ac22 --- /dev/null +++ b/packages/server/src/services/Authentication/AuthSignup.ts @@ -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} + */ + public async signUp(signupDTO: IRegisterDTO): Promise { + 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); + } + } +} diff --git a/packages/server/src/services/Authentication/_constants.ts b/packages/server/src/services/Authentication/_constants.ts new file mode 100644 index 000000000..16a3a5831 --- /dev/null +++ b/packages/server/src/services/Authentication/_constants.ts @@ -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', +}; diff --git a/packages/server/src/services/Authentication/_utils.ts b/packages/server/src/services/Authentication/_utils.ts new file mode 100644 index 000000000..24158e3c9 --- /dev/null +++ b/packages/server/src/services/Authentication/_utils.ts @@ -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 + ); +}; diff --git a/packages/server/src/services/Authentication/index.ts b/packages/server/src/services/Authentication/index.ts deleted file mode 100644 index 8e5a35d77..000000000 --- a/packages/server/src/services/Authentication/index.ts +++ /dev/null @@ -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 { - 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} - */ - private async newTenantOrganization(): Promise { - 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 { - 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 {} - */ - public async sendResetPassword(email: string): Promise { - 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} - */ - public async resetPassword(token: string, password: string): Promise { - 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 - ); - } -} diff --git a/packages/server/src/services/InviteUsers/AcceptInviteUser.ts b/packages/server/src/services/InviteUsers/AcceptInviteUser.ts index 628e630ac..04b99e944 100644 --- a/packages/server/src/services/InviteUsers/AcceptInviteUser.ts +++ b/packages/server/src/services/InviteUsers/AcceptInviteUser.ts @@ -3,8 +3,6 @@ import moment from 'moment'; import { ServiceError } from '@/exceptions'; import { Invite, SystemUser, Tenant } from '@/system/models'; import { hashPassword } from 'utils'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import InviteUsersMailMessages from '@/services/InviteUsers/InviteUsersMailMessages'; import events from '@/subscribers/events'; import { IAcceptInviteEventPayload, @@ -12,29 +10,13 @@ import { ICheckInviteEventPayload, IUserInvite, } from '@/interfaces'; -import TenantsManagerService from '@/services/Tenancy/TenantsManager'; import { ERRORS } from './constants'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; @Service() export default class AcceptInviteUserService { @Inject() - eventPublisher: EventPublisher; - - @Inject() - tenancy: TenancyService; - - @Inject('logger') - logger: any; - - @Inject() - mailMessages: InviteUsersMailMessages; - - @Inject('repositories') - sysRepositories: any; - - @Inject() - tenantsManager: TenantsManagerService; + private eventPublisher: EventPublisher; /** * Accept the received invite. @@ -50,9 +32,6 @@ export default class AcceptInviteUserService { // Retrieve the invite token or throw not found error. const inviteToken = await this.getInviteTokenOrThrowError(token); - // Validates the user phone number. - await this.validateUserPhoneNumberNotExists(inviteUserDTO.phoneNumber); - // Hash the given password. const hashedPassword = await hashPassword(inviteUserDTO.password); diff --git a/packages/server/src/services/Organization/constants.ts b/packages/server/src/services/Organization/constants.ts index 46380409c..a1c0bd6f7 100644 --- a/packages/server/src/services/Organization/constants.ts +++ b/packages/server/src/services/Organization/constants.ts @@ -14,8 +14,6 @@ export const DATE_FORMATS = [ 'MMMM dd, YYYY', 'EEE, MMMM dd, YYYY', ]; -export const ACCEPTED_CURRENCIES = Object.keys(currencies); - export const MONTHS = [ 'january', 'february', diff --git a/packages/server/src/services/Users/UsersService.ts b/packages/server/src/services/Users/UsersService.ts index e6f15c36a..fc1f365c2 100644 --- a/packages/server/src/services/Users/UsersService.ts +++ b/packages/server/src/services/Users/UsersService.ts @@ -17,20 +17,17 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; @Service() export default class UsersService { - @Inject('logger') - logger: any; - @Inject('repositories') - repositories: any; + private repositories: any; @Inject() - rolesService: RolesService; + private rolesService: RolesService; @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; @Inject() - eventPublisher: EventPublisher; + private eventPublisher: EventPublisher; /** * Creates a new user. @@ -46,7 +43,7 @@ export default class UsersService { authorizedUser: ISystemUser ): Promise { const { User } = this.tenancy.models(tenantId); - const { email, phoneNumber } = editUserDTO; + const { email } = editUserDTO; // Retrieve the tenant user or throw not found service error. const oldTenantUser = await this.getTenantUserOrThrowError( @@ -62,9 +59,6 @@ export default class UsersService { // Validate user email should be unique. await this.validateUserEmailUniquiness(tenantId, email, userId); - // Validate user phone number should be unique. - await this.validateUserPhoneNumberUniqiness(tenantId, phoneNumber, userId); - // Retrieve the given role or throw not found service error. const role = await this.rolesService.getRoleOrThrowError( tenantId, @@ -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. * @param {ITenantUser} oldTenantUser diff --git a/packages/server/src/subscribers/Authentication/ResetLoginThrottle.ts b/packages/server/src/subscribers/Authentication/ResetLoginThrottle.ts index 781241fa1..2dcd7ed59 100644 --- a/packages/server/src/subscribers/Authentication/ResetLoginThrottle.ts +++ b/packages/server/src/subscribers/Authentication/ResetLoginThrottle.ts @@ -1,27 +1,29 @@ import { Container, Service } from 'typedi'; import events from '@/subscribers/events'; +import { IAuthSignedInEventPayload } from '@/interfaces'; @Service() export default class ResetLoginThrottleSubscriber { /** * Attaches events with handlers. - * @param bus + * @param bus */ public attach(bus) { - bus.subscribe(events.auth.login, this.resetLoginThrottleOnceSuccessLogin); + bus.subscribe(events.auth.signIn, this.resetLoginThrottleOnceSuccessLogin); } /** * Resets the login throttle once the login success. + * @param {IAuthSignedInEventPayload} payload - */ - private async resetLoginThrottleOnceSuccessLogin(payload) { - const { emailOrPhone, password, user } = payload; - + private async resetLoginThrottleOnceSuccessLogin( + payload: IAuthSignedInEventPayload + ) { + const { email, user } = payload; const loginThrottler = Container.get('rateLimiter.login'); // Reset the login throttle by the given email and phone number. await loginThrottler.reset(user.email); - await loginThrottler.reset(user.phoneNumber); - await loginThrottler.reset(emailOrPhone); + await loginThrottler.reset(email); } } diff --git a/packages/server/src/subscribers/Authentication/SendWelcomeMail.ts b/packages/server/src/subscribers/Authentication/SendWelcomeMail.ts index ea9bf9de3..e692033c1 100644 --- a/packages/server/src/subscribers/Authentication/SendWelcomeMail.ts +++ b/packages/server/src/subscribers/Authentication/SendWelcomeMail.ts @@ -10,14 +10,14 @@ export default class AuthSendWelcomeMailSubscriber { * Attaches events with handlers. */ public attach(bus) { - bus.subscribe(events.auth.register, this.sendWelcomeEmailOnceUserRegister); + bus.subscribe(events.auth.signUp, this.sendWelcomeEmailOnceUserRegister); } /** * Sends welcome email once the user register. */ private sendWelcomeEmailOnceUserRegister = async (payload) => { - const { registerDTO, tenant, user } = payload; + const { tenant, user } = payload; // Send welcome mail to the user. await this.agenda.now('welcome-email', { diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index a4abf80fb..19416b65d 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -3,10 +3,17 @@ export default { * Authentication service. */ auth: { - login: 'onLogin', - register: 'onRegister', + signIn: 'onSignIn', + signingIn: 'onSigningIn', + + signUp: 'onSignUp', + signingUp: 'onSigningUp', + + sendingResetPassword: 'onSendingResetPassword', sendResetPassword: 'onSendResetPassword', + resetPassword: 'onResetPassword', + resetingPassword: 'onResetingPassword' }, /** diff --git a/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js b/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js new file mode 100644 index 000000000..9ab142779 --- /dev/null +++ b/packages/server/src/system/migrations/20230405011450_drop_phone_number_column_from_users_table.js @@ -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) => {}); +}; diff --git a/packages/webapp/src/components/App.tsx b/packages/webapp/src/components/App.tsx index de8263e5c..0ad264ee8 100644 --- a/packages/webapp/src/components/App.tsx +++ b/packages/webapp/src/components/App.tsx @@ -13,7 +13,7 @@ import AppIntlLoader from './AppIntlLoader'; import PrivateRoute from '@/components/Guards/PrivateRoute'; import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors'; import DashboardPrivatePages from '@/components/Dashboard/PrivatePages'; -import Authentication from '@/components/Authentication'; +import { Authentication } from '@/containers/Authentication/Authentication'; import { SplashScreen, DashboardThemeProvider } from '../components'; import { queryConfig } from '../hooks/query/base'; diff --git a/packages/webapp/src/components/Authentication.tsx b/packages/webapp/src/components/Authentication.tsx deleted file mode 100644 index 1a3267554..000000000 --- a/packages/webapp/src/components/Authentication.tsx +++ /dev/null @@ -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 ; -} - -export default function AuthenticationWrapper({ ...rest }) { - const to = { pathname: '/' }; - const location = useLocation(); - const isAuthenticated = useIsAuthenticated(); - const locationKey = location.pathname; - - return ( - <> - {isAuthenticated ? ( - - ) : ( - -
- - - - -
-
-
- -
- - - - - {authenticationRoutes.map((route, index) => ( - - ))} - - - -
-
-
-
- )} - - ); -} diff --git a/packages/webapp/src/constants/countries.ts b/packages/webapp/src/constants/countries.ts new file mode 100644 index 000000000..36691ded2 --- /dev/null +++ b/packages/webapp/src/constants/countries.ts @@ -0,0 +1,2269 @@ +interface Country { + name: string; + native: string; + phone: number[]; + continent: string; + continents?: string[]; + capital: string; + currency: string[]; + languages: string[]; +} + +export const Countries: Record = { + AD: { + name: 'Andorra', + native: 'Andorra', + phone: [376], + continent: 'EU', + capital: 'Andorra la Vella', + currency: ['EUR'], + languages: ['ca'], + }, + AE: { + name: 'United Arab Emirates', + native: 'دولة الإمارات العربية المتحدة', + phone: [971], + continent: 'AS', + capital: 'Abu Dhabi', + currency: ['AED'], + languages: ['ar'], + }, + AF: { + name: 'Afghanistan', + native: 'افغانستان', + phone: [93], + continent: 'AS', + capital: 'Kabul', + currency: ['AFN'], + languages: ['ps', 'uz', 'tk'], + }, + AG: { + name: 'Antigua and Barbuda', + native: 'Antigua and Barbuda', + phone: [1268], + continent: 'NA', + capital: "Saint John's", + currency: ['XCD'], + languages: ['en'], + }, + AI: { + name: 'Anguilla', + native: 'Anguilla', + phone: [1264], + continent: 'NA', + capital: 'The Valley', + currency: ['XCD'], + languages: ['en'], + }, + AL: { + name: 'Albania', + native: 'Shqipëria', + phone: [355], + continent: 'EU', + capital: 'Tirana', + currency: ['ALL'], + languages: ['sq'], + }, + AM: { + name: 'Armenia', + native: 'Հայաստան', + phone: [374], + continent: 'AS', + capital: 'Yerevan', + currency: ['AMD'], + languages: ['hy', 'ru'], + }, + AO: { + name: 'Angola', + native: 'Angola', + phone: [244], + continent: 'AF', + capital: 'Luanda', + currency: ['AOA'], + languages: ['pt'], + }, + AQ: { + name: 'Antarctica', + native: 'Antarctica', + phone: [672], + continent: 'AN', + capital: '', + currency: [], + languages: [], + }, + AR: { + name: 'Argentina', + native: 'Argentina', + phone: [54], + continent: 'SA', + capital: 'Buenos Aires', + currency: ['ARS'], + languages: ['es', 'gn'], + }, + AS: { + name: 'American Samoa', + native: 'American Samoa', + phone: [1684], + continent: 'OC', + capital: 'Pago Pago', + currency: ['USD'], + languages: ['en', 'sm'], + }, + AT: { + name: 'Austria', + native: 'Österreich', + phone: [43], + continent: 'EU', + capital: 'Vienna', + currency: ['EUR'], + languages: ['de'], + }, + AU: { + name: 'Australia', + native: 'Australia', + phone: [61], + continent: 'OC', + capital: 'Canberra', + currency: ['AUD'], + languages: ['en'], + }, + AW: { + name: 'Aruba', + native: 'Aruba', + phone: [297], + continent: 'NA', + capital: 'Oranjestad', + currency: ['AWG'], + languages: ['nl', 'pa'], + }, + AX: { + name: 'Åland', + native: 'Åland', + phone: [358], + continent: 'EU', + capital: 'Mariehamn', + currency: ['EUR'], + languages: ['sv'], + }, + AZ: { + name: 'Azerbaijan', + native: 'Azərbaycan', + phone: [994], + continent: 'AS', + continents: ['AS', 'EU'], + capital: 'Baku', + currency: ['AZN'], + languages: ['az'], + }, + BA: { + name: 'Bosnia and Herzegovina', + native: 'Bosna i Hercegovina', + phone: [387], + continent: 'EU', + capital: 'Sarajevo', + currency: ['BAM'], + languages: ['bs', 'hr', 'sr'], + }, + BB: { + name: 'Barbados', + native: 'Barbados', + phone: [1246], + continent: 'NA', + capital: 'Bridgetown', + currency: ['BBD'], + languages: ['en'], + }, + BD: { + name: 'Bangladesh', + native: 'Bangladesh', + phone: [880], + continent: 'AS', + capital: 'Dhaka', + currency: ['BDT'], + languages: ['bn'], + }, + BE: { + name: 'Belgium', + native: 'België', + phone: [32], + continent: 'EU', + capital: 'Brussels', + currency: ['EUR'], + languages: ['nl', 'fr', 'de'], + }, + BF: { + name: 'Burkina Faso', + native: 'Burkina Faso', + phone: [226], + continent: 'AF', + capital: 'Ouagadougou', + currency: ['XOF'], + languages: ['fr', 'ff'], + }, + BG: { + name: 'Bulgaria', + native: 'България', + phone: [359], + continent: 'EU', + capital: 'Sofia', + currency: ['BGN'], + languages: ['bg'], + }, + BH: { + name: 'Bahrain', + native: '‏البحرين', + phone: [973], + continent: 'AS', + capital: 'Manama', + currency: ['BHD'], + languages: ['ar'], + }, + BI: { + name: 'Burundi', + native: 'Burundi', + phone: [257], + continent: 'AF', + capital: 'Bujumbura', + currency: ['BIF'], + languages: ['fr', 'rn'], + }, + BJ: { + name: 'Benin', + native: 'Bénin', + phone: [229], + continent: 'AF', + capital: 'Porto-Novo', + currency: ['XOF'], + languages: ['fr'], + }, + BL: { + name: 'Saint Barthélemy', + native: 'Saint-Barthélemy', + phone: [590], + continent: 'NA', + capital: 'Gustavia', + currency: ['EUR'], + languages: ['fr'], + }, + BM: { + name: 'Bermuda', + native: 'Bermuda', + phone: [1441], + continent: 'NA', + capital: 'Hamilton', + currency: ['BMD'], + languages: ['en'], + }, + BN: { + name: 'Brunei', + native: 'Negara Brunei Darussalam', + phone: [673], + continent: 'AS', + capital: 'Bandar Seri Begawan', + currency: ['BND'], + languages: ['ms'], + }, + BO: { + name: 'Bolivia', + native: 'Bolivia', + phone: [591], + continent: 'SA', + capital: 'Sucre', + currency: ['BOB', 'BOV'], + languages: ['es', 'ay', 'qu'], + }, + BQ: { + name: 'Bonaire', + native: 'Bonaire', + phone: [5997], + continent: 'NA', + capital: 'Kralendijk', + currency: ['USD'], + languages: ['nl'], + }, + BR: { + name: 'Brazil', + native: 'Brasil', + phone: [55], + continent: 'SA', + capital: 'Brasília', + currency: ['BRL'], + languages: ['pt'], + }, + BS: { + name: 'Bahamas', + native: 'Bahamas', + phone: [1242], + continent: 'NA', + capital: 'Nassau', + currency: ['BSD'], + languages: ['en'], + }, + BT: { + name: 'Bhutan', + native: 'ʼbrug-yul', + phone: [975], + continent: 'AS', + capital: 'Thimphu', + currency: ['BTN', 'INR'], + languages: ['dz'], + }, + BV: { + name: 'Bouvet Island', + native: 'Bouvetøya', + phone: [47], + continent: 'AN', + capital: '', + currency: ['NOK'], + languages: ['no', 'nb', 'nn'], + }, + BW: { + name: 'Botswana', + native: 'Botswana', + phone: [267], + continent: 'AF', + capital: 'Gaborone', + currency: ['BWP'], + languages: ['en', 'tn'], + }, + BY: { + name: 'Belarus', + native: 'Белару́сь', + phone: [375], + continent: 'EU', + capital: 'Minsk', + currency: ['BYN'], + languages: ['be', 'ru'], + }, + BZ: { + name: 'Belize', + native: 'Belize', + phone: [501], + continent: 'NA', + capital: 'Belmopan', + currency: ['BZD'], + languages: ['en', 'es'], + }, + CA: { + name: 'Canada', + native: 'Canada', + phone: [1], + continent: 'NA', + capital: 'Ottawa', + currency: ['CAD'], + languages: ['en', 'fr'], + }, + CC: { + name: 'Cocos [Keeling] Islands', + native: 'Cocos (Keeling) Islands', + phone: [61], + continent: 'AS', + capital: 'West Island', + currency: ['AUD'], + languages: ['en'], + }, + CD: { + name: 'Democratic Republic of the Congo', + native: 'République démocratique du Congo', + phone: [243], + continent: 'AF', + capital: 'Kinshasa', + currency: ['CDF'], + languages: ['fr', 'ln', 'kg', 'sw', 'lu'], + }, + CF: { + name: 'Central African Republic', + native: 'Ködörösêse tî Bêafrîka', + phone: [236], + continent: 'AF', + capital: 'Bangui', + currency: ['XAF'], + languages: ['fr', 'sg'], + }, + CG: { + name: 'Republic of the Congo', + native: 'République du Congo', + phone: [242], + continent: 'AF', + capital: 'Brazzaville', + currency: ['XAF'], + languages: ['fr', 'ln'], + }, + CH: { + name: 'Switzerland', + native: 'Schweiz', + phone: [41], + continent: 'EU', + capital: 'Bern', + currency: ['CHE', 'CHF', 'CHW'], + languages: ['de', 'fr', 'it'], + }, + CI: { + name: 'Ivory Coast', + native: "Côte d'Ivoire", + phone: [225], + continent: 'AF', + capital: 'Yamoussoukro', + currency: ['XOF'], + languages: ['fr'], + }, + CK: { + name: 'Cook Islands', + native: 'Cook Islands', + phone: [682], + continent: 'OC', + capital: 'Avarua', + currency: ['NZD'], + languages: ['en'], + }, + CL: { + name: 'Chile', + native: 'Chile', + phone: [56], + continent: 'SA', + capital: 'Santiago', + currency: ['CLF', 'CLP'], + languages: ['es'], + }, + CM: { + name: 'Cameroon', + native: 'Cameroon', + phone: [237], + continent: 'AF', + capital: 'Yaoundé', + currency: ['XAF'], + languages: ['en', 'fr'], + }, + CN: { + name: 'China', + native: '中国', + phone: [86], + continent: 'AS', + capital: 'Beijing', + currency: ['CNY'], + languages: ['zh'], + }, + CO: { + name: 'Colombia', + native: 'Colombia', + phone: [57], + continent: 'SA', + capital: 'Bogotá', + currency: ['COP'], + languages: ['es'], + }, + CR: { + name: 'Costa Rica', + native: 'Costa Rica', + phone: [506], + continent: 'NA', + capital: 'San José', + currency: ['CRC'], + languages: ['es'], + }, + CU: { + name: 'Cuba', + native: 'Cuba', + phone: [53], + continent: 'NA', + capital: 'Havana', + currency: ['CUC', 'CUP'], + languages: ['es'], + }, + CV: { + name: 'Cape Verde', + native: 'Cabo Verde', + phone: [238], + continent: 'AF', + capital: 'Praia', + currency: ['CVE'], + languages: ['pt'], + }, + CW: { + name: 'Curacao', + native: 'Curaçao', + phone: [5999], + continent: 'NA', + capital: 'Willemstad', + currency: ['ANG'], + languages: ['nl', 'pa', 'en'], + }, + CX: { + name: 'Christmas Island', + native: 'Christmas Island', + phone: [61], + continent: 'AS', + capital: 'Flying Fish Cove', + currency: ['AUD'], + languages: ['en'], + }, + CY: { + name: 'Cyprus', + native: 'Κύπρος', + phone: [357], + continent: 'EU', + capital: 'Nicosia', + currency: ['EUR'], + languages: ['el', 'tr', 'hy'], + }, + CZ: { + name: 'Czech Republic', + native: 'Česká republika', + phone: [420], + continent: 'EU', + capital: 'Prague', + currency: ['CZK'], + languages: ['cs', 'sk'], + }, + DE: { + name: 'Germany', + native: 'Deutschland', + phone: [49], + continent: 'EU', + capital: 'Berlin', + currency: ['EUR'], + languages: ['de'], + }, + DJ: { + name: 'Djibouti', + native: 'Djibouti', + phone: [253], + continent: 'AF', + capital: 'Djibouti', + currency: ['DJF'], + languages: ['fr', 'ar'], + }, + DK: { + name: 'Denmark', + native: 'Danmark', + phone: [45], + continent: 'EU', + continents: ['EU', 'NA'], + capital: 'Copenhagen', + currency: ['DKK'], + languages: ['da'], + }, + DM: { + name: 'Dominica', + native: 'Dominica', + phone: [1767], + continent: 'NA', + capital: 'Roseau', + currency: ['XCD'], + languages: ['en'], + }, + DO: { + name: 'Dominican Republic', + native: 'República Dominicana', + phone: [1809, 1829, 1849], + continent: 'NA', + capital: 'Santo Domingo', + currency: ['DOP'], + languages: ['es'], + }, + DZ: { + name: 'Algeria', + native: 'الجزائر', + phone: [213], + continent: 'AF', + capital: 'Algiers', + currency: ['DZD'], + languages: ['ar'], + }, + EC: { + name: 'Ecuador', + native: 'Ecuador', + phone: [593], + continent: 'SA', + capital: 'Quito', + currency: ['USD'], + languages: ['es'], + }, + EE: { + name: 'Estonia', + native: 'Eesti', + phone: [372], + continent: 'EU', + capital: 'Tallinn', + currency: ['EUR'], + languages: ['et'], + }, + EG: { + name: 'Egypt', + native: 'مصر‎', + phone: [20], + continent: 'AF', + continents: ['AF', 'AS'], + capital: 'Cairo', + currency: ['EGP'], + languages: ['ar'], + }, + EH: { + name: 'Western Sahara', + native: 'الصحراء الغربية', + phone: [212], + continent: 'AF', + capital: 'El Aaiún', + currency: ['MAD', 'DZD', 'MRU'], + languages: ['es'], + }, + ER: { + name: 'Eritrea', + native: 'ኤርትራ', + phone: [291], + continent: 'AF', + capital: 'Asmara', + currency: ['ERN'], + languages: ['ti', 'ar', 'en'], + }, + ES: { + name: 'Spain', + native: 'España', + phone: [34], + continent: 'EU', + capital: 'Madrid', + currency: ['EUR'], + languages: ['es', 'eu', 'ca', 'gl', 'oc'], + }, + ET: { + name: 'Ethiopia', + native: 'ኢትዮጵያ', + phone: [251], + continent: 'AF', + capital: 'Addis Ababa', + currency: ['ETB'], + languages: ['am'], + }, + FI: { + name: 'Finland', + native: 'Suomi', + phone: [358], + continent: 'EU', + capital: 'Helsinki', + currency: ['EUR'], + languages: ['fi', 'sv'], + }, + FJ: { + name: 'Fiji', + native: 'Fiji', + phone: [679], + continent: 'OC', + capital: 'Suva', + currency: ['FJD'], + languages: ['en', 'fj', 'hi', 'ur'], + }, + FK: { + name: 'Falkland Islands', + native: 'Falkland Islands', + phone: [500], + continent: 'SA', + capital: 'Stanley', + currency: ['FKP'], + languages: ['en'], + }, + FM: { + name: 'Micronesia', + native: 'Micronesia', + phone: [691], + continent: 'OC', + capital: 'Palikir', + currency: ['USD'], + languages: ['en'], + }, + FO: { + name: 'Faroe Islands', + native: 'Føroyar', + phone: [298], + continent: 'EU', + capital: 'Tórshavn', + currency: ['DKK'], + languages: ['fo'], + }, + FR: { + name: 'France', + native: 'France', + phone: [33], + continent: 'EU', + capital: 'Paris', + currency: ['EUR'], + languages: ['fr'], + }, + GA: { + name: 'Gabon', + native: 'Gabon', + phone: [241], + continent: 'AF', + capital: 'Libreville', + currency: ['XAF'], + languages: ['fr'], + }, + GB: { + name: 'United Kingdom', + native: 'United Kingdom', + phone: [44], + continent: 'EU', + capital: 'London', + currency: ['GBP'], + languages: ['en'], + }, + GD: { + name: 'Grenada', + native: 'Grenada', + phone: [1473], + continent: 'NA', + capital: "St. George's", + currency: ['XCD'], + languages: ['en'], + }, + GE: { + name: 'Georgia', + native: 'საქართველო', + phone: [995], + continent: 'AS', + capital: 'Tbilisi', + currency: ['GEL'], + languages: ['ka'], + }, + GF: { + name: 'French Guiana', + native: 'Guyane française', + phone: [594], + continent: 'SA', + capital: 'Cayenne', + currency: ['EUR'], + languages: ['fr'], + }, + GG: { + name: 'Guernsey', + native: 'Guernsey', + phone: [44], + continent: 'EU', + capital: 'St. Peter Port', + currency: ['GBP'], + languages: ['en', 'fr'], + }, + GH: { + name: 'Ghana', + native: 'Ghana', + phone: [233], + continent: 'AF', + capital: 'Accra', + currency: ['GHS'], + languages: ['en'], + }, + GI: { + name: 'Gibraltar', + native: 'Gibraltar', + phone: [350], + continent: 'EU', + capital: 'Gibraltar', + currency: ['GIP'], + languages: ['en'], + }, + GL: { + name: 'Greenland', + native: 'Kalaallit Nunaat', + phone: [299], + continent: 'NA', + capital: 'Nuuk', + currency: ['DKK'], + languages: ['kl'], + }, + GM: { + name: 'Gambia', + native: 'Gambia', + phone: [220], + continent: 'AF', + capital: 'Banjul', + currency: ['GMD'], + languages: ['en'], + }, + GN: { + name: 'Guinea', + native: 'Guinée', + phone: [224], + continent: 'AF', + capital: 'Conakry', + currency: ['GNF'], + languages: ['fr', 'ff'], + }, + GP: { + name: 'Guadeloupe', + native: 'Guadeloupe', + phone: [590], + continent: 'NA', + capital: 'Basse-Terre', + currency: ['EUR'], + languages: ['fr'], + }, + GQ: { + name: 'Equatorial Guinea', + native: 'Guinea Ecuatorial', + phone: [240], + continent: 'AF', + capital: 'Malabo', + currency: ['XAF'], + languages: ['es', 'fr'], + }, + GR: { + name: 'Greece', + native: 'Ελλάδα', + phone: [30], + continent: 'EU', + capital: 'Athens', + currency: ['EUR'], + languages: ['el'], + }, + GS: { + name: 'South Georgia and the South Sandwich Islands', + native: 'South Georgia', + phone: [500], + continent: 'AN', + capital: 'King Edward Point', + currency: ['GBP'], + languages: ['en'], + }, + GT: { + name: 'Guatemala', + native: 'Guatemala', + phone: [502], + continent: 'NA', + capital: 'Guatemala City', + currency: ['GTQ'], + languages: ['es'], + }, + GU: { + name: 'Guam', + native: 'Guam', + phone: [1671], + continent: 'OC', + capital: 'Hagåtña', + currency: ['USD'], + languages: ['en', 'ch', 'es'], + }, + GW: { + name: 'Guinea-Bissau', + native: 'Guiné-Bissau', + phone: [245], + continent: 'AF', + capital: 'Bissau', + currency: ['XOF'], + languages: ['pt'], + }, + GY: { + name: 'Guyana', + native: 'Guyana', + phone: [592], + continent: 'SA', + capital: 'Georgetown', + currency: ['GYD'], + languages: ['en'], + }, + HK: { + name: 'Hong Kong', + native: '香港', + phone: [852], + continent: 'AS', + capital: 'City of Victoria', + currency: ['HKD'], + languages: ['zh', 'en'], + }, + HM: { + name: 'Heard Island and McDonald Islands', + native: 'Heard Island and McDonald Islands', + phone: [61], + continent: 'AN', + capital: '', + currency: ['AUD'], + languages: ['en'], + }, + HN: { + name: 'Honduras', + native: 'Honduras', + phone: [504], + continent: 'NA', + capital: 'Tegucigalpa', + currency: ['HNL'], + languages: ['es'], + }, + HR: { + name: 'Croatia', + native: 'Hrvatska', + phone: [385], + continent: 'EU', + capital: 'Zagreb', + currency: ['HRK'], + languages: ['hr'], + }, + HT: { + name: 'Haiti', + native: 'Haïti', + phone: [509], + continent: 'NA', + capital: 'Port-au-Prince', + currency: ['HTG', 'USD'], + languages: ['fr', 'ht'], + }, + HU: { + name: 'Hungary', + native: 'Magyarország', + phone: [36], + continent: 'EU', + capital: 'Budapest', + currency: ['HUF'], + languages: ['hu'], + }, + ID: { + name: 'Indonesia', + native: 'Indonesia', + phone: [62], + continent: 'AS', + capital: 'Jakarta', + currency: ['IDR'], + languages: ['id'], + }, + IE: { + name: 'Ireland', + native: 'Éire', + phone: [353], + continent: 'EU', + capital: 'Dublin', + currency: ['EUR'], + languages: ['ga', 'en'], + }, + IL: { + name: 'Israel', + native: 'יִשְׂרָאֵל', + phone: [972], + continent: 'AS', + capital: 'Jerusalem', + currency: ['ILS'], + languages: ['he', 'ar'], + }, + IM: { + name: 'Isle of Man', + native: 'Isle of Man', + phone: [44], + continent: 'EU', + capital: 'Douglas', + currency: ['GBP'], + languages: ['en', 'gv'], + }, + IN: { + name: 'India', + native: 'भारत', + phone: [91], + continent: 'AS', + capital: 'New Delhi', + currency: ['INR'], + languages: ['hi', 'en'], + }, + IO: { + name: 'British Indian Ocean Territory', + native: 'British Indian Ocean Territory', + phone: [246], + continent: 'AS', + capital: 'Diego Garcia', + currency: ['USD'], + languages: ['en'], + }, + IQ: { + name: 'Iraq', + native: 'العراق', + phone: [964], + continent: 'AS', + capital: 'Baghdad', + currency: ['IQD'], + languages: ['ar', 'ku'], + }, + IR: { + name: 'Iran', + native: 'ایران', + phone: [98], + continent: 'AS', + capital: 'Tehran', + currency: ['IRR'], + languages: ['fa'], + }, + IS: { + name: 'Iceland', + native: 'Ísland', + phone: [354], + continent: 'EU', + capital: 'Reykjavik', + currency: ['ISK'], + languages: ['is'], + }, + IT: { + name: 'Italy', + native: 'Italia', + phone: [39], + continent: 'EU', + capital: 'Rome', + currency: ['EUR'], + languages: ['it'], + }, + JE: { + name: 'Jersey', + native: 'Jersey', + phone: [44], + continent: 'EU', + capital: 'Saint Helier', + currency: ['GBP'], + languages: ['en', 'fr'], + }, + JM: { + name: 'Jamaica', + native: 'Jamaica', + phone: [1876], + continent: 'NA', + capital: 'Kingston', + currency: ['JMD'], + languages: ['en'], + }, + JO: { + name: 'Jordan', + native: 'الأردن', + phone: [962], + continent: 'AS', + capital: 'Amman', + currency: ['JOD'], + languages: ['ar'], + }, + JP: { + name: 'Japan', + native: '日本', + phone: [81], + continent: 'AS', + capital: 'Tokyo', + currency: ['JPY'], + languages: ['ja'], + }, + KE: { + name: 'Kenya', + native: 'Kenya', + phone: [254], + continent: 'AF', + capital: 'Nairobi', + currency: ['KES'], + languages: ['en', 'sw'], + }, + KG: { + name: 'Kyrgyzstan', + native: 'Кыргызстан', + phone: [996], + continent: 'AS', + capital: 'Bishkek', + currency: ['KGS'], + languages: ['ky', 'ru'], + }, + KH: { + name: 'Cambodia', + native: 'Kâmpŭchéa', + phone: [855], + continent: 'AS', + capital: 'Phnom Penh', + currency: ['KHR'], + languages: ['km'], + }, + KI: { + name: 'Kiribati', + native: 'Kiribati', + phone: [686], + continent: 'OC', + capital: 'South Tarawa', + currency: ['AUD'], + languages: ['en'], + }, + KM: { + name: 'Comoros', + native: 'Komori', + phone: [269], + continent: 'AF', + capital: 'Moroni', + currency: ['KMF'], + languages: ['ar', 'fr'], + }, + KN: { + name: 'Saint Kitts and Nevis', + native: 'Saint Kitts and Nevis', + phone: [1869], + continent: 'NA', + capital: 'Basseterre', + currency: ['XCD'], + languages: ['en'], + }, + KP: { + name: 'North Korea', + native: '북한', + phone: [850], + continent: 'AS', + capital: 'Pyongyang', + currency: ['KPW'], + languages: ['ko'], + }, + KR: { + name: 'South Korea', + native: '대한민국', + phone: [82], + continent: 'AS', + capital: 'Seoul', + currency: ['KRW'], + languages: ['ko'], + }, + KW: { + name: 'Kuwait', + native: 'الكويت', + phone: [965], + continent: 'AS', + capital: 'Kuwait City', + currency: ['KWD'], + languages: ['ar'], + }, + KY: { + name: 'Cayman Islands', + native: 'Cayman Islands', + phone: [1345], + continent: 'NA', + capital: 'George Town', + currency: ['KYD'], + languages: ['en'], + }, + KZ: { + name: 'Kazakhstan', + native: 'Қазақстан', + phone: [76, 77], + continent: 'AS', + continents: ['AS', 'EU'], + capital: 'Astana', + currency: ['KZT'], + languages: ['kk', 'ru'], + }, + LA: { + name: 'Laos', + native: 'ສປປລາວ', + phone: [856], + continent: 'AS', + capital: 'Vientiane', + currency: ['LAK'], + languages: ['lo'], + }, + LB: { + name: 'Lebanon', + native: 'لبنان', + phone: [961], + continent: 'AS', + capital: 'Beirut', + currency: ['LBP'], + languages: ['ar', 'fr'], + }, + LC: { + name: 'Saint Lucia', + native: 'Saint Lucia', + phone: [1758], + continent: 'NA', + capital: 'Castries', + currency: ['XCD'], + languages: ['en'], + }, + LI: { + name: 'Liechtenstein', + native: 'Liechtenstein', + phone: [423], + continent: 'EU', + capital: 'Vaduz', + currency: ['CHF'], + languages: ['de'], + }, + LK: { + name: 'Sri Lanka', + native: 'śrī laṃkāva', + phone: [94], + continent: 'AS', + capital: 'Colombo', + currency: ['LKR'], + languages: ['si', 'ta'], + }, + LR: { + name: 'Liberia', + native: 'Liberia', + phone: [231], + continent: 'AF', + capital: 'Monrovia', + currency: ['LRD'], + languages: ['en'], + }, + LS: { + name: 'Lesotho', + native: 'Lesotho', + phone: [266], + continent: 'AF', + capital: 'Maseru', + currency: ['LSL', 'ZAR'], + languages: ['en', 'st'], + }, + LT: { + name: 'Lithuania', + native: 'Lietuva', + phone: [370], + continent: 'EU', + capital: 'Vilnius', + currency: ['EUR'], + languages: ['lt'], + }, + LU: { + name: 'Luxembourg', + native: 'Luxembourg', + phone: [352], + continent: 'EU', + capital: 'Luxembourg', + currency: ['EUR'], + languages: ['fr', 'de', 'lb'], + }, + LV: { + name: 'Latvia', + native: 'Latvija', + phone: [371], + continent: 'EU', + capital: 'Riga', + currency: ['EUR'], + languages: ['lv'], + }, + LY: { + name: 'Libya', + native: '‏ليبيا', + phone: [218], + continent: 'AF', + capital: 'Tripoli', + currency: ['LYD'], + languages: ['ar'], + }, + MA: { + name: 'Morocco', + native: 'المغرب', + phone: [212], + continent: 'AF', + capital: 'Rabat', + currency: ['MAD'], + languages: ['ar'], + }, + MC: { + name: 'Monaco', + native: 'Monaco', + phone: [377], + continent: 'EU', + capital: 'Monaco', + currency: ['EUR'], + languages: ['fr'], + }, + MD: { + name: 'Moldova', + native: 'Moldova', + phone: [373], + continent: 'EU', + capital: 'Chișinău', + currency: ['MDL'], + languages: ['ro'], + }, + ME: { + name: 'Montenegro', + native: 'Црна Гора', + phone: [382], + continent: 'EU', + capital: 'Podgorica', + currency: ['EUR'], + languages: ['sr', 'bs', 'sq', 'hr'], + }, + MF: { + name: 'Saint Martin', + native: 'Saint-Martin', + phone: [590], + continent: 'NA', + capital: 'Marigot', + currency: ['EUR'], + languages: ['en', 'fr', 'nl'], + }, + MG: { + name: 'Madagascar', + native: 'Madagasikara', + phone: [261], + continent: 'AF', + capital: 'Antananarivo', + currency: ['MGA'], + languages: ['fr', 'mg'], + }, + MH: { + name: 'Marshall Islands', + native: 'M̧ajeļ', + phone: [692], + continent: 'OC', + capital: 'Majuro', + currency: ['USD'], + languages: ['en', 'mh'], + }, + MK: { + name: 'North Macedonia', + native: 'Северна Македонија', + phone: [389], + continent: 'EU', + capital: 'Skopje', + currency: ['MKD'], + languages: ['mk'], + }, + ML: { + name: 'Mali', + native: 'Mali', + phone: [223], + continent: 'AF', + capital: 'Bamako', + currency: ['XOF'], + languages: ['fr'], + }, + MM: { + name: 'Myanmar [Burma]', + native: 'မြန်မာ', + phone: [95], + continent: 'AS', + capital: 'Naypyidaw', + currency: ['MMK'], + languages: ['my'], + }, + MN: { + name: 'Mongolia', + native: 'Монгол улс', + phone: [976], + continent: 'AS', + capital: 'Ulan Bator', + currency: ['MNT'], + languages: ['mn'], + }, + MO: { + name: 'Macao', + native: '澳門', + phone: [853], + continent: 'AS', + capital: '', + currency: ['MOP'], + languages: ['zh', 'pt'], + }, + MP: { + name: 'Northern Mariana Islands', + native: 'Northern Mariana Islands', + phone: [1670], + continent: 'OC', + capital: 'Saipan', + currency: ['USD'], + languages: ['en', 'ch'], + }, + MQ: { + name: 'Martinique', + native: 'Martinique', + phone: [596], + continent: 'NA', + capital: 'Fort-de-France', + currency: ['EUR'], + languages: ['fr'], + }, + MR: { + name: 'Mauritania', + native: 'موريتانيا', + phone: [222], + continent: 'AF', + capital: 'Nouakchott', + currency: ['MRU'], + languages: ['ar'], + }, + MS: { + name: 'Montserrat', + native: 'Montserrat', + phone: [1664], + continent: 'NA', + capital: 'Plymouth', + currency: ['XCD'], + languages: ['en'], + }, + MT: { + name: 'Malta', + native: 'Malta', + phone: [356], + continent: 'EU', + capital: 'Valletta', + currency: ['EUR'], + languages: ['mt', 'en'], + }, + MU: { + name: 'Mauritius', + native: 'Maurice', + phone: [230], + continent: 'AF', + capital: 'Port Louis', + currency: ['MUR'], + languages: ['en'], + }, + MV: { + name: 'Maldives', + native: 'Maldives', + phone: [960], + continent: 'AS', + capital: 'Malé', + currency: ['MVR'], + languages: ['dv'], + }, + MW: { + name: 'Malawi', + native: 'Malawi', + phone: [265], + continent: 'AF', + capital: 'Lilongwe', + currency: ['MWK'], + languages: ['en', 'ny'], + }, + MX: { + name: 'Mexico', + native: 'México', + phone: [52], + continent: 'NA', + capital: 'Mexico City', + currency: ['MXN'], + languages: ['es'], + }, + MY: { + name: 'Malaysia', + native: 'Malaysia', + phone: [60], + continent: 'AS', + capital: 'Kuala Lumpur', + currency: ['MYR'], + languages: ['ms'], + }, + MZ: { + name: 'Mozambique', + native: 'Moçambique', + phone: [258], + continent: 'AF', + capital: 'Maputo', + currency: ['MZN'], + languages: ['pt'], + }, + NA: { + name: 'Namibia', + native: 'Namibia', + phone: [264], + continent: 'AF', + capital: 'Windhoek', + currency: ['NAD', 'ZAR'], + languages: ['en', 'af'], + }, + NC: { + name: 'New Caledonia', + native: 'Nouvelle-Calédonie', + phone: [687], + continent: 'OC', + capital: 'Nouméa', + currency: ['XPF'], + languages: ['fr'], + }, + NE: { + name: 'Niger', + native: 'Niger', + phone: [227], + continent: 'AF', + capital: 'Niamey', + currency: ['XOF'], + languages: ['fr'], + }, + NF: { + name: 'Norfolk Island', + native: 'Norfolk Island', + phone: [672], + continent: 'OC', + capital: 'Kingston', + currency: ['AUD'], + languages: ['en'], + }, + NG: { + name: 'Nigeria', + native: 'Nigeria', + phone: [234], + continent: 'AF', + capital: 'Abuja', + currency: ['NGN'], + languages: ['en'], + }, + NI: { + name: 'Nicaragua', + native: 'Nicaragua', + phone: [505], + continent: 'NA', + capital: 'Managua', + currency: ['NIO'], + languages: ['es'], + }, + NL: { + name: 'Netherlands', + native: 'Nederland', + phone: [31], + continent: 'EU', + capital: 'Amsterdam', + currency: ['EUR'], + languages: ['nl'], + }, + NO: { + name: 'Norway', + native: 'Norge', + phone: [47], + continent: 'EU', + capital: 'Oslo', + currency: ['NOK'], + languages: ['no', 'nb', 'nn'], + }, + NP: { + name: 'Nepal', + native: 'नपल', + phone: [977], + continent: 'AS', + capital: 'Kathmandu', + currency: ['NPR'], + languages: ['ne'], + }, + NR: { + name: 'Nauru', + native: 'Nauru', + phone: [674], + continent: 'OC', + capital: 'Yaren', + currency: ['AUD'], + languages: ['en', 'na'], + }, + NU: { + name: 'Niue', + native: 'Niuē', + phone: [683], + continent: 'OC', + capital: 'Alofi', + currency: ['NZD'], + languages: ['en'], + }, + NZ: { + name: 'New Zealand', + native: 'New Zealand', + phone: [64], + continent: 'OC', + capital: 'Wellington', + currency: ['NZD'], + languages: ['en', 'mi'], + }, + OM: { + name: 'Oman', + native: 'عمان', + phone: [968], + continent: 'AS', + capital: 'Muscat', + currency: ['OMR'], + languages: ['ar'], + }, + PA: { + name: 'Panama', + native: 'Panamá', + phone: [507], + continent: 'NA', + capital: 'Panama City', + currency: ['PAB', 'USD'], + languages: ['es'], + }, + PE: { + name: 'Peru', + native: 'Perú', + phone: [51], + continent: 'SA', + capital: 'Lima', + currency: ['PEN'], + languages: ['es'], + }, + PF: { + name: 'French Polynesia', + native: 'Polynésie française', + phone: [689], + continent: 'OC', + capital: 'Papeetē', + currency: ['XPF'], + languages: ['fr'], + }, + PG: { + name: 'Papua New Guinea', + native: 'Papua Niugini', + phone: [675], + continent: 'OC', + capital: 'Port Moresby', + currency: ['PGK'], + languages: ['en'], + }, + PH: { + name: 'Philippines', + native: 'Pilipinas', + phone: [63], + continent: 'AS', + capital: 'Manila', + currency: ['PHP'], + languages: ['en'], + }, + PK: { + name: 'Pakistan', + native: 'Pakistan', + phone: [92], + continent: 'AS', + capital: 'Islamabad', + currency: ['PKR'], + languages: ['en', 'ur'], + }, + PL: { + name: 'Poland', + native: 'Polska', + phone: [48], + continent: 'EU', + capital: 'Warsaw', + currency: ['PLN'], + languages: ['pl'], + }, + PM: { + name: 'Saint Pierre and Miquelon', + native: 'Saint-Pierre-et-Miquelon', + phone: [508], + continent: 'NA', + capital: 'Saint-Pierre', + currency: ['EUR'], + languages: ['fr'], + }, + PN: { + name: 'Pitcairn Islands', + native: 'Pitcairn Islands', + phone: [64], + continent: 'OC', + capital: 'Adamstown', + currency: ['NZD'], + languages: ['en'], + }, + PR: { + name: 'Puerto Rico', + native: 'Puerto Rico', + phone: [1787, 1939], + continent: 'NA', + capital: 'San Juan', + currency: ['USD'], + languages: ['es', 'en'], + }, + PS: { + name: 'Palestine', + native: 'فلسطين', + phone: [970], + continent: 'AS', + capital: 'Ramallah', + currency: ['ILS'], + languages: ['ar'], + }, + PT: { + name: 'Portugal', + native: 'Portugal', + phone: [351], + continent: 'EU', + capital: 'Lisbon', + currency: ['EUR'], + languages: ['pt'], + }, + PW: { + name: 'Palau', + native: 'Palau', + phone: [680], + continent: 'OC', + capital: 'Ngerulmud', + currency: ['USD'], + languages: ['en'], + }, + PY: { + name: 'Paraguay', + native: 'Paraguay', + phone: [595], + continent: 'SA', + capital: 'Asunción', + currency: ['PYG'], + languages: ['es', 'gn'], + }, + QA: { + name: 'Qatar', + native: 'قطر', + phone: [974], + continent: 'AS', + capital: 'Doha', + currency: ['QAR'], + languages: ['ar'], + }, + RE: { + name: 'Réunion', + native: 'La Réunion', + phone: [262], + continent: 'AF', + capital: 'Saint-Denis', + currency: ['EUR'], + languages: ['fr'], + }, + RO: { + name: 'Romania', + native: 'România', + phone: [40], + continent: 'EU', + capital: 'Bucharest', + currency: ['RON'], + languages: ['ro'], + }, + RS: { + name: 'Serbia', + native: 'Србија', + phone: [381], + continent: 'EU', + capital: 'Belgrade', + currency: ['RSD'], + languages: ['sr'], + }, + RU: { + name: 'Russia', + native: 'Россия', + phone: [7], + continent: 'EU', + continents: ['AS', 'EU'], + capital: 'Moscow', + currency: ['RUB'], + languages: ['ru'], + }, + RW: { + name: 'Rwanda', + native: 'Rwanda', + phone: [250], + continent: 'AF', + capital: 'Kigali', + currency: ['RWF'], + languages: ['rw', 'en', 'fr'], + }, + SA: { + name: 'Saudi Arabia', + native: 'العربية السعودية', + phone: [966], + continent: 'AS', + capital: 'Riyadh', + currency: ['SAR'], + languages: ['ar'], + }, + SB: { + name: 'Solomon Islands', + native: 'Solomon Islands', + phone: [677], + continent: 'OC', + capital: 'Honiara', + currency: ['SBD'], + languages: ['en'], + }, + SC: { + name: 'Seychelles', + native: 'Seychelles', + phone: [248], + continent: 'AF', + capital: 'Victoria', + currency: ['SCR'], + languages: ['fr', 'en'], + }, + SD: { + name: 'Sudan', + native: 'السودان', + phone: [249], + continent: 'AF', + capital: 'Khartoum', + currency: ['SDG'], + languages: ['ar', 'en'], + }, + SE: { + name: 'Sweden', + native: 'Sverige', + phone: [46], + continent: 'EU', + capital: 'Stockholm', + currency: ['SEK'], + languages: ['sv'], + }, + SG: { + name: 'Singapore', + native: 'Singapore', + phone: [65], + continent: 'AS', + capital: 'Singapore', + currency: ['SGD'], + languages: ['en', 'ms', 'ta', 'zh'], + }, + SH: { + name: 'Saint Helena', + native: 'Saint Helena', + phone: [290], + continent: 'AF', + capital: 'Jamestown', + currency: ['SHP'], + languages: ['en'], + }, + SI: { + name: 'Slovenia', + native: 'Slovenija', + phone: [386], + continent: 'EU', + capital: 'Ljubljana', + currency: ['EUR'], + languages: ['sl'], + }, + SJ: { + name: 'Svalbard and Jan Mayen', + native: 'Svalbard og Jan Mayen', + phone: [4779], + continent: 'EU', + capital: 'Longyearbyen', + currency: ['NOK'], + languages: ['no'], + }, + SK: { + name: 'Slovakia', + native: 'Slovensko', + phone: [421], + continent: 'EU', + capital: 'Bratislava', + currency: ['EUR'], + languages: ['sk'], + }, + SL: { + name: 'Sierra Leone', + native: 'Sierra Leone', + phone: [232], + continent: 'AF', + capital: 'Freetown', + currency: ['SLL'], + languages: ['en'], + }, + SM: { + name: 'San Marino', + native: 'San Marino', + phone: [378], + continent: 'EU', + capital: 'City of San Marino', + currency: ['EUR'], + languages: ['it'], + }, + SN: { + name: 'Senegal', + native: 'Sénégal', + phone: [221], + continent: 'AF', + capital: 'Dakar', + currency: ['XOF'], + languages: ['fr'], + }, + SO: { + name: 'Somalia', + native: 'Soomaaliya', + phone: [252], + continent: 'AF', + capital: 'Mogadishu', + currency: ['SOS'], + languages: ['so', 'ar'], + }, + SR: { + name: 'Suriname', + native: 'Suriname', + phone: [597], + continent: 'SA', + capital: 'Paramaribo', + currency: ['SRD'], + languages: ['nl'], + }, + SS: { + name: 'South Sudan', + native: 'South Sudan', + phone: [211], + continent: 'AF', + capital: 'Juba', + currency: ['SSP'], + languages: ['en'], + }, + ST: { + name: 'São Tomé and Príncipe', + native: 'São Tomé e Príncipe', + phone: [239], + continent: 'AF', + capital: 'São Tomé', + currency: ['STN'], + languages: ['pt'], + }, + SV: { + name: 'El Salvador', + native: 'El Salvador', + phone: [503], + continent: 'NA', + capital: 'San Salvador', + currency: ['SVC', 'USD'], + languages: ['es'], + }, + SX: { + name: 'Sint Maarten', + native: 'Sint Maarten', + phone: [1721], + continent: 'NA', + capital: 'Philipsburg', + currency: ['ANG'], + languages: ['nl', 'en'], + }, + SY: { + name: 'Syria', + native: 'سوريا', + phone: [963], + continent: 'AS', + capital: 'Damascus', + currency: ['SYP'], + languages: ['ar'], + }, + SZ: { + name: 'Swaziland', + native: 'Swaziland', + phone: [268], + continent: 'AF', + capital: 'Lobamba', + currency: ['SZL'], + languages: ['en', 'ss'], + }, + TC: { + name: 'Turks and Caicos Islands', + native: 'Turks and Caicos Islands', + phone: [1649], + continent: 'NA', + capital: 'Cockburn Town', + currency: ['USD'], + languages: ['en'], + }, + TD: { + name: 'Chad', + native: 'Tchad', + phone: [235], + continent: 'AF', + capital: "N'Djamena", + currency: ['XAF'], + languages: ['fr', 'ar'], + }, + TF: { + name: 'French Southern Territories', + native: 'Territoire des Terres australes et antarctiques fr', + phone: [262], + continent: 'AN', + capital: 'Port-aux-Français', + currency: ['EUR'], + languages: ['fr'], + }, + TG: { + name: 'Togo', + native: 'Togo', + phone: [228], + continent: 'AF', + capital: 'Lomé', + currency: ['XOF'], + languages: ['fr'], + }, + TH: { + name: 'Thailand', + native: 'ประเทศไทย', + phone: [66], + continent: 'AS', + capital: 'Bangkok', + currency: ['THB'], + languages: ['th'], + }, + TJ: { + name: 'Tajikistan', + native: 'Тоҷикистон', + phone: [992], + continent: 'AS', + capital: 'Dushanbe', + currency: ['TJS'], + languages: ['tg', 'ru'], + }, + TK: { + name: 'Tokelau', + native: 'Tokelau', + phone: [690], + continent: 'OC', + capital: 'Fakaofo', + currency: ['NZD'], + languages: ['en'], + }, + TL: { + name: 'East Timor', + native: 'Timor-Leste', + phone: [670], + continent: 'OC', + capital: 'Dili', + currency: ['USD'], + languages: ['pt'], + }, + TM: { + name: 'Turkmenistan', + native: 'Türkmenistan', + phone: [993], + continent: 'AS', + capital: 'Ashgabat', + currency: ['TMT'], + languages: ['tk', 'ru'], + }, + TN: { + name: 'Tunisia', + native: 'تونس', + phone: [216], + continent: 'AF', + capital: 'Tunis', + currency: ['TND'], + languages: ['ar'], + }, + TO: { + name: 'Tonga', + native: 'Tonga', + phone: [676], + continent: 'OC', + capital: "Nuku'alofa", + currency: ['TOP'], + languages: ['en', 'to'], + }, + TR: { + name: 'Turkey', + native: 'Türkiye', + phone: [90], + continent: 'AS', + continents: ['AS', 'EU'], + capital: 'Ankara', + currency: ['TRY'], + languages: ['tr'], + }, + TT: { + name: 'Trinidad and Tobago', + native: 'Trinidad and Tobago', + phone: [1868], + continent: 'NA', + capital: 'Port of Spain', + currency: ['TTD'], + languages: ['en'], + }, + TV: { + name: 'Tuvalu', + native: 'Tuvalu', + phone: [688], + continent: 'OC', + capital: 'Funafuti', + currency: ['AUD'], + languages: ['en'], + }, + TW: { + name: 'Taiwan', + native: '臺灣', + phone: [886], + continent: 'AS', + capital: 'Taipei', + currency: ['TWD'], + languages: ['zh'], + }, + TZ: { + name: 'Tanzania', + native: 'Tanzania', + phone: [255], + continent: 'AF', + capital: 'Dodoma', + currency: ['TZS'], + languages: ['sw', 'en'], + }, + UA: { + name: 'Ukraine', + native: 'Україна', + phone: [380], + continent: 'EU', + capital: 'Kyiv', + currency: ['UAH'], + languages: ['uk'], + }, + UG: { + name: 'Uganda', + native: 'Uganda', + phone: [256], + continent: 'AF', + capital: 'Kampala', + currency: ['UGX'], + languages: ['en', 'sw'], + }, + UM: { + name: 'U.S. Minor Outlying Islands', + native: 'United States Minor Outlying Islands', + phone: [1], + continent: 'OC', + capital: '', + currency: ['USD'], + languages: ['en'], + }, + US: { + name: 'United States', + native: 'United States', + phone: [1], + continent: 'NA', + capital: 'Washington D.C.', + currency: ['USD', 'USN', 'USS'], + languages: ['en'], + }, + UY: { + name: 'Uruguay', + native: 'Uruguay', + phone: [598], + continent: 'SA', + capital: 'Montevideo', + currency: ['UYI', 'UYU'], + languages: ['es'], + }, + UZ: { + name: 'Uzbekistan', + native: 'O‘zbekiston', + phone: [998], + continent: 'AS', + capital: 'Tashkent', + currency: ['UZS'], + languages: ['uz', 'ru'], + }, + VA: { + name: 'Vatican City', + native: 'Vaticano', + phone: [379], + continent: 'EU', + capital: 'Vatican City', + currency: ['EUR'], + languages: ['it', 'la'], + }, + VC: { + name: 'Saint Vincent and the Grenadines', + native: 'Saint Vincent and the Grenadines', + phone: [1784], + continent: 'NA', + capital: 'Kingstown', + currency: ['XCD'], + languages: ['en'], + }, + VE: { + name: 'Venezuela', + native: 'Venezuela', + phone: [58], + continent: 'SA', + capital: 'Caracas', + currency: ['VES'], + languages: ['es'], + }, + VG: { + name: 'British Virgin Islands', + native: 'British Virgin Islands', + phone: [1284], + continent: 'NA', + capital: 'Road Town', + currency: ['USD'], + languages: ['en'], + }, + VI: { + name: 'U.S. Virgin Islands', + native: 'United States Virgin Islands', + phone: [1340], + continent: 'NA', + capital: 'Charlotte Amalie', + currency: ['USD'], + languages: ['en'], + }, + VN: { + name: 'Vietnam', + native: 'Việt Nam', + phone: [84], + continent: 'AS', + capital: 'Hanoi', + currency: ['VND'], + languages: ['vi'], + }, + VU: { + name: 'Vanuatu', + native: 'Vanuatu', + phone: [678], + continent: 'OC', + capital: 'Port Vila', + currency: ['VUV'], + languages: ['bi', 'en', 'fr'], + }, + WF: { + name: 'Wallis and Futuna', + native: 'Wallis et Futuna', + phone: [681], + continent: 'OC', + capital: 'Mata-Utu', + currency: ['XPF'], + languages: ['fr'], + }, + WS: { + name: 'Samoa', + native: 'Samoa', + phone: [685], + continent: 'OC', + capital: 'Apia', + currency: ['WST'], + languages: ['sm', 'en'], + }, + XK: { + name: 'Kosovo', + native: 'Republika e Kosovës', + phone: [377, 381, 383, 386], + continent: 'EU', + capital: 'Pristina', + currency: ['EUR'], + languages: ['sq', 'sr'], + }, + YE: { + name: 'Yemen', + native: 'اليَمَن', + phone: [967], + continent: 'AS', + capital: "Sana'a", + currency: ['YER'], + languages: ['ar'], + }, + YT: { + name: 'Mayotte', + native: 'Mayotte', + phone: [262], + continent: 'AF', + capital: 'Mamoudzou', + currency: ['EUR'], + languages: ['fr'], + }, + ZA: { + name: 'South Africa', + native: 'South Africa', + phone: [27], + continent: 'AF', + capital: 'Pretoria', + currency: ['ZAR'], + languages: ['af', 'en', 'nr', 'st', 'ss', 'tn', 'ts', 've', 'xh', 'zu'], + }, + ZM: { + name: 'Zambia', + native: 'Zambia', + phone: [260], + continent: 'AF', + capital: 'Lusaka', + currency: ['ZMW'], + languages: ['en'], + }, + ZW: { + name: 'Zimbabwe', + native: 'Zimbabwe', + phone: [263], + continent: 'AF', + capital: 'Harare', + currency: ['USD', 'ZAR', 'BWP', 'GBP', 'AUD', 'CNY', 'INR', 'JPY'], + languages: ['en', 'sn', 'nd'], + }, +}; diff --git a/packages/webapp/src/constants/languagesOptions.tsx b/packages/webapp/src/constants/languagesOptions.tsx index 1286d8fe1..f0b1a7317 100644 --- a/packages/webapp/src/constants/languagesOptions.tsx +++ b/packages/webapp/src/constants/languagesOptions.tsx @@ -3,5 +3,4 @@ import intl from 'react-intl-universal'; export const getLanguages = () => [ { name: intl.get('english'), value: 'en' }, - { name: intl.get('arabic'), value: 'ar' }, ]; diff --git a/packages/webapp/src/containers/Authentication/AuthCopyright.tsx b/packages/webapp/src/containers/Authentication/AuthCopyright.tsx index 6fcc71fd4..3f77a44d4 100644 --- a/packages/webapp/src/containers/Authentication/AuthCopyright.tsx +++ b/packages/webapp/src/containers/Authentication/AuthCopyright.tsx @@ -1,20 +1,7 @@ // @ts-nocheck import React from 'react'; -import moment from 'moment'; -import intl from 'react-intl-universal'; import { Icon } from '@/components/Icon'; export default function AuthCopyright() { - return ( - - ); + return ; } diff --git a/packages/webapp/src/containers/Authentication/AuthInsider.tsx b/packages/webapp/src/containers/Authentication/AuthInsider.tsx index 9a683ab3f..cef34393c 100644 --- a/packages/webapp/src/containers/Authentication/AuthInsider.tsx +++ b/packages/webapp/src/containers/Authentication/AuthInsider.tsx @@ -1,6 +1,8 @@ // @ts-nocheck import React from 'react'; +import styled from 'styled-components'; import AuthCopyright from './AuthCopyright'; +import { AuthInsiderContent, AuthInsiderCopyright } from './_components'; /** * Authentication insider page. @@ -9,16 +11,21 @@ export default function AuthInsider({ logo = true, copyright = true, children, + classNames, }) { return ( -
-
- { children } -
+ + + {children} + - -
+ {copyright && ( + + + + )} + ); } + +const AuthInsiderContentWrap = styled.div``; diff --git a/packages/webapp/src/containers/Authentication/Authentication.tsx b/packages/webapp/src/containers/Authentication/Authentication.tsx new file mode 100644 index 000000000..1fc2515e9 --- /dev/null +++ b/packages/webapp/src/containers/Authentication/Authentication.tsx @@ -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 ; + } + return ( + + + + + + + + + + + {authenticationRoutes.map((route, index) => ( + + ))} + + + + + + + ); +} + +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; +`; diff --git a/packages/webapp/src/containers/Authentication/InviteAcceptForm.tsx b/packages/webapp/src/containers/Authentication/InviteAcceptForm.tsx index 4d6b00799..d4a000c4c 100644 --- a/packages/webapp/src/containers/Authentication/InviteAcceptForm.tsx +++ b/packages/webapp/src/containers/Authentication/InviteAcceptForm.tsx @@ -4,13 +4,21 @@ import intl from 'react-intl-universal'; import { Formik } from 'formik'; import { useHistory } from 'react-router-dom'; import { Intent, Position } from '@blueprintjs/core'; -import { FormattedMessage as T } from '@/components'; import { isEmpty } from 'lodash'; import { useInviteAcceptContext } from './InviteAcceptProvider'; import { AppToaster } from '@/components'; import { InviteAcceptSchema } from './utils'; import InviteAcceptFormContent from './InviteAcceptFormContent'; +import { AuthInsiderCard } from './_components'; + +const initialValues = { + organization_name: '', + invited_email: '', + first_name: '', + last_name: '', + password: '', +}; export default function InviteAcceptForm() { const history = useHistory(); @@ -19,9 +27,8 @@ export default function InviteAcceptForm() { const { inviteAcceptMutate, inviteMeta, token } = useInviteAcceptContext(); // Invite value. - const inviteValue = { - organization_name: '', - invited_email: '', + const inviteFormValue = { + ...initialValues, ...(!isEmpty(inviteMeta) ? { invited_email: inviteMeta.email, @@ -33,19 +40,17 @@ export default function InviteAcceptForm() { // Handle form submitting. const handleSubmit = (values, { setSubmitting, setErrors }) => { inviteAcceptMutate([values, token]) - .then((response) => { + .then(() => { AppToaster.show({ message: intl.getHTML( 'congrats_your_account_has_been_created_and_invited', { - organization_name: inviteValue.organization_name, + organization_name: inviteMeta.organizationName, }, ), - intent: Intent.SUCCESS, }); history.push('/auth/login'); - setSubmitting(false); }) .catch( ({ @@ -80,23 +85,13 @@ export default function InviteAcceptForm() { }; return ( -
-
-

- -

-

- {' '} - {inviteValue.organization_name} -

-
- + -
+ ); } diff --git a/packages/webapp/src/containers/Authentication/InviteAcceptFormContent.tsx b/packages/webapp/src/containers/Authentication/InviteAcceptFormContent.tsx index 9b800be76..7a517112e 100644 --- a/packages/webapp/src/containers/Authentication/InviteAcceptFormContent.tsx +++ b/packages/webapp/src/containers/Authentication/InviteAcceptFormContent.tsx @@ -1,110 +1,73 @@ // @ts-nocheck -import React from 'react'; +import React, { useState } from 'react'; import intl from 'react-intl-universal'; -import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; -import { Form, ErrorMessage, FastField, useFormikContext } from 'formik'; +import { Button, InputGroup, Intent } from '@blueprintjs/core'; +import { Form, useFormikContext } from 'formik'; import { Link } from 'react-router-dom'; -import { Col, Row, FormattedMessage as T } from '@/components'; -import { inputIntent } from '@/utils'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import styled from 'styled-components'; + +import { + Col, + FFormGroup, + FInputGroup, + Row, + FormattedMessage as T, +} from '@/components'; import { useInviteAcceptContext } from './InviteAcceptProvider'; -import { PasswordRevealer } from './components'; +import { AuthSubmitButton } from './_components'; /** * Invite user form. */ export default function InviteUserFormContent() { - // Invite accept context. - const { inviteMeta } = useInviteAcceptContext(); + const [showPassword, setShowPassword] = useState(false); - // Formik context. + const { inviteMeta } = useInviteAcceptContext(); const { isSubmitting } = useFormikContext(); - const [passwordType, setPasswordType] = React.useState('password'); - // Handle password revealer changing. - const handlePasswordRevealerChange = React.useCallback( - (shown) => { - const type = shown ? 'text' : 'password'; - setPasswordType(type); - }, - [setPasswordType], + const handleLockClick = () => { + setShowPassword(!showPassword); + }; + const lockButton = ( + + - + + + ); } + +const InviteAcceptFooterParagraphs = styled.div` + opacity: 0.8; +`; + +const InviteAuthSubmitButton = styled(AuthSubmitButton)` + margin-top: 1.6rem; +`; diff --git a/packages/webapp/src/containers/Authentication/InviteAcceptProvider.tsx b/packages/webapp/src/containers/Authentication/InviteAcceptProvider.tsx index 5df701f6e..f78043079 100644 --- a/packages/webapp/src/containers/Authentication/InviteAcceptProvider.tsx +++ b/packages/webapp/src/containers/Authentication/InviteAcceptProvider.tsx @@ -1,8 +1,8 @@ // @ts-nocheck -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useInviteMetaByToken, useAuthInviteAccept } from '@/hooks/query'; import { InviteAcceptLoading } from './components'; -import { useHistory } from 'react-router-dom'; const InviteAcceptContext = createContext(); @@ -22,11 +22,10 @@ function InviteAcceptProvider({ token, ...props }) { const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({ retry: false, }); - // History context. const history = useHistory(); - React.useEffect(() => { + useEffect(() => { if (inviteMetaError) { history.push('/auth/login'); } }, [history, inviteMetaError]); diff --git a/packages/webapp/src/containers/Authentication/Login.tsx b/packages/webapp/src/containers/Authentication/Login.tsx index 33a746fde..4032f2e67 100644 --- a/packages/webapp/src/containers/Authentication/Login.tsx +++ b/packages/webapp/src/containers/Authentication/Login.tsx @@ -1,14 +1,25 @@ // @ts-nocheck import React from 'react'; -import { Link } from 'react-router-dom'; import { Formik } from 'formik'; -import { AppToaster as Toaster, FormattedMessage as T } from '@/components'; +import { Link } from 'react-router-dom'; +import { AppToaster as Toaster, FormattedMessage as T } from '@/components'; import AuthInsider from '@/containers/Authentication/AuthInsider'; import { useAuthLogin } from '@/hooks/query'; import LoginForm from './LoginForm'; import { LoginSchema, transformLoginErrorsToToasts } from './utils'; +import { + AuthFooterLinks, + AuthFooterLink, + AuthInsiderCard, +} from './_components'; + +const initialValues = { + crediential: '', + password: '', + keepLoggedIn: false +}; /** * Login page. @@ -38,34 +49,32 @@ export default function Login() { return ( -
-
-

- -

- {/* - - {' '} - - */} -
- + + - -
+
); } + +function LoginFooterLinks() { + return ( + + + Don't have an account? Sign up + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Authentication/LoginForm.tsx b/packages/webapp/src/containers/Authentication/LoginForm.tsx index 22b806774..72d844ba1 100644 --- a/packages/webapp/src/containers/Authentication/LoginForm.tsx +++ b/packages/webapp/src/containers/Authentication/LoginForm.tsx @@ -1,89 +1,63 @@ // @ts-nocheck -import React from 'react'; -import { - Button, - InputGroup, - Intent, - FormGroup, - Checkbox, -} from '@blueprintjs/core'; -import { Form, ErrorMessage, Field } from 'formik'; -import { T } from '@/components'; -import { inputIntent } from '@/utils'; -import { PasswordRevealer } from './components'; +import React, { useState } from 'react'; +import { Button, Intent } from '@blueprintjs/core'; +import { Form } from 'formik'; +import { Tooltip2 } from '@blueprintjs/popover2'; + +import { FFormGroup, FInputGroup, FCheckbox, T } from '@/components'; +import { AuthSubmitButton } from './_components'; /** * Login form. */ export default function LoginForm({ isSubmitting }) { - const [passwordType, setPasswordType] = React.useState('password'); + const [showPassword, setShowPassword] = useState(false); // Handle password revealer changing. - const handlePasswordRevealerChange = React.useCallback( - (shown) => { - const type = shown ? 'text' : 'password'; - setPasswordType(type); - }, - [setPasswordType], + const handleLockClick = () => { + setShowPassword(!showPassword); + }; + + const lockButton = ( + + - + + + ); } diff --git a/packages/webapp/src/containers/Authentication/Register.tsx b/packages/webapp/src/containers/Authentication/Register.tsx index bdbff0d47..aba50d734 100644 --- a/packages/webapp/src/containers/Authentication/Register.tsx +++ b/packages/webapp/src/containers/Authentication/Register.tsx @@ -11,6 +11,18 @@ import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication'; import RegisterForm from './RegisterForm'; import { RegisterSchema, transformRegisterErrorsToForm } from './utils'; +import { + AuthFooterLinks, + AuthFooterLink, + AuthInsiderCard, +} from './_components'; + +const initialValues = { + first_name: '', + last_name: '', + email: '', + password: '', +}; /** * Register form. @@ -19,18 +31,6 @@ export default function RegisterUserForm() { const { mutateAsync: authLoginMutate } = useAuthLogin(); const { mutateAsync: authRegisterMutate } = useAuthRegister(); - const initialValues = useMemo( - () => ({ - first_name: '', - last_name: '', - email: '', - phone_number: '', - password: '', - country: 'LY', - }), - [], - ); - const handleSubmit = (values, { setSubmitting, setErrors }) => { authRegisterMutate(values) .then((response) => { @@ -66,24 +66,32 @@ export default function RegisterUserForm() { return ( -
-
-

- -

- - - - -
- + -
+ + +
); } + +function RegisterFooterLinks() { + return ( + + + Return to Sign In + + + + + + + + + ); +} diff --git a/packages/webapp/src/containers/Authentication/RegisterForm.tsx b/packages/webapp/src/containers/Authentication/RegisterForm.tsx index 8f44056a4..c5c591300 100644 --- a/packages/webapp/src/containers/Authentication/RegisterForm.tsx +++ b/packages/webapp/src/containers/Authentication/RegisterForm.tsx @@ -1,148 +1,101 @@ // @ts-nocheck import React from 'react'; +import { Form } from 'formik'; import intl from 'react-intl-universal'; -import { - Button, - InputGroup, - Intent, - FormGroup, - Spinner, -} from '@blueprintjs/core'; -import { ErrorMessage, Field, Form } from 'formik'; -import { FormattedMessage as T } from '@/components'; +import { Intent, Button } from '@blueprintjs/core'; import { Link } from 'react-router-dom'; -import { Row, Col, If } from '@/components'; -import { PasswordRevealer } from './components'; -import { inputIntent } from '@/utils'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import styled from 'styled-components'; + +import { + FFormGroup, + FInputGroup, + Row, + Col, + FormattedMessage as T, +} from '@/components'; +import { AuthSubmitButton, AuthenticationLoadingOverlay } from './_components'; /** * Register form. */ export default function RegisterForm({ isSubmitting }) { - const [passwordType, setPasswordType] = React.useState('password'); + const [showPassword, setShowPassword] = React.useState(false); // Handle password revealer changing. - const handlePasswordRevealerChange = React.useCallback( - (shown) => { - const type = shown ? 'text' : 'password'; - setPasswordType(type); - }, - [setPasswordType], + const handleLockClick = () => { + setShowPassword(!showPassword); + }; + + const lockButton = ( + + - - - -
- -
-
- + {isSubmitting && } + ); } + +const TermsConditionsText = styled.p` + opacity: 0.8; + margin-bottom: 1.4rem; +`; + +const RegisterFormRoot = styled(Form)` + position: relative; +`; diff --git a/packages/webapp/src/containers/Authentication/ResetPassword.tsx b/packages/webapp/src/containers/Authentication/ResetPassword.tsx index 027f2da1b..63796bcff 100644 --- a/packages/webapp/src/containers/Authentication/ResetPassword.tsx +++ b/packages/webapp/src/containers/Authentication/ResetPassword.tsx @@ -4,14 +4,23 @@ import intl from 'react-intl-universal'; import { Formik } from 'formik'; import { Intent, Position } from '@blueprintjs/core'; import { Link, useParams, useHistory } from 'react-router-dom'; -import { AppToaster, FormattedMessage as T } from '@/components'; +import { AppToaster } from '@/components'; import { useAuthResetPassword } from '@/hooks/query'; - import AuthInsider from '@/containers/Authentication/AuthInsider'; +import { + AuthFooterLink, + AuthFooterLinks, + AuthInsiderCard, +} from './_components'; import ResetPasswordForm from './ResetPasswordForm'; import { ResetPasswordSchema } from './utils'; + +const initialValues = { + password: '', + confirm_password: '', +}; /** * Reset password page. */ @@ -22,15 +31,6 @@ export default function ResetPassword() { // Authentication reset password. const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword(); - // Initial values of the form. - const initialValues = useMemo( - () => ({ - password: '', - confirm_password: '', - }), - [], - ); - // Handle the form submitting. const handleSubmit = (values, { setSubmitting }) => { authResetPasswordMutate([token, values]) @@ -64,24 +64,30 @@ export default function ResetPassword() { return ( -
-
-

- -

- {' '} - - - -
- + -
+ + +
); } + +function ResetPasswordFooterLinks() { + return ( + + + Don't have an account? Sign up + + + + Return to Sign In + + + ); +} diff --git a/packages/webapp/src/containers/Authentication/ResetPasswordForm.tsx b/packages/webapp/src/containers/Authentication/ResetPasswordForm.tsx index 6b0233a9f..fe7d0f5f4 100644 --- a/packages/webapp/src/containers/Authentication/ResetPasswordForm.tsx +++ b/packages/webapp/src/containers/Authentication/ResetPasswordForm.tsx @@ -1,9 +1,9 @@ // @ts-nocheck import React from 'react'; -import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; -import { Form, ErrorMessage, FastField } from 'formik'; -import { FormattedMessage as T } from '@/components'; -import { inputIntent } from '@/utils'; +import { Intent } from '@blueprintjs/core'; +import { Form } from 'formik'; +import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components'; +import { AuthSubmitButton } from './_components'; /** * Reset password form. @@ -11,54 +11,23 @@ import { inputIntent } from '@/utils'; export default function ResetPasswordForm({ isSubmitting }) { return (
- - {({ form, field, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - helperText={} - className={'form-group--password'} - > - - - )} - + }> + + - - {({ form, field, meta: { error, touched } }) => ( - } - labelInfo={'(again):'} - intent={inputIntent({ error, touched })} - helperText={} - className={'form-group--confirm-password'} - > - - - )} - + }> + + -
- -
+ + +
); } diff --git a/packages/webapp/src/containers/Authentication/SendResetPassword.tsx b/packages/webapp/src/containers/Authentication/SendResetPassword.tsx index dd2a67a50..8bf07809f 100644 --- a/packages/webapp/src/containers/Authentication/SendResetPassword.tsx +++ b/packages/webapp/src/containers/Authentication/SendResetPassword.tsx @@ -5,33 +5,32 @@ import { Formik } from 'formik'; import { Link, useHistory } from 'react-router-dom'; import { Intent } from '@blueprintjs/core'; -import { AppToaster, FormattedMessage as T } from '@/components'; +import { AppToaster } from '@/components'; import { useAuthSendResetPassword } from '@/hooks/query'; import SendResetPasswordForm from './SendResetPasswordForm'; +import { + AuthFooterLink, + AuthFooterLinks, + AuthInsiderCard, +} from './_components'; import { SendResetPasswordSchema, transformSendResetPassErrorsToToasts, } from './utils'; - import AuthInsider from '@/containers/Authentication/AuthInsider'; +const initialValues = { + crediential: '', +}; + /** * Send reset password page. */ export default function SendResetPassword({ requestSendResetPassword }) { const history = useHistory(); - const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword(); - // Initial values. - const initialValues = useMemo( - () => ({ - crediential: '', - }), - [], - ); - // Handle form submitting. const handleSubmit = (values, { setSubmitting }) => { sendResetPasswordMutate({ email: values.crediential }) @@ -61,28 +60,30 @@ export default function SendResetPassword({ requestSendResetPassword }) { return ( -
-
-

- -

-

- -

-
- + - -
+ + +
); } + +function SendResetPasswordFooterLinks() { + return ( + + + Don't have an account? Sign up + + + + Return to Sign In + + + ); +} diff --git a/packages/webapp/src/containers/Authentication/SendResetPasswordForm.tsx b/packages/webapp/src/containers/Authentication/SendResetPasswordForm.tsx index d8439e971..3f2718d59 100644 --- a/packages/webapp/src/containers/Authentication/SendResetPasswordForm.tsx +++ b/packages/webapp/src/containers/Authentication/SendResetPasswordForm.tsx @@ -1,43 +1,41 @@ // @ts-nocheck import React from 'react'; -import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; -import { Form, ErrorMessage, FastField } from 'formik'; -import { FormattedMessage as T } from '@/components'; -import { inputIntent } from '@/utils'; +import { Intent } from '@blueprintjs/core'; +import { Form } from 'formik'; +import styled from 'styled-components'; + +import { FInputGroup, FFormGroup, FormattedMessage as T } from '@/components'; +import { AuthSubmitButton } from './_components'; /** * Send reset password form. */ export default function SendResetPasswordForm({ isSubmitting }) { return ( -
- - {({ form, field, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - helperText={} - className={'form-group--crediential'} - > - - - )} - + + + Enter the email address associated with your account and we'll send you + a link to reset your password. + -
- -
+ }> + + + + + Reset Password +
); } + +const TopParagraph = styled.p` + margin-bottom: 1.6rem; + opacity: 0.8; +`; diff --git a/packages/webapp/src/containers/Authentication/_components.tsx b/packages/webapp/src/containers/Authentication/_components.tsx new file mode 100644 index 000000000..0199ad254 --- /dev/null +++ b/packages/webapp/src/containers/Authentication/_components.tsx @@ -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 ( + + + + ); +} + +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); + } + } +`; diff --git a/packages/webapp/src/containers/Authentication/components.tsx b/packages/webapp/src/containers/Authentication/components.tsx index a9e36dbb0..e7c5ce381 100644 --- a/packages/webapp/src/containers/Authentication/components.tsx +++ b/packages/webapp/src/containers/Authentication/components.tsx @@ -1,57 +1,42 @@ // @ts-nocheck import React from 'react'; -import ContentLoader from 'react-content-loader'; -import { If, Icon, FormattedMessage as T } from '@/components'; -import { saveInvoke } from '@/utils'; - -export function PasswordRevealer({ defaultShown = false, onChange }) { - const [shown, setShown] = React.useState(defaultShown); - - const handleClick = () => { - setShown(!shown); - saveInvoke(onChange, !shown); - }; - - return ( - - - {' '} - - - - - - {' '} - - - - - - ); -} +import styled from 'styled-components'; +import { AuthInsiderCard } from './_components'; +import { Skeleton } from '@/components'; /** * Invite accept loading space. */ -export function InviteAcceptLoading({ isLoading, children, ...props }) { +export function InviteAcceptLoading({ isLoading, children }) { return isLoading ? ( - - - - - - - - + + + + + + + ) : ( children ); } + +function SkeletonField() { + return ( + + XXXX XXXX + XXXX XXXX XXXX XXXX + + ); +} + +const Fields = styled.div` + display: flex; + flex-direction: column; + gap: 20px; +`; +const SkeletonFieldRoot = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; diff --git a/packages/webapp/src/containers/Authentication/utils.tsx b/packages/webapp/src/containers/Authentication/utils.tsx index 1cb6e694b..2d8edfafd 100644 --- a/packages/webapp/src/containers/Authentication/utils.tsx +++ b/packages/webapp/src/containers/Authentication/utils.tsx @@ -15,42 +15,19 @@ const REGISTER_ERRORS = { }; export const LoginSchema = Yup.object().shape({ - crediential: Yup.string() - .required() - .email() - .label(intl.get('email')), - password: Yup.string() - .required() - .min(4) - .label(intl.get('password')), + crediential: Yup.string().required().email().label(intl.get('email')), + password: Yup.string().required().min(4).label(intl.get('password')), }); export const RegisterSchema = Yup.object().shape({ - first_name: Yup.string() - .required() - .label(intl.get('first_name_')), - last_name: Yup.string() - .required() - .label(intl.get('last_name_')), - email: Yup.string() - .email() - .required() - .label(intl.get('email')), - phone_number: Yup.string() - .matches() - .required() - .label(intl.get('phone_number_')), - password: Yup.string() - .min(4) - .required() - .label(intl.get('password')), + first_name: Yup.string().required().label(intl.get('first_name_')), + last_name: Yup.string().required().label(intl.get('last_name_')), + email: Yup.string().email().required().label(intl.get('email')), + password: Yup.string().min(4).required().label(intl.get('password')), }); export const ResetPasswordSchema = Yup.object().shape({ - password: Yup.string() - .min(4) - .required() - .label(intl.get('password')), + password: Yup.string().min(4).required().label(intl.get('password')), confirm_password: Yup.string() .oneOf([Yup.ref('password'), null]) .required() @@ -59,27 +36,13 @@ export const ResetPasswordSchema = Yup.object().shape({ // Validation schema. export const SendResetPasswordSchema = Yup.object().shape({ - crediential: Yup.string() - .required() - .email() - .label(intl.get('email')), + crediential: Yup.string().required().email().label(intl.get('email')), }); export const InviteAcceptSchema = Yup.object().shape({ - first_name: Yup.string() - .required() - .label(intl.get('first_name_')), - last_name: Yup.string() - .required() - .label(intl.get('last_name_')), - phone_number: Yup.string() - .matches() - .required() - .label(intl.get('phone_number')), - password: Yup.string() - .min(4) - .required() - .label(intl.get('password')), + first_name: Yup.string().required().label(intl.get('first_name_')), + last_name: Yup.string().required().label(intl.get('last_name_')), + password: Yup.string().min(4).required().label(intl.get('password')), }); export const transformSendResetPassErrorsToToasts = (errors) => { @@ -92,7 +55,7 @@ export const transformSendResetPassErrorsToToasts = (errors) => { }); } return toastBuilders; -} +}; export const transformLoginErrorsToToasts = (errors) => { const toastBuilders = []; @@ -109,25 +72,25 @@ export const transformLoginErrorsToToasts = (errors) => { intent: Intent.DANGER, }); } - if ( - errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS) - ) { + if (errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)) { toastBuilders.push({ message: intl.get('your_account_has_been_locked'), intent: Intent.DANGER, }); } return toastBuilders; -} +}; export const transformRegisterErrorsToForm = (errors) => { const formErrors = {}; if (errors.some((e) => e.type === REGISTER_ERRORS.PHONE_NUMBER_EXISTS)) { - formErrors.phone_number = intl.get('the_phone_number_already_used_in_another_account'); + formErrors.phone_number = intl.get( + 'the_phone_number_already_used_in_another_account', + ); } if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) { formErrors.email = intl.get('the_email_already_used_in_another_account'); } return formErrors; -} \ No newline at end of file +}; diff --git a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.schema.tsx b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.schema.tsx index a17a891b8..ec890a35e 100644 --- a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.schema.tsx +++ b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.schema.tsx @@ -6,10 +6,6 @@ const Schema = Yup.object().shape({ email: Yup.string().email().required().label(intl.get('email')), first_name: Yup.string().required().label(intl.get('first_name_')), last_name: Yup.string().required().label(intl.get('last_name_')), - phone_number: Yup.string() - .matches() - .required() - .label(intl.get('phone_number_')), role_id: Yup.string().required().label(intl.get('roles.label.role_name_')), }); diff --git a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.tsx b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.tsx index 76dd6eae3..79ccc5d99 100644 --- a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.tsx +++ b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserForm.tsx @@ -13,7 +13,14 @@ import UserFormContent from './UserFormContent'; import { useUserFormContext } from './UserFormProvider'; import { transformErrors } from './utils'; -import { compose, objectKeysTransform } from '@/utils'; +import { compose, objectKeysTransform, transformToForm } from '@/utils'; + +const initialValues = { + first_name: '', + last_name: '', + email: '', + role_id: '', +}; /** * User form. @@ -27,12 +34,9 @@ function UserForm({ const { dialogName, user, userId, isEditMode, EditUserMutate } = useUserFormContext(); - const initialValues = { - ...(isEditMode && - pick( - objectKeysTransform(user, snakeCase), - Object.keys(UserFormSchema.fields), - )), + const initialFormValues = { + ...initialValues, + ...(isEditMode && transformToForm(user, initialValues)), }; const handleSubmit = (values, { setSubmitting, setErrors }) => { @@ -68,7 +72,7 @@ function UserForm({ return ( diff --git a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserFormContent.tsx b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserFormContent.tsx index 3114ed6bd..54f3af717 100644 --- a/packages/webapp/src/containers/Dialogs/UserFormDialog/UserFormContent.tsx +++ b/packages/webapp/src/containers/Dialogs/UserFormDialog/UserFormContent.tsx @@ -8,9 +8,10 @@ import { Button, } from '@blueprintjs/core'; import { FastField, Form, useFormikContext, ErrorMessage } from 'formik'; -import { FormattedMessage as T } from '@/components'; -import { CLASSES } from '@/constants/classes'; import classNames from 'classnames'; + +import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components'; +import { CLASSES } from '@/constants/classes'; import { inputIntent } from '@/utils'; import { ListSelect, FieldRequiredHint } from '@/components'; import { useUserFormContext } from './UserFormProvider'; @@ -23,6 +24,7 @@ import { UserFormCalloutAlerts } from './components'; */ function UserFormContent({ calloutCode, + // #withDialogActions closeDialog, }) { @@ -39,60 +41,20 @@ function UserFormContent({ {/* ----------- Email ----------- */} - - {({ field, meta: { error, touched } }) => ( - } - labelInfo={} - className={classNames('form-group--email', CLASSES.FILL)} - intent={inputIntent({ error, touched })} - helperText={} - > - - - )} - + }> + + {/* ----------- First name ----------- */} - - {({ form, field, meta: { error, touched } }) => ( - } - labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - > - - - )} - + }> + + {/* ----------- Last name ----------- */} - - {({ form, field, meta: { error, touched } }) => ( - } - labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - > - - - )} - - {/* ----------- Phone name ----------- */} - - {({ form, field, meta: { error, touched } }) => ( - } - labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - > - - - )} - + }> + + + {/* ----------- Role name ----------- */} {({ form, field: { value }, meta: { error, touched } }) => ( @@ -127,7 +89,12 @@ function UserFormContent({ - diff --git a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx index faf729473..bf225a1fc 100644 --- a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx +++ b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx @@ -15,14 +15,15 @@ import { } from '@/components'; import { inputIntent } from '@/utils'; import { CLASSES } from '@/constants/classes'; -import { getCountries } from '@/constants/countries'; import { getAllCurrenciesOptions } from '@/constants/currencies'; import { getFiscalYear } from '@/constants/fiscalYearOptions'; import { getLanguages } from '@/constants/languagesOptions'; import { useGeneralFormContext } from './GeneralFormProvider'; +import { getAllCountries } from '@/utils/countries'; import { shouldBaseCurrencyUpdate } from './utils'; +const Countries = getAllCountries(); /** * Preferences general form. */ @@ -30,7 +31,6 @@ export default function PreferencesGeneralForm({ isSubmitting }) { const history = useHistory(); const FiscalYear = getFiscalYear(); - const Countries = getCountries(); const Languages = getLanguages(); const Currencies = getAllCurrenciesOptions(); diff --git a/packages/webapp/src/containers/Setup/SetupOrganizationForm.tsx b/packages/webapp/src/containers/Setup/SetupOrganizationForm.tsx index 1ef1fb00c..bec282215 100644 --- a/packages/webapp/src/containers/Setup/SetupOrganizationForm.tsx +++ b/packages/webapp/src/containers/Setup/SetupOrganizationForm.tsx @@ -5,15 +5,12 @@ import { Button, Intent, FormGroup, - InputGroup, MenuItem, Classes, } from '@blueprintjs/core'; import classNames from 'classnames'; import { TimezonePicker } from '@blueprintjs/timezone'; -import useAutofocus from '@/hooks/useAutofocus' -import { FormattedMessage as T } from '@/components'; -import { getCountries } from '@/constants/countries'; +import { FFormGroup, FInputGroup, FormattedMessage as T } from '@/components'; import { Col, Row, ListSelect } from '@/components'; import { inputIntent } from '@/utils'; @@ -21,6 +18,9 @@ import { inputIntent } from '@/utils'; import { getFiscalYear } from '@/constants/fiscalYearOptions'; import { getLanguages } from '@/constants/languagesOptions'; import { getAllCurrenciesOptions } from '@/constants/currencies'; +import { getAllCountries } from '@/utils/countries'; + +const countries = getAllCountries(); /** * Setup organization form. @@ -29,9 +29,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { const FiscalYear = getFiscalYear(); const Languages = getLanguages(); const currencies = getAllCurrenciesOptions(); - const countries = getCountries(); - - const accountRef = useAutofocus(); return (
@@ -40,22 +37,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { {/* ---------- Organization name ---------- */} - - {({ form, field, meta: { error, touched } }) => ( - } - className={'form-group--name'} - intent={inputIntent({ error, touched })} - helperText={} - > - - - )} - + }> + + {/* ---------- Location ---------- */} @@ -71,11 +55,11 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { > { - form.setFieldValue('location', value); + onItemSelect={({ countryCode }) => { + form.setFieldValue('location', countryCode); }} selectedItem={value} - selectedItemProp={'value'} + selectedItemProp={'countryCode'} defaultText={} textProp={'name'} popoverProps={{ minimal: true }} diff --git a/packages/webapp/src/containers/Setup/SetupOrganizationPage.tsx b/packages/webapp/src/containers/Setup/SetupOrganizationPage.tsx index 90457377f..7cf30db87 100644 --- a/packages/webapp/src/containers/Setup/SetupOrganizationPage.tsx +++ b/packages/webapp/src/containers/Setup/SetupOrganizationPage.tsx @@ -16,7 +16,7 @@ import { getSetupOrganizationValidation } from './SetupOrganization.schema'; // Initial values. const defaultValues = { name: '', - location: 'libya', + location: '', baseCurrency: '', language: 'en', fiscalYear: '', diff --git a/packages/webapp/src/lang/en/index.json b/packages/webapp/src/lang/en/index.json index db8dbaf6c..8a4dc1411 100644 --- a/packages/webapp/src/lang/en/index.json +++ b/packages/webapp/src/lang/en/index.json @@ -31,13 +31,14 @@ "phone_number": "Phone Number", "you_email_address_is": "You email address is", "you_will_use_this_address_to_sign_in_to_bigcapital": "You will use this address to sign in to Bigcapital.", - "signing_in_or_creating": "By signing in or creating an account, you agree with our
Terms & Conditions and Privacy Statement ", + "signing_in_or_creating": "By signing in or creating an account, you agree with our Terms & Conditions and Privacy Statement ", "and": "And", "create_account": "Create Account", "success": "Success", "register_a_new_organization": "Register a New Organization.", "organization_name": "Organization Name", "email": "Email", + "email_address": "Email Address", "register": "Register", "password_successfully_updated": "The Password for your account was successfully updated.", "choose_a_new_password": "Choose a new password", diff --git a/packages/webapp/src/style/pages/Authentication/Auth.scss b/packages/webapp/src/style/pages/Authentication/Auth.scss index 7a5a376d2..15cecf753 100644 --- a/packages/webapp/src/style/pages/Authentication/Auth.scss +++ b/packages/webapp/src/style/pages/Authentication/Auth.scss @@ -1,224 +1,32 @@ - body.authentication { background-color: #fcfdff; } -.authentication-insider { - width: 384px; - margin: 0 auto; - margin-bottom: 40px; - padding-top: 80px; - - &__logo-section { - text-align: center; - margin-bottom: 60px; - } - - &__content { - position: relative; - } - - &__footer { - .auth-copyright { - text-align: center; - font-size: 12px; - color: #666; - - .bp3-icon-bigcapital { - margin-top: 9px; - - svg { - path { - fill: #a3a3a3; - } - } - } - } - } -} - - -.authTransition{ - +.authTransition { &-enter { opacity: 0; } - + &-enter-active { opacity: 1; transition: opacity 250ms ease-in-out; } - + &-enter-done { opacity: 1; } - + &-exit { opacity: 1; } - + &-exit-active { opacity: 0.5; transition: opacity 250ms ease-in-out; } + &-exit-active { opacity: 0; display: none; } - -} - - -.authentication-page { - &__goto-bigcapital { - position: fixed; - margin-top: 30px; - margin-left: 30px; - color: #777; - } - - .bp3-input { - min-height: 40px; - } - .bp3-form-group { - margin-bottom: 25px; - } - - .bp3-form-group.has-password-revealer { - .bp3-label { - display: flex; - justify-content: space-between; - } - - .password-revealer { - .text { - font-size: 12px; - } - } - } - - .bp3-button.bp3-fill.bp3-intent-primary { - font-size: 16px; - } - - &__label-section { - margin-bottom: 30px; - color: #555; - - h3 { - font-weight: 500; - font-size: 22px; - color: #2d2b43; - margin: 0 0 12px; - } - - a { - text-decoration: underline; - color: #0040bd; - } - } - - &__form-wrapper { - width: 100%; - margin: 0 auto; - } - - &__footer-links { - padding: 9px; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - text-align: center; - margin-bottom: 1.2rem; - - a { - color: #0052cc; - } - } - - &__loading-overlay { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: rgba(252, 253, 255, 0.5); - display: flex; - justify-content: center; - } - - &__submit-button-wrap { - margin: 0px 0px 24px 0px; - - .bp3-button { - background-color: #0052cc; - min-height: 45px; - } - } - - // Login Form - // ------------------------------ - .login-form { - // width: 690px; - // margin: 0px auto; - // padding: 85px 50px; - - .checkbox { - &--remember-me { - margin: -6px 0 26px 0px; - font-size: 14px; - } - } - } - - // Register form - // ---------------------------- - .register-form { - - &__agreement-section { - margin-top: -10px; - - p { - font-size: 13px; - margin-top: -10px; - margin-bottom: 24px; - line-height: 1.65; - } - } - - &__submit-button-wrap { - margin: 25px 0px 25px 0px; - - .bp3-button { - min-height: 45px; - background-color: #0052cc; - } - } - } - - // Send reset password - // ---------------------------- - .send-reset-password { - .form-group--crediential { - margin-bottom: 36px; - } - } - - // Invite form. - // ---------------- - .invite-form { - - &__statement-section { - margin-top: -10px; - - p { - font-size: 14px; - margin-bottom: 20px; - line-height: 1.65; - } - } - - .authentication-page__loading-overlay { - background: rgba(252, 253, 255, 0.9); - } - } -} +} \ No newline at end of file diff --git a/packages/webapp/src/style/pages/Setup/Organization.scss b/packages/webapp/src/style/pages/Setup/Organization.scss index 20e64efc1..02ceff63a 100644 --- a/packages/webapp/src/style/pages/Setup/Organization.scss +++ b/packages/webapp/src/style/pages/Setup/Organization.scss @@ -55,6 +55,11 @@ height: 40px; font-size: 15px; width: 100%; + + &:disabled, + &.bp3-loading{ + background-color: rgba(28, 36, 72, 0.5); + } } } } diff --git a/packages/webapp/src/utils/countries.tsx b/packages/webapp/src/utils/countries.tsx new file mode 100644 index 000000000..073542b82 --- /dev/null +++ b/packages/webapp/src/utils/countries.tsx @@ -0,0 +1,10 @@ +import { Countries } from '@/constants/countries'; + +export const getAllCountries = () => { + return Object.keys(Countries).map((countryCode) => { + return { + ...Countries[countryCode], + countryCode, + } + }); +};