Feature/setup api versioning (#783)

* Setup API versioning

* Update changelog
pull/786/head
Thomas Kaul 3 years ago committed by GitHub
parent eb77652d6a
commit 6762572658
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added API versioning
- Added more durations in the coupon system - Added more durations in the coupon system
### Changed ### Changed

@ -1,4 +1,4 @@
import { Logger, ValidationPipe } from '@nestjs/common'; import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
@ -7,8 +7,11 @@ import { environment } from './environments/environment';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.enableCors(); app.enableCors();
const globalPrefix = 'api'; app.enableVersioning({
app.setGlobalPrefix(globalPrefix); defaultVersion: '1',
type: VersioningType.URI
});
app.setGlobalPrefix('api');
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
forbidNonWhitelisted: true, forbidNonWhitelisted: true,

@ -8,7 +8,7 @@
<div> <div>
<ng-container *ngIf="data.hasPermissionToUseSocialLogin"> <ng-container *ngIf="data.hasPermissionToUseSocialLogin">
<div class="text-center"> <div class="text-center">
<a color="accent" href="/api/auth/google" mat-flat-button <a color="accent" href="/api/v1/auth/google" mat-flat-button
><ion-icon class="mr-1" name="logo-google"></ion-icon ><ion-icon class="mr-1" name="logo-google"></ion-icon
><span i18n>Sign in with Google</span></a ><span i18n>Sign in with Google</span></a
> >

@ -35,7 +35,7 @@
> >
or or
</div> </div>
<a color="accent" href="/api/auth/google" mat-flat-button <a color="accent" href="/api/v1/auth/google" mat-flat-button
><ion-icon class="mr-1" name="logo-google"></ion-icon ><ion-icon class="mr-1" name="logo-google"></ion-icon
><span i18n>Continue with Google</span></a ><span i18n>Continue with Google</span></a
> >

@ -19,7 +19,7 @@ export class AdminService {
public deleteProfileData({ dataSource, symbol }: UniqueAsset) { public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/admin/profile-data/${dataSource}/${symbol}` `/api/v1/admin/profile-data/${dataSource}/${symbol}`
); );
} }
@ -31,7 +31,7 @@ export class AdminService {
symbol: string; symbol: string;
}): Observable<AdminMarketDataDetails> { }): Observable<AdminMarketDataDetails> {
return this.http return this.http
.get<any>(`/api/admin/market-data/${dataSource}/${symbol}`) .get<any>(`/api/v1/admin/market-data/${dataSource}/${symbol}`)
.pipe( .pipe(
map((data) => { map((data) => {
for (const item of data.marketData) { for (const item of data.marketData) {
@ -43,16 +43,16 @@ export class AdminService {
} }
public gatherMax() { public gatherMax() {
return this.http.post<void>(`/api/admin/gather/max`, {}); return this.http.post<void>(`/api/v1/admin/gather/max`, {});
} }
public gatherProfileData() { public gatherProfileData() {
return this.http.post<void>(`/api/admin/gather/profile-data`, {}); return this.http.post<void>(`/api/v1/admin/gather/profile-data`, {});
} }
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
return this.http.post<void>( return this.http.post<void>(
`/api/admin/gather/profile-data/${dataSource}/${symbol}`, `/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`,
{} {}
); );
} }
@ -64,7 +64,7 @@ export class AdminService {
}: UniqueAsset & { }: UniqueAsset & {
date?: Date; date?: Date;
}) { }) {
let url = `/api/admin/gather/${dataSource}/${symbol}`; let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
if (date) { if (date) {
url = `${url}/${format(date, DATE_FORMAT)}`; url = `${url}/${format(date, DATE_FORMAT)}`;
@ -82,7 +82,7 @@ export class AdminService {
date: Date; date: Date;
symbol: string; symbol: string;
}) { }) {
const url = `/api/symbol/${dataSource}/${symbol}/${format( const url = `/api/v1/symbol/${dataSource}/${symbol}/${format(
date, date,
DATE_FORMAT DATE_FORMAT
)}`; )}`;
@ -101,7 +101,7 @@ export class AdminService {
marketData: UpdateMarketDataDto; marketData: UpdateMarketDataDto;
symbol: string; symbol: string;
}) { }) {
const url = `/api/admin/market-data/${dataSource}/${symbol}/${format( const url = `/api/v1/admin/market-data/${dataSource}/${symbol}/${format(
date, date,
DATE_FORMAT DATE_FORMAT
)}`; )}`;

@ -8,6 +8,6 @@ export class CacheService {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public flush() { public flush() {
return this.http.post<any>(`/api/cache/flush`, {}); return this.http.post<any>(`/api/v1/cache/flush`, {});
} }
} }

@ -23,12 +23,10 @@ import {
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformance,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -52,46 +50,46 @@ export class DataService {
couponId?: string; couponId?: string;
priceId: string; priceId: string;
}) { }) {
return this.http.post('/api/subscription/stripe/checkout-session', { return this.http.post('/api/v1/subscription/stripe/checkout-session', {
couponId, couponId,
priceId priceId
}); });
} }
public fetchAccounts() { public fetchAccounts() {
return this.http.get<Accounts>('/api/account'); return this.http.get<Accounts>('/api/v1/account');
} }
public fetchAdminData() { public fetchAdminData() {
return this.http.get<AdminData>('/api/admin'); return this.http.get<AdminData>('/api/v1/admin');
} }
public fetchAdminMarketData() { public fetchAdminMarketData() {
return this.http.get<AdminMarketData>('/api/admin/market-data'); return this.http.get<AdminMarketData>('/api/v1/admin/market-data');
} }
public deleteAccess(aId: string) { public deleteAccess(aId: string) {
return this.http.delete<any>(`/api/access/${aId}`); return this.http.delete<any>(`/api/v1/access/${aId}`);
} }
public deleteAccount(aId: string) { public deleteAccount(aId: string) {
return this.http.delete<any>(`/api/account/${aId}`); return this.http.delete<any>(`/api/v1/account/${aId}`);
} }
public deleteOrder(aId: string) { public deleteOrder(aId: string) {
return this.http.delete<any>(`/api/order/${aId}`); return this.http.delete<any>(`/api/v1/order/${aId}`);
} }
public deleteUser(aId: string) { public deleteUser(aId: string) {
return this.http.delete<any>(`/api/user/${aId}`); return this.http.delete<any>(`/api/v1/user/${aId}`);
} }
public fetchAccesses() { public fetchAccesses() {
return this.http.get<Access[]>('/api/access'); return this.http.get<Access[]>('/api/v1/access');
} }
public fetchChart({ range }: { range: DateRange }) { public fetchChart({ range }: { range: DateRange }) {
return this.http.get<PortfolioChart>('/api/portfolio/chart', { return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
params: { range } params: { range }
}); });
} }
@ -103,7 +101,7 @@ export class DataService {
params = params.append('activityIds', activityIds.join(',')); params = params.append('activityIds', activityIds.join(','));
} }
return this.http.get<Export>('/api/export', { return this.http.get<Export>('/api/v1/export', {
params params
}); });
} }
@ -121,7 +119,7 @@ export class DataService {
} }
public fetchInvestments(): Observable<PortfolioInvestments> { public fetchInvestments(): Observable<PortfolioInvestments> {
return this.http.get<any>('/api/portfolio/investments').pipe( return this.http.get<any>('/api/v1/portfolio/investments').pipe(
map((response) => { map((response) => {
if (response.firstOrderDate) { if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate); response.firstOrderDate = parseISO(response.firstOrderDate);
@ -147,7 +145,7 @@ export class DataService {
params = params.append('includeHistoricalData', includeHistoricalData); params = params.append('includeHistoricalData', includeHistoricalData);
} }
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`, { return this.http.get<SymbolItem>(`/api/v1/symbol/${dataSource}/${symbol}`, {
params params
}); });
} }
@ -157,14 +155,14 @@ export class DataService {
}: { }: {
range: DateRange; range: DateRange;
}): Observable<PortfolioPositions> { }): Observable<PortfolioPositions> {
return this.http.get<PortfolioPositions>('/api/portfolio/positions', { return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
params: { range } params: { range }
}); });
} }
public fetchSymbols(aQuery: string) { public fetchSymbols(aQuery: string) {
return this.http return this.http
.get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`) .get<{ items: LookupItem[] }>(`/api/v1/symbol/lookup?query=${aQuery}`)
.pipe( .pipe(
map((respose) => { map((respose) => {
return respose.items; return respose.items;
@ -173,7 +171,7 @@ export class DataService {
} }
public fetchOrders(): Observable<Activities> { public fetchOrders(): Observable<Activities> {
return this.http.get<any>('/api/order').pipe( return this.http.get<any>('/api/v1/order').pipe(
map(({ activities }) => { map(({ activities }) => {
for (const activity of activities) { for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt); activity.createdAt = parseISO(activity.createdAt);
@ -185,14 +183,14 @@ export class DataService {
} }
public fetchPortfolioDetails(aParams: { [param: string]: any }) { public fetchPortfolioDetails(aParams: { [param: string]: any }) {
return this.http.get<PortfolioDetails>('/api/portfolio/details', { return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params: aParams params: aParams
}); });
} }
public fetchPortfolioPerformance(params: { [param: string]: any }) { public fetchPortfolioPerformance(params: { [param: string]: any }) {
return this.http.get<PortfolioPerformanceResponse>( return this.http.get<PortfolioPerformanceResponse>(
'/api/portfolio/performance', '/api/v1/portfolio/performance',
{ {
params params
} }
@ -201,16 +199,16 @@ export class DataService {
public fetchPortfolioPublic(aId: string) { public fetchPortfolioPublic(aId: string) {
return this.http.get<PortfolioPublicDetails>( return this.http.get<PortfolioPublicDetails>(
`/api/portfolio/public/${aId}` `/api/v1/portfolio/public/${aId}`
); );
} }
public fetchPortfolioReport() { public fetchPortfolioReport() {
return this.http.get<PortfolioReport>('/api/portfolio/report'); return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
} }
public fetchPortfolioSummary(): Observable<PortfolioSummary> { public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/portfolio/summary').pipe( return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => { map((summary) => {
if (summary.firstOrderDate) { if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate); summary.firstOrderDate = parseISO(summary.firstOrderDate);
@ -229,7 +227,7 @@ export class DataService {
symbol: string; symbol: string;
}) { }) {
return this.http return this.http
.get<any>(`/api/portfolio/position/${dataSource}/${symbol}`) .get<any>(`/api/v1/portfolio/position/${dataSource}/${symbol}`)
.pipe( .pipe(
map((data) => { map((data) => {
if (data.orders) { if (data.orders) {
@ -245,47 +243,47 @@ export class DataService {
} }
public loginAnonymous(accessToken: string) { public loginAnonymous(accessToken: string) {
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`); return this.http.get<any>(`/api/v1/auth/anonymous/${accessToken}`);
} }
public postAccess(aAccess: CreateAccessDto) { public postAccess(aAccess: CreateAccessDto) {
return this.http.post<OrderModel>(`/api/access`, aAccess); return this.http.post<OrderModel>(`/api/v1/access`, aAccess);
} }
public postAccount(aAccount: CreateAccountDto) { public postAccount(aAccount: CreateAccountDto) {
return this.http.post<OrderModel>(`/api/account`, aAccount); return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
} }
public postOrder(aOrder: CreateOrderDto) { public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>(`/api/order`, aOrder); return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
} }
public postUser() { public postUser() {
return this.http.post<UserItem>(`/api/user`, {}); return this.http.post<UserItem>(`/api/v1/user`, {});
} }
public putAccount(aAccount: UpdateAccountDto) { public putAccount(aAccount: UpdateAccountDto) {
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount); return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
} }
public putAdminSetting(key: string, aData: PropertyDto) { public putAdminSetting(key: string, aData: PropertyDto) {
return this.http.put<void>(`/api/admin/settings/${key}`, aData); return this.http.put<void>(`/api/v1/admin/settings/${key}`, aData);
} }
public putOrder(aOrder: UpdateOrderDto) { public putOrder(aOrder: UpdateOrderDto) {
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder); return this.http.put<UserItem>(`/api/v1/order/${aOrder.id}`, aOrder);
} }
public putUserSetting(aData: UpdateUserSettingDto) { public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put<User>(`/api/user/setting`, aData); return this.http.put<User>(`/api/v1/user/setting`, aData);
} }
public putUserSettings(aData: UpdateUserSettingsDto) { public putUserSettings(aData: UpdateUserSettingsDto) {
return this.http.put<User>(`/api/user/settings`, aData); return this.http.put<User>(`/api/v1/user/settings`, aData);
} }
public redeemCoupon(couponCode: string) { public redeemCoupon(couponCode: string) {
return this.http.post('/api/subscription/redeem-coupon', { return this.http.post('/api/v1/subscription/redeem-coupon', {
couponCode couponCode
}); });
} }

