diff --git a/src/AccountTracking.Web/nswag.json b/src/AccountTracking.Web/nswag.json new file mode 100644 index 0000000..0828d24 --- /dev/null +++ b/src/AccountTracking.Web/nswag.json @@ -0,0 +1,75 @@ +{ + "runtime": "Default", + "defaultVariables": null, + "documentGenerator": { + "fromDocument": { + "url": "openapi.json", + "output": null, + "newLineBehavior": "Auto" + } + }, + "codeGenerators": { + "openApiToTypeScriptClient": { + "className": "{controller}Client", + "moduleName": "", + "namespace": "", + "typeScriptVersion": 5.0, + "template": "Fetch", + "promiseType": "Promise", + "httpClass": "HttpClient", + "withCredentials": false, + "useSingletonProvider": false, + "injectionTokenType": "OpaqueToken", + "rxJsVersion": 6.0, + "dateTimeType": "String", + "nullValue": "Undefined", + "generateClientClasses": true, + "generateClientInterfaces": false, + "generateOptionalParameters": false, + "exportTypes": true, + "wrapDtoExceptions": false, + "exceptionClass": "SwaggerException", + "clientBaseClass": null, + "wrapResponses": false, + "wrapResponseMethods": [], + "generateResponseClasses": true, + "responseClass": "SwaggerResponse", + "protectedMethods": [], + "configurationClass": null, + "useTransformOptionsMethod": false, + "useTransformResultMethod": false, + "generateDtoTypes": true, + "operationGenerationMode": "MultipleClientsFromOperationId", + "includedOperationIds": [], + "excludedOperationIds": [], + "markOptionalProperties": true, + "generateCloneMethod": false, + "typeStyle": "Interface", + "enumStyle": "Enum", + "useLeafType": false, + "classTypes": [], + "extendedClasses": [], + "extensionCode": null, + "generateDefaultValues": true, + "excludedTypeNames": [], + "excludedParameterNames": [], + "handleReferences": false, + "generateTypeCheckFunctions": false, + "generateConstructorInterface": true, + "convertConstructorInterfaceData": false, + "importRequiredTypes": true, + "useGetBaseUrlMethod": false, + "baseUrlTokenName": "API_BASE_URL", + "queryNullValue": "", + "useAbortSignal": false, + "inlineNamedDictionaries": false, + "inlineNamedAny": false, + "includeHttpContext": false, + "templateDirectory": null, + "serviceHost": null, + "serviceSchemes": null, + "output": "src/api/apiClient.ts", + "newLineBehavior": "Auto" + } + } +} \ No newline at end of file diff --git a/src/AccountTracking.Web/src/api/apiClient.ts b/src/AccountTracking.Web/src/api/apiClient.ts new file mode 100644 index 0000000..564f826 --- /dev/null +++ b/src/AccountTracking.Web/src/api/apiClient.ts @@ -0,0 +1,470 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.6.3.0 (NJsonSchema v11.5.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +/* eslint-disable */ +// ReSharper disable InconsistentNaming + +export class AuthClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5099"; + } + + login(request: LoginRequest): Promise { + let url_ = this.baseUrl + "/api/auth/login"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(request); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processLogin(_response); + }); + } + + protected processLogin(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LoginResponse; + return result200; + }); + } else if (status === 401) { + return response.text().then((_responseText) => { + let result401: any = null; + result401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails; + return throwException("A server side error occurred.", status, _responseText, _headers, result401); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + +export class DashboardClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5099"; + } + + summary(year: number | undefined, month: number | null | undefined): Promise { + let url_ = this.baseUrl + "/api/dashboard/summary?"; + if (year === null) + throw new globalThis.Error("The parameter 'year' cannot be null."); + else if (year !== undefined) + url_ += "year=" + encodeURIComponent("" + year) + "&"; + if (month !== undefined && month !== null) + url_ += "month=" + encodeURIComponent("" + month) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processSummary(_response); + }); + } + + protected processSummary(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as SummaryDto; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + spendingByCategory(year: number | undefined, month: number | null | undefined): Promise { + let url_ = this.baseUrl + "/api/dashboard/spending-by-category?"; + if (year === null) + throw new globalThis.Error("The parameter 'year' cannot be null."); + else if (year !== undefined) + url_ += "year=" + encodeURIComponent("" + year) + "&"; + if (month !== undefined && month !== null) + url_ += "month=" + encodeURIComponent("" + month) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processSpendingByCategory(_response); + }); + } + + protected processSpendingByCategory(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as CategorySpendingDto[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + monthlyBalances(year: number | undefined): Promise { + let url_ = this.baseUrl + "/api/dashboard/monthly-balances?"; + if (year === null) + throw new globalThis.Error("The parameter 'year' cannot be null."); + else if (year !== undefined) + url_ += "year=" + encodeURIComponent("" + year) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processMonthlyBalances(_response); + }); + } + + protected processMonthlyBalances(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MonthlyBalanceDto[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + cumulativeSpending(year: number | undefined, month: number | undefined): Promise { + let url_ = this.baseUrl + "/api/dashboard/cumulative-spending?"; + if (year === null) + throw new globalThis.Error("The parameter 'year' cannot be null."); + else if (year !== undefined) + url_ += "year=" + encodeURIComponent("" + year) + "&"; + if (month === null) + throw new globalThis.Error("The parameter 'month' cannot be null."); + else if (month !== undefined) + url_ += "month=" + encodeURIComponent("" + month) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processCumulativeSpending(_response); + }); + } + + protected processCumulativeSpending(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as CumulativeSpendingDto[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + +export class TransactionsClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5099"; + } + + import(file: FileParameter | null | undefined): Promise { + let url_ = this.baseUrl + "/api/transactions/import"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = new FormData(); + if (file !== null && file !== undefined) + content_.append("file", file.data, file.fileName ? file.fileName : "file"); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processImport(_response); + }); + } + + protected processImport(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ImportResult; + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorResult; + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + list(year: number | undefined, month: number | null | undefined, category: string | null | undefined, search: string | null | undefined, page: number | undefined, pageSize: number | undefined): Promise { + let url_ = this.baseUrl + "/api/transactions?"; + if (year === null) + throw new globalThis.Error("The parameter 'year' cannot be null."); + else if (year !== undefined) + url_ += "year=" + encodeURIComponent("" + year) + "&"; + if (month !== undefined && month !== null) + url_ += "month=" + encodeURIComponent("" + month) + "&"; + if (category !== undefined && category !== null) + url_ += "category=" + encodeURIComponent("" + category) + "&"; + if (search !== undefined && search !== null) + url_ += "search=" + encodeURIComponent("" + search) + "&"; + if (page === null) + throw new globalThis.Error("The parameter 'page' cannot be null."); + else if (page !== undefined) + url_ += "page=" + encodeURIComponent("" + page) + "&"; + if (pageSize === null) + throw new globalThis.Error("The parameter 'pageSize' cannot be null."); + else if (pageSize !== undefined) + url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processList(_response); + }); + } + + protected processList(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TransactionListResponse; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + categories(): Promise { + let url_ = this.baseUrl + "/api/transactions/categories"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processCategories(_response); + }); + } + + protected processCategories(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string[]; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + +export interface LoginResponse { + token?: string; + expiresAt?: string; +} + +export interface ProblemDetails { + type?: string | undefined; + title?: string | undefined; + status?: number | undefined; + detail?: string | undefined; + instance?: string | undefined; + + [key: string]: any; +} + +export interface LoginRequest { + username?: string; + password?: string; +} + +export interface SummaryDto { + totalSpent?: number; + totalIncome?: number; +} + +export interface CategorySpendingDto { + category?: string; + total?: number; +} + +export interface MonthlyBalanceDto { + month?: number; + closingBalance?: number; +} + +export interface CumulativeSpendingDto { + day?: number; + cumulativeSpent?: number; +} + +export interface ImportResult { + recordsImported?: number; + recordsSkipped?: number; +} + +export interface ErrorResult { + error?: string; +} + +export interface TransactionListResponse { + items?: TransactionDto[]; + totalCount?: number; + page?: number; + pageSize?: number; +} + +export interface TransactionDto { + id?: number; + bookingDate?: string; + counterPartyName?: string | undefined; + category?: string | undefined; + amount?: number; + balance?: number; + message?: string | undefined; +} + +export interface FileParameter { + data: any; + fileName: string; +} + +export class SwaggerException extends Error { + override message: string; + status: number; + response: string; + headers: { [key: string]: any; }; + result: any; + + constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) { + super(); + + this.message = message; + this.status = status; + this.response = response; + this.headers = headers; + this.result = result; + } + + protected isSwaggerException = true; + + static isSwaggerException(obj: any): obj is SwaggerException { + return obj.isSwaggerException === true; + } +} + +function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any { + if (result !== null && result !== undefined) + throw result; + else + throw new SwaggerException(message, status, response, headers, null); +} \ No newline at end of file diff --git a/src/AccountTracking.Web/src/api/index.ts b/src/AccountTracking.Web/src/api/index.ts new file mode 100644 index 0000000..eb90709 --- /dev/null +++ b/src/AccountTracking.Web/src/api/index.ts @@ -0,0 +1,27 @@ +import { AuthClient, TransactionsClient, DashboardClient } from './apiClient' + +// Custom fetch that injects the JWT and handles 401 +class AuthFetch { + fetch(url: RequestInfo, init?: RequestInit): Promise { + const token = localStorage.getItem('token') + const headers = new Headers(init?.headers) + if (token) headers.set('Authorization', `Bearer ${token}`) + + return fetch(url, { ...init, headers }).then((res) => { + if (res.status === 401) { + localStorage.removeItem('token') + localStorage.removeItem('expiresAt') + // Navigate to login — import router lazily to avoid circular dep + import('../router').then(({ default: router }) => router.push('/login')) + } + return res + }) + } +} + +const httpClient = new AuthFetch() + +// Base URL is empty: NSwag generates full paths including /api/ prefix +export const authApi = new AuthClient('', httpClient) +export const transactionsApi = new TransactionsClient('', httpClient) +export const dashboardApi = new DashboardClient('', httpClient)