Refactor AuthInterceptor (#1764)
* Refactor AuthInterceptor * Refactor JwtStrategy
This commit is contained in:
parent
cfc05cce41
commit
261a0fb0b9
@ -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<Accounts> {
|
||||
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<AccountWithValue> {
|
||||
const impersonationUserId =
|
||||
|
@ -231,12 +231,27 @@ export class AdminService {
|
||||
}
|
||||
|
||||
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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<PortfolioPositionDetail> {
|
||||
@ -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<PortfolioReport> {
|
||||
const report = await this.portfolioService.getReport(impersonationId);
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
country?: string;
|
||||
}
|
@ -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<UserItem> {
|
||||
public async signupUser(): Promise<UserItem> {
|
||||
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' }
|
||||
});
|
||||
|
||||
|
@ -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<User> {
|
||||
}: {
|
||||
data: Prisma.UserCreateInput;
|
||||
}): Promise<User> {
|
||||
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 } }
|
||||
}
|
||||
});
|
||||
|
@ -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<T>
|
||||
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 ||
|
||||
|
@ -24,7 +24,7 @@ export class TransformDataSourceInRequestInterceptor<T>
|
||||
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);
|
||||
}
|
||||
|
@ -26,9 +26,7 @@ export class TransformDataSourceInResponseInterceptor<T>
|
||||
): Observable<any> {
|
||||
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: [
|
||||
{
|
||||
|
@ -28,7 +28,13 @@
|
||||
>
|
||||
<ng-container i18n>Engagement per Day</ng-container>
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Last Request</th>
|
||||
<th
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-header-cell px-1 py-2"
|
||||
i18n
|
||||
>
|
||||
Last Request
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -86,7 +92,10 @@
|
||||
[value]="userItem.engagement"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="mat-cell px-1 py-2"
|
||||
>
|
||||
{{ formatDistanceToNow(userItem.lastActivity) }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
|
@ -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<any>,
|
||||
next: HttpHandler
|
||||
): Observable<HttpEvent<any>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -405,8 +405,8 @@ export class DataService {
|
||||
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
||||
}
|
||||
|
||||
public postUser({ country }: { country: string }) {
|
||||
return this.http.post<UserItem>(`/api/v1/user`, { country });
|
||||
public postUser() {
|
||||
return this.http.post<UserItem>(`/api/v1/user`, {});
|
||||
}
|
||||
|
||||
public putAccount(aAccount: UpdateAccountDto) {
|
||||
|
@ -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<UserStoreState> {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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'
|
||||
};
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user