From 261a0fb0b9301a63dda9f86674a9d288ac0b4a8a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 4 Mar 2023 10:13:04 +0100 Subject: [PATCH] Refactor AuthInterceptor (#1764) * Refactor AuthInterceptor * Refactor JwtStrategy --- .../api/src/app/account/account.controller.ts | 5 +- apps/api/src/app/admin/admin.service.ts | 36 +- apps/api/src/app/auth/jwt.strategy.ts | 27 +- apps/api/src/app/info/info.service.ts | 4 +- apps/api/src/app/order/order.controller.ts | 3 +- .../src/app/portfolio/portfolio.controller.ts | 15 +- apps/api/src/app/user/create-user.dto.ts | 7 - apps/api/src/app/user/user.controller.ts | 4 +- apps/api/src/app/user/user.service.ts | 8 +- .../redact-values-in-response.interceptor.ts | 4 +- ...form-data-source-in-request.interceptor.ts | 2 +- ...orm-data-source-in-response.interceptor.ts | 4 +- .../components/admin-users/admin-users.html | 13 +- apps/client/src/app/core/auth.interceptor.ts | 28 +- .../pages/register/register-page.component.ts | 2 +- apps/client/src/app/services/data.service.ts | 4 +- .../src/app/services/user/user.service.ts | 15 - libs/common/src/lib/config.ts | 4 + .../src/lib/timezone-cities-to-countries.ts | 426 ------------------ package.json | 1 + yarn.lock | 5 + 21 files changed, 109 insertions(+), 508 deletions(-) delete mode 100644 apps/api/src/app/user/create-user.dto.ts delete mode 100644 libs/common/src/lib/timezone-cities-to-countries.ts diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index d91e8c5c7..4f458bcfa 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,6 +1,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { Accounts } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { @@ -83,7 +84,7 @@ export class AccountController { @UseGuards(AuthGuard('jwt')) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAllAccounts( - @Headers('impersonation-id') impersonationId + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId( @@ -101,7 +102,7 @@ export class AccountController { @UseGuards(AuthGuard('jwt')) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountById( - @Headers('impersonation-id') impersonationId, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Param('id') id: string ): Promise { const impersonationUserId = diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index ad1620a23..f2fe18fc9 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -231,12 +231,27 @@ export class AdminService { } private async getUsersWithAnalytics(): Promise { - const usersWithAnalytics = await this.prismaService.user.findMany({ - orderBy: { + let orderBy: any = { + createdAt: 'desc' + }; + let where; + + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + orderBy = { Analytics: { updatedAt: 'desc' } - }, + }; + where = { + NOT: { + Analytics: null + } + }; + } + + const usersWithAnalytics = await this.prismaService.user.findMany({ + orderBy, + where, select: { _count: { select: { Account: true, Order: true } @@ -252,19 +267,16 @@ export class AdminService { id: true, Subscription: true }, - take: 30, - where: { - NOT: { - Analytics: null - } - } + take: 30 }); return usersWithAnalytics.map( ({ _count, Analytics, createdAt, id, Subscription }) => { const daysSinceRegistration = differenceInDays(new Date(), createdAt) + 1; - const engagement = Analytics.activityCount / daysSinceRegistration; + const engagement = Analytics + ? Analytics.activityCount / daysSinceRegistration + : undefined; const subscription = this.configurationService.get( 'ENABLE_FEATURE_SUBSCRIPTION' @@ -278,8 +290,8 @@ export class AdminService { id, subscription, accountCount: _count.Account || 0, - country: Analytics.country, - lastActivity: Analytics.updatedAt, + country: Analytics?.country, + lastActivity: Analytics?.updatedAt, transactionCount: _count.Order || 0 }; } diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts index ee50e3b72..af21ecc0e 100644 --- a/apps/api/src/app/auth/jwt.strategy.ts +++ b/apps/api/src/app/auth/jwt.strategy.ts @@ -1,33 +1,46 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; +import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; +import * as countriesAndTimezones from 'countries-and-timezones'; import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { public constructor( - readonly configurationService: ConfigurationService, + private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService, private readonly userService: UserService ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + passReqToCallback: true, secretOrKey: configurationService.get('JWT_SECRET_KEY') }); } - public async validate({ id }: { id: string }) { + public async validate(request: Request, { id }: { id: string }) { try { + const timezone = request.headers[HEADER_KEY_TIMEZONE.toLowerCase()]; const user = await this.userService.user({ id }); if (user) { - await this.prismaService.analytics.upsert({ - create: { User: { connect: { id: user.id } } }, - update: { activityCount: { increment: 1 }, updatedAt: new Date() }, - where: { userId: user.id } - }); + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + const country = + countriesAndTimezones.getCountryForTimezone(timezone)?.id; + + await this.prismaService.analytics.upsert({ + create: { country, User: { connect: { id: user.id } } }, + update: { + country, + activityCount: { increment: 1 }, + updatedAt: new Date() + }, + where: { userId: user.id } + }); + } return user; } else { diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index a13e3395f..191c841bd 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -59,9 +59,7 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true - ) { + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { info.fearAndGreedDataSource = encodeDataSource( ghostfolioFearAndGreedIndexDataSource ); diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 5a46f0866..5e5da4fde 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -3,6 +3,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { @@ -66,7 +67,7 @@ export class OrderController { @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getAllOrders( - @Headers('impersonation-id') impersonationId, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('tags') filterByTags?: string diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 795362195..dd6eb0ca6 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -10,6 +10,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { PortfolioDetails, PortfolioDividends, @@ -65,7 +66,7 @@ export class PortfolioController { @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getDetails( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @@ -189,7 +190,7 @@ export class PortfolioController { @Get('dividends') @UseGuards(AuthGuard('jwt')) public async getDividends( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('groupBy') groupBy?: GroupBy, @@ -239,7 +240,7 @@ export class PortfolioController { @Get('investments') @UseGuards(AuthGuard('jwt')) public async getInvestments( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('groupBy') groupBy?: GroupBy, @@ -291,7 +292,7 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) @Version('2') public async getPerformanceV2( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @@ -360,7 +361,7 @@ export class PortfolioController { @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPositions( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @@ -451,7 +452,7 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) @UseGuards(AuthGuard('jwt')) public async getPosition( - @Headers('impersonation-id') impersonationId: string, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('dataSource') dataSource, @Param('symbol') symbol ): Promise { @@ -474,7 +475,7 @@ export class PortfolioController { @Get('report') @UseGuards(AuthGuard('jwt')) public async getReport( - @Headers('impersonation-id') impersonationId: string + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string ): Promise { const report = await this.portfolioService.getReport(impersonationId); diff --git a/apps/api/src/app/user/create-user.dto.ts b/apps/api/src/app/user/create-user.dto.ts deleted file mode 100644 index 7751f75fa..000000000 --- a/apps/api/src/app/user/create-user.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsOptional, IsString } from 'class-validator'; - -export class CreateUserDto { - @IsString() - @IsOptional() - country?: string; -} diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 0eb37fb60..44d21e9c9 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -22,7 +22,6 @@ import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { size } from 'lodash'; -import { CreateUserDto } from './create-user.dto'; import { UserItem } from './interfaces/user-item.interface'; import { UpdateUserSettingDto } from './update-user-setting.dto'; import { UserService } from './user.service'; @@ -66,7 +65,7 @@ export class UserController { } @Post() - public async signupUser(@Body() data: CreateUserDto): Promise { + public async signupUser(): Promise { const isUserSignupEnabled = await this.propertyService.isUserSignupEnabled(); @@ -80,7 +79,6 @@ export class UserController { const hasAdmin = await this.userService.hasAdmin(); const { accessToken, id, role } = await this.userService.createUser({ - country: data.country, data: { role: hasAdmin ? 'USER' : 'ADMIN' } }); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index b45d1849d..1630a74aa 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -18,8 +18,6 @@ import { Injectable } from '@nestjs/common'; import { Prisma, Role, User } from '@prisma/client'; import { sortBy } from 'lodash'; -import { CreateUserDto } from './create-user.dto'; - const crypto = require('crypto'); @Injectable() @@ -234,9 +232,10 @@ export class UserService { } public async createUser({ - country, data - }: CreateUserDto & { data: Prisma.UserCreateInput }): Promise { + }: { + data: Prisma.UserCreateInput; + }): Promise { if (!data?.provider) { data.provider = 'ANONYMOUS'; } @@ -264,7 +263,6 @@ export class UserService { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { await this.prismaService.analytics.create({ data: { - country, User: { connect: { id: user.id } } } }); diff --git a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts index 0cc7b2168..6b10a4ebb 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -1,5 +1,6 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { CallHandler, ExecutionContext, @@ -22,7 +23,8 @@ export class RedactValuesInResponseInterceptor return next.handle().pipe( map((data: any) => { const request = context.switchToHttp().getRequest(); - const hasImpersonationId = !!request.headers?.['impersonation-id']; + const hasImpersonationId = + !!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()]; if ( hasImpersonationId || diff --git a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts index aa9952473..ecef35bb6 100644 --- a/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-request.interceptor.ts @@ -24,7 +24,7 @@ export class TransformDataSourceInRequestInterceptor const http = context.switchToHttp(); const request = http.getRequest(); - if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) { + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (request.body.dataSource) { request.body.dataSource = decodeDataSource(request.body.dataSource); } diff --git a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts index d02c26fca..0d8b8f264 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response.interceptor.ts @@ -26,9 +26,7 @@ export class TransformDataSourceInResponseInterceptor ): Observable { return next.handle().pipe( map((data: any) => { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true - ) { + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { data = redactAttributes({ options: [ { diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index 317086c90..15127802e 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -28,7 +28,13 @@ > Engagement per Day - Last Request + + Last Request + @@ -86,7 +92,10 @@ [value]="userItem.engagement" > - + {{ formatDistanceToNow(userItem.lastActivity) }} diff --git a/apps/client/src/app/core/auth.interceptor.ts b/apps/client/src/app/core/auth.interceptor.ts index 481f83b71..5cbbbf868 100644 --- a/apps/client/src/app/core/auth.interceptor.ts +++ b/apps/client/src/app/core/auth.interceptor.ts @@ -5,14 +5,16 @@ import { HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { + HEADER_KEY_IMPERSONATION, + HEADER_KEY_TIMEZONE, + HEADER_KEY_TOKEN +} from '@ghostfolio/common/config'; import { Observable } from 'rxjs'; import { ImpersonationStorageService } from '../services/impersonation-storage.service'; import { TokenStorageService } from '../services/token-storage.service'; -const IMPERSONATION_KEY = 'Impersonation-Id'; -const TOKEN_HEADER_KEY = 'Authorization'; - @Injectable() export class AuthInterceptor implements HttpInterceptor { public constructor( @@ -24,21 +26,27 @@ export class AuthInterceptor implements HttpInterceptor { req: HttpRequest, next: HttpHandler ): Observable> { - let authReq = req; + let request = req; + let headers = request.headers.set( + HEADER_KEY_TIMEZONE, + Intl?.DateTimeFormat().resolvedOptions().timeZone + ); + const token = this.tokenStorageService.getToken(); - const impersonationId = this.impersonationStorageService.getId(); if (token !== null) { - let headers = req.headers.set(TOKEN_HEADER_KEY, `Bearer ${token}`); + headers = headers.set(HEADER_KEY_TOKEN, `Bearer ${token}`); + + const impersonationId = this.impersonationStorageService.getId(); if (impersonationId !== null) { - headers = headers.set(IMPERSONATION_KEY, impersonationId); + headers = headers.set(HEADER_KEY_IMPERSONATION, impersonationId); } - - authReq = req.clone({ headers }); } - return next.handle(authReq); + request = request.clone({ headers }); + + return next.handle(request); } } diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts index 9752c6012..4ecff5a63 100644 --- a/apps/client/src/app/pages/register/register-page.component.ts +++ b/apps/client/src/app/pages/register/register-page.component.ts @@ -63,7 +63,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit { public async createAccount() { this.dataService - .postUser({ country: this.userService.getCountry() }) + .postUser() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ accessToken, authToken, role }) => { this.openShowAccessTokenDialog(accessToken, authToken, role); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index d34d54ded..d4071caeb 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -405,8 +405,8 @@ export class DataService { return this.http.post(`/api/v1/order`, aOrder); } - public postUser({ country }: { country: string }) { - return this.http.post(`/api/v1/user`, { country }); + public postUser() { + return this.http.post(`/api/v1/user`, {}); } public putAccount(aAccount: UpdateAccountDto) { diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index fbbab5a1a..7f903df3a 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -6,7 +6,6 @@ import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/compone import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { timezoneCitiesToCountries } from '@ghostfolio/common/timezone-cities-to-countries'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, of } from 'rxjs'; import { throwError } from 'rxjs'; @@ -46,20 +45,6 @@ export class UserService extends ObservableStore { } } - public getCountry() { - let country: string; - - if (Intl) { - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const timeZoneArray = timeZone.split('/'); - const city = timeZoneArray[timeZoneArray.length - 1]; - - country = timezoneCitiesToCountries[city]; - } - - return country; - } - public remove() { this.setState({ user: null }, UserStoreActions.RemoveUser); } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 577644116..c7151b89e 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -69,6 +69,10 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = { } }; +export const HEADER_KEY_IMPERSONATION = 'Impersonation-Id'; +export const HEADER_KEY_TIMEZONE = 'Timezone'; +export const HEADER_KEY_TOKEN = 'Authorization'; + export const MAX_CHART_ITEMS = 365; export const PROPERTY_BENCHMARKS = 'BENCHMARKS'; diff --git a/libs/common/src/lib/timezone-cities-to-countries.ts b/libs/common/src/lib/timezone-cities-to-countries.ts deleted file mode 100644 index 6f6973179..000000000 --- a/libs/common/src/lib/timezone-cities-to-countries.ts +++ /dev/null @@ -1,426 +0,0 @@ -export const timezoneCitiesToCountries = { - Abidjan: 'CI', - Accra: 'GH', - Adak: 'US', - Addis_Ababa: 'ET', - Adelaide: 'AU', - Aden: 'YE', - Algiers: 'DZ', - Almaty: 'KZ', - Amman: 'JO', - Amsterdam: 'NL', - Anadyr: 'RU', - Anchorage: 'US', - Andorra: 'AD', - Anguilla: 'AI', - Antananarivo: 'MG', - Antigua: 'AG', - Apia: 'WS', - Aqtau: 'KZ', - Aqtobe: 'KZ', - Araguaina: 'BR', - Aruba: 'AW', - Ashgabat: 'TM', - Asmara: 'ER', - Astrakhan: 'RU', - Asuncion: 'PY', - Athens: 'GR', - Atikokan: 'CA', - Atyrau: 'KZ', - Auckland: 'NZ', - Azores: 'PT', - Baghdad: 'IQ', - Bahia: 'BR', - Bahia_Banderas: 'MX', - Bahrain: 'BH', - Baku: 'AZ', - Bamako: 'ML', - Bangkok: 'TH', - Bangui: 'CF', - Banjul: 'GM', - Barbados: 'BB', - Barnaul: 'RU', - Beirut: 'LB', - Belem: 'BR', - Belgrade: 'RS', - Belize: 'BZ', - Berlin: 'DE', - Bermuda: 'BM', - Beulah: 'US', - Bishkek: 'KG', - Bissau: 'GW', - 'Blanc-Sablon': 'CA', - Blantyre: 'MW', - Boa_Vista: 'BR', - Bogota: 'CO', - Boise: 'US', - Bougainville: 'PG', - Bratislava: 'SK', - Brazzaville: 'CG', - Brisbane: 'AU', - Broken_Hill: 'AU', - Brunei: 'BN', - Brussels: 'BE', - Bucharest: 'RO', - Budapest: 'HU', - Buenos_Aires: 'AR', - Bujumbura: 'BI', - Busingen: 'DE', - Cairo: 'EG', - Cambridge_Bay: 'CA', - Campo_Grande: 'BR', - Canary: 'ES', - Cancun: 'MX', - Cape_Verde: 'CV', - Caracas: 'VE', - Casablanca: 'MA', - Casey: 'AQ', - Catamarca: 'AR', - Cayenne: 'GF', - Cayman: 'KY', - Center: 'US', - Ceuta: 'ES', - Chagos: 'IO', - Chatham: 'NZ', - Chicago: 'US', - Chihuahua: 'MX', - Chisinau: 'MD', - Chita: 'RU', - Choibalsan: 'MN', - Christmas: 'CX', - Chuuk: 'FM', - Cocos: 'CC', - Colombo: 'LK', - Comoro: 'KM', - Conakry: 'GN', - Copenhagen: 'DK', - Cordoba: 'AR', - Costa_Rica: 'CR', - Creston: 'CA', - Cuiaba: 'BR', - Curacao: 'CW', - Dakar: 'SN', - Damascus: 'SY', - Danmarkshavn: 'GL', - Dar_es_Salaam: 'TZ', - Darwin: 'AU', - Davis: 'AQ', - Dawson: 'CA', - Dawson_Creek: 'CA', - Denver: 'US', - Detroit: 'US', - Dhaka: 'BD', - Dili: 'TL', - Djibouti: 'DJ', - Dominica: 'DM', - Douala: 'CM', - Dubai: 'AE', - Dublin: 'IE', - DumontDUrville: 'AQ', - Dushanbe: 'TJ', - Easter: 'CL', - Edmonton: 'CA', - Efate: 'VU', - Eirunepe: 'BR', - El_Aaiun: 'EH', - El_Salvador: 'SV', - Eucla: 'AU', - Fakaofo: 'TK', - Famagusta: 'CY', - Faroe: 'FO', - Fiji: 'FJ', - Fort_Nelson: 'CA', - Fortaleza: 'BR', - Freetown: 'SL', - Funafuti: 'TV', - Gaborone: 'BW', - Galapagos: 'EC', - Gambier: 'PF', - Gaza: 'PS', - Gibraltar: 'GI', - Glace_Bay: 'CA', - Goose_Bay: 'CA', - Grand_Turk: 'TC', - Grenada: 'GD', - Guadalcanal: 'SB', - Guadeloupe: 'GP', - Guam: 'GU', - Guatemala: 'GT', - Guayaquil: 'EC', - Guernsey: 'GG', - Guyana: 'GY', - Halifax: 'CA', - Harare: 'ZW', - Havana: 'CU', - Hebron: 'PS', - Helsinki: 'FI', - Hermosillo: 'MX', - Ho_Chi_Minh: 'VN', - Hobart: 'AU', - Hong_Kong: 'HK', - Honolulu: 'US', - Hovd: 'MN', - Indianapolis: 'US', - Inuvik: 'CA', - Iqaluit: 'CA', - Irkutsk: 'RU', - Isle_of_Man: 'IM', - Istanbul: 'TR', - Jakarta: 'ID', - Jamaica: 'JM', - Jayapura: 'ID', - Jersey: 'JE', - Jerusalem: 'IL', - Johannesburg: 'ZA', - Juba: 'SS', - Jujuy: 'AR', - Juneau: 'US', - Kabul: 'AF', - Kaliningrad: 'RU', - Kamchatka: 'RU', - Kampala: 'UG', - Kanton: 'KI', - Karachi: 'PK', - Kathmandu: 'NP', - Kerguelen: 'TF', - Khandyga: 'RU', - Khartoum: 'SD', - Kiev: 'UA', - Kigali: 'RW', - Kinshasa: 'CD', - Kiritimati: 'KI', - Kirov: 'RU', - Knox: 'US', - Kolkata: 'IN', - Kosrae: 'FM', - Kralendijk: 'NL', - Krasnoyarsk: 'RU', - Kuala_Lumpur: 'MY', - Kuching: 'MY', - Kuwait: 'KW', - Kwajalein: 'MH', - La_Paz: 'BO', - La_Rioja: 'AR', - Lagos: 'NG', - Libreville: 'GA', - Lima: 'PE', - Lindeman: 'AU', - Lisbon: 'PT', - Ljubljana: 'SI', - Lome: 'TG', - London: 'GB', - Longyearbyen: 'SJ', - Lord_Howe: 'AU', - Los_Angeles: 'US', - Louisville: 'US', - Lower_Princes: 'SX', - Luanda: 'AO', - Lubumbashi: 'CD', - Lusaka: 'ZM', - Luxembourg: 'LU', - Macau: 'MO', - Maceio: 'BR', - Macquarie: 'AU', - Madeira: 'PT', - Madrid: 'ES', - Magadan: 'RU', - Mahe: 'SC', - Majuro: 'MH', - Makassar: 'ID', - Malabo: 'GQ', - Maldives: 'MV', - Malta: 'MT', - Managua: 'NI', - Manaus: 'BR', - Manila: 'PH', - Maputo: 'MZ', - Marengo: 'US', - Mariehamn: 'AX', - Marigot: 'MF', - Marquesas: 'PF', - Martinique: 'MQ', - Maseru: 'LS', - Matamoros: 'MX', - Mauritius: 'MU', - Mawson: 'AQ', - Mayotte: 'YT', - Mazatlan: 'MX', - Mbabane: 'SZ', - McMurdo: 'AQ', - Melbourne: 'AU', - Mendoza: 'AR', - Menominee: 'US', - Merida: 'MX', - Metlakatla: 'US', - Mexico_City: 'MX', - Midway: 'UM', - Minsk: 'BY', - Miquelon: 'PM', - Mogadishu: 'SO', - Monaco: 'MC', - Moncton: 'CA', - Monrovia: 'LR', - Monterrey: 'MX', - Montevideo: 'UY', - Monticello: 'US', - Montserrat: 'MS', - Moscow: 'RU', - Muscat: 'OM', - Nairobi: 'KE', - Nassau: 'BS', - Nauru: 'NR', - Ndjamena: 'TD', - New_Salem: 'US', - New_York: 'US', - Niamey: 'NE', - Nicosia: 'CY', - Nipigon: 'CA', - Niue: 'NU', - Nome: 'US', - Norfolk: 'NF', - Noronha: 'BR', - Nouakchott: 'MR', - Noumea: 'NC', - Novokuznetsk: 'RU', - Novosibirsk: 'RU', - Nuuk: 'GL', - Ojinaga: 'MX', - Omsk: 'RU', - Oral: 'KZ', - Oslo: 'NO', - Ouagadougou: 'BF', - Pago_Pago: 'AS', - Palau: 'PW', - Palmer: 'AQ', - Panama: 'PA', - Pangnirtung: 'CA', - Paramaribo: 'SR', - Paris: 'FR', - Perth: 'AU', - Petersburg: 'US', - Phnom_Penh: 'KH', - Phoenix: 'US', - Pitcairn: 'PN', - Podgorica: 'ME', - Pohnpei: 'FM', - Pontianak: 'ID', - 'Port-au-Prince': 'HT', - Port_Moresby: 'PG', - Port_of_Spain: 'TT', - 'Porto-Novo': 'BJ', - Porto_Velho: 'BR', - Prague: 'CZ', - Puerto_Rico: 'PR', - Punta_Arenas: 'CL', - Pyongyang: 'KP', - Qatar: 'QA', - Qostanay: 'KZ', - Qyzylorda: 'KZ', - Rainy_River: 'CA', - Rankin_Inlet: 'CA', - Rarotonga: 'CK', - Recife: 'BR', - Regina: 'CA', - Resolute: 'CA', - Reunion: 'RE', - Reykjavik: 'IS', - Riga: 'LV', - Rio_Branco: 'BR', - Rio_Gallegos: 'AR', - Riyadh: 'SA', - Rome: 'IT', - Rothera: 'AQ', - Saipan: 'MP', - Sakhalin: 'RU', - Salta: 'AR', - Samara: 'RU', - Samarkand: 'UZ', - San_Juan: 'AR', - San_Luis: 'AR', - San_Marino: 'SM', - Santarem: 'BR', - Santiago: 'CL', - Santo_Domingo: 'DO', - Sao_Paulo: 'BR', - Sao_Tome: 'ST', - Sarajevo: 'BA', - Saratov: 'RU', - Scoresbysund: 'GL', - Seoul: 'KR', - Shanghai: 'CN', - Simferopol: 'RU', - Singapore: 'SG', - Sitka: 'US', - Skopje: 'MK', - Sofia: 'BG', - South_Georgia: 'GS', - Srednekolymsk: 'RU', - St_Barthelemy: 'BL', - St_Helena: 'SH', - St_Johns: 'CA', - St_Kitts: 'KN', - St_Lucia: 'LC', - St_Thomas: 'VI', - St_Vincent: 'VC', - Stanley: 'FK', - Stockholm: 'SE', - Swift_Current: 'CA', - Sydney: 'AU', - Syowa: 'AQ', - Tahiti: 'PF', - Taipei: 'TW', - Tallinn: 'EE', - Tarawa: 'KI', - Tashkent: 'UZ', - Tbilisi: 'GE', - Tegucigalpa: 'HN', - Tehran: 'IR', - Tell_City: 'US', - Thimphu: 'BT', - Thule: 'GL', - Thunder_Bay: 'CA', - Tijuana: 'MX', - Tirane: 'AL', - Tokyo: 'JP', - Tomsk: 'RU', - Tongatapu: 'TO', - Toronto: 'CA', - Tortola: 'VI (UK)', - Tripoli: 'LY', - Troll: 'AQ', - Tucuman: 'AR', - Tunis: 'TN', - Ulaanbaatar: 'MN', - Ulyanovsk: 'RU', - Urumqi: 'CN', - Ushuaia: 'AR', - 'Ust-Nera': 'RU', - Uzhgorod: 'UA', - Vaduz: 'LI', - Vancouver: 'CA', - Vatican: 'VA', - Vevay: 'US', - Vienna: 'AT', - Vientiane: 'LA', - Vilnius: 'LT', - Vincennes: 'US', - Vladivostok: 'RU', - Volgograd: 'RU', - Vostok: 'AQ', - Wake: 'UM', - Wallis: 'WF', - Warsaw: 'PL', - Whitehorse: 'CA', - Winamac: 'US', - Windhoek: 'NA', - Winnipeg: 'CA', - Yakutat: 'US', - Yakutsk: 'RU', - Yangon: 'MM', - Yekaterinburg: 'RU', - Yellowknife: 'CA', - Yerevan: 'AM', - Zagreb: 'HR', - Zaporozhye: 'UA', - Zurich: 'CH' -}; diff --git a/package.json b/package.json index f2e253cf6..3b3ef2dae 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "class-transformer": "0.3.2", "class-validator": "0.13.1", "color": "4.2.3", + "countries-and-timezones": "3.4.1", "countries-list": "2.6.1", "countup.js": "2.3.2", "date-fns": "2.29.3", diff --git a/yarn.lock b/yarn.lock index cf669ad27..72842aafd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9352,6 +9352,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +countries-and-timezones@3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/countries-and-timezones/-/countries-and-timezones-3.4.1.tgz#0ec2540f57e42f0f740eb2acaede786043347fe1" + integrity sha512-INeHGCony4XUUR8iGL/lmt9s1Oi+n+gFHeJAMfbV5hJfYeDOB8JG1oxz5xFQu5oBZoRCJe/87k1Vzue9DoIauA== + countries-list@2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/countries-list/-/countries-list-2.6.1.tgz"