@ -282,6 +282,6 @@ export class ImportTransactionsService {
} }
private postImport(aImportData: { orders: CreateOrderDto[] }) { private postImport(aImportData: { orders: CreateOrderDto[] }) {
return this.http.post<void>('/api/import', aImportData); return this.http.post<void>('/api/v1/import', aImportData);
} }
} }

@ -36,7 +36,7 @@ export class UserService extends ObservableStore<UserStoreState> {
} }
private fetchUser() { private fetchUser() {
return this.http.get<User>('/api/user').pipe( return this.http.get<User>('/api/v1/user').pipe(
map((user) => { map((user) => {
this.setState({ user }, UserStoreActions.GetUser); this.setState({ user }, UserStoreActions.GetUser);
return user; return user;

@ -35,7 +35,7 @@ export class WebAuthnService {
public register() { public register() {
return this.http return this.http
.get<PublicKeyCredentialCreationOptionsJSON>( .get<PublicKeyCredentialCreationOptionsJSON>(
`/api/auth/webauthn/generate-registration-options`, `/api/v1/auth/webauthn/generate-registration-options`,
{} {}
) )
.pipe( .pipe(
@ -48,7 +48,7 @@ export class WebAuthnService {
}), }),
switchMap((attResp) => { switchMap((attResp) => {
return this.http.post<AuthDeviceDto>( return this.http.post<AuthDeviceDto>(
`/api/auth/webauthn/verify-attestation`, `/api/v1/auth/webauthn/verify-attestation`,
{ {
credential: attResp credential: attResp
} }
@ -65,31 +65,33 @@ export class WebAuthnService {
public deregister() { public deregister() {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http.delete<AuthDeviceDto>(`/api/auth-device/${deviceId}`).pipe( return this.http
catchError((error) => { .delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`)
console.warn(`Could not deregister device ${deviceId}`, error); .pipe(
return of(null); catchError((error) => {
}), console.warn(`Could not deregister device ${deviceId}`, error);
tap(() => return of(null);
this.settingsStorageService.removeSetting( }),
WebAuthnService.WEB_AUTH_N_DEVICE_ID tap(() =>
this.settingsStorageService.removeSetting(
WebAuthnService.WEB_AUTH_N_DEVICE_ID
)
) )
) );
);
} }
public login() { public login() {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http return this.http
.post<PublicKeyCredentialRequestOptionsJSON>( .post<PublicKeyCredentialRequestOptionsJSON>(
`/api/auth/webauthn/generate-assertion-options`, `/api/v1/auth/webauthn/generate-assertion-options`,
{ deviceId } { deviceId }
) )
.pipe( .pipe(
switchMap(startAuthentication), switchMap(startAuthentication),
switchMap((assertionResponse) => { switchMap((assertionResponse) => {
return this.http.post<{ authToken: string }>( return this.http.post<{ authToken: string }>(
`/api/auth/webauthn/verify-assertion`, `/api/v1/auth/webauthn/verify-assertion`,
{ {
credential: assertionResponse, credential: assertionResponse,
deviceId deviceId

@ -8,7 +8,7 @@ import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
(async () => { (async () => {
const response = await fetch('/api/info'); const response = await fetch('/api/v1/info');
const info: InfoItem = await response.json(); const info: InfoItem = await response.json();
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') { if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {

Loading…
Cancel
Save