diff --git a/.drone.yml b/.drone.yml index 1c80823..d2a2651 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,6 @@ kind: pipeline type: docker -name: default +name: frontend steps: - name: Build and push frontend image @@ -14,7 +14,18 @@ steps: context: frontend/ repo: docker.humenius.me/humenius/ts-onlinetime-ranks-frontend registry: docker.humenius.me - tags: ["latest", "v1.${DRONE_BUILD_NUMBER}"] + tags: ["latest", "${DRONE_SEMVER}"] + +trigger: + ref: + - refs/tags/* # only trigger when tagging + +--- +kind: pipeline +type: docker +name: backend + +steps: - name: Build and push backend image image: plugins/docker settings: @@ -26,4 +37,8 @@ steps: context: backend/ repo: docker.humenius.me/humenius/ts-onlinetime-ranks-backend registry: docker.humenius.me - tags: ["latest", "v1.${DRONE_BUILD_NUMBER}"] + tags: ["latest", "${DRONE_SEMVER}"] + +trigger: + ref: + - refs/tags/* # only trigger when tagging \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 9b1b602..804abb1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,6 +10,6 @@ FROM node:14.3.0-alpine WORKDIR /app COPY --from=builder /app . -EXPOSE 3000 +EXPOSE 3500 CMD ["npm", "run", "start:prod"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 5f37230..fef11a0 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -7,6 +7,6 @@ COPY package-lock.json . RUN npm install COPY . . -EXPOSE 3000 +EXPOSE 3500 CMD ["npm", "run", "start:${ENVIRONMENT}"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 8ebf1c3..0847323 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2470,6 +2470,16 @@ "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4565,6 +4575,13 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7360,9 +7377,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.memoize": { "version": "4.1.2", @@ -7856,6 +7873,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -10379,9 +10403,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "ts-jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.2.1.tgz", - "integrity": "sha512-TnntkEEjuXq/Gxpw7xToarmHbAafgCaAzOpnajnFC6jI7oo1trMzAHA04eWpc3MhV6+yvhE8uUBAmN+teRJh0A==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz", + "integrity": "sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A==", "dev": true, "requires": { "bs-logger": "0.x", @@ -10390,10 +10414,10 @@ "json5": "2.x", "lodash.memoize": "4.x", "make-error": "1.x", - "mkdirp": "0.x", - "resolve": "1.x", - "semver": "^5.5", - "yargs-parser": "^16.1.0" + "micromatch": "4.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "18.x" }, "dependencies": { "json5": { @@ -10405,15 +10429,27 @@ "minimist": "^1.2.5" } }, - "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "dev": true, "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "braces": "^3.0.1", + "picomatch": "^2.0.5" } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true } } }, @@ -10937,7 +10973,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/backend/package.json b/backend/package.json index 22b79e9..b7df95b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,8 +1,7 @@ { - "name": "backend", - "version": "0.0.1", - "description": "", - "author": "", + "name": "ts-onlinetime-ranks-backend", + "version": "0.0.2", + "description": "Backend microservice for TeamSpeak 3 Online Time Ranking", "private": true, "license": "UNLICENSED", "scripts": { @@ -46,7 +45,7 @@ "jest": "^25.1.0", "prettier": "^1.19.1", "supertest": "^4.0.2", - "ts-jest": "25.2.1", + "ts-jest": "^26.1.1", "ts-loader": "^6.2.1", "ts-node": "^8.6.2", "tsconfig-paths": "^3.9.0", diff --git a/backend/src/main.ts b/backend/src/main.ts index da5451c..d5a2baa 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -4,6 +4,6 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); - await app.listen(3000); + await app.listen(3500); } bootstrap(); diff --git a/backend/src/models/RequestError.ts b/backend/src/models/RequestError.ts new file mode 100644 index 0000000..19dac85 --- /dev/null +++ b/backend/src/models/RequestError.ts @@ -0,0 +1,12 @@ +// @ts-ignore +interface RequestError extends Error { + response?: any; +} + +interface RequestErrorConstructor extends ErrorConstructor { + new(message?: string): RequestError; + (message?: string): RequestError; + readonly prototype: RequestError; +} + +declare var RequestError: RequestErrorConstructor; diff --git a/backend/src/services/sinusbot.service.ts b/backend/src/services/sinusbot.service.ts index 79d4269..4e2af7d 100644 --- a/backend/src/services/sinusbot.service.ts +++ b/backend/src/services/sinusbot.service.ts @@ -25,19 +25,23 @@ export class SinusBotService { private bearer: string; public async fetchStats(): Promise { - if (this.bearer == null) { - logger.info(`Hey! I'm trying to get my Bearer token!`); - await this.login() - .then(token => this.bearer = token) - .catch(error => { - logger.error(`Oh oh! Something went wrong while fetching Bearer token.`, error); - throw this.createHttpException(HttpStatus.UNAUTHORIZED, error.message); - }); - logger.debug( - `Seems like I have my Bearer token! - Looks like it's ${this.bearer == null ? 'undefined' : 'not undefined'}` - ); - } + // Skip check as either way + // - An interval needs to reset this.bearer to null + // - The Sinusbot token is not a JWT => Expiration date is not decodable + // - Estimated expiration time: 1d? + // - I don't know if it makes a difference to check via interval or to just fetch the token + // everytime a request is sent against this API. + // if (this.bearer == null) { + logger.info(`Hey! I'm trying to get my Bearer token!`); + await this.login() + .then(token => this.bearer = token) + .then(() => { + logger.debug( + `Seems like I have my Bearer token! + Looks like it's ${this.bearer == null ? 'undefined' : 'not undefined'}` + ); + }); + // } logger.info(`I try to fetch user data now! The URL is called ${this.tunaKillURL}`); return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer)) @@ -46,7 +50,7 @@ export class SinusBotService { .then(data => this.consumeTunakillResponse(data)) .catch(error => { logger.error(`I couldn't fetch user data.`, error); - throw this.createHttpException(error.statusCode, error.message); + throw this.createHttpException(HttpStatus.INTERNAL_SERVER_ERROR, error.message); }); } @@ -78,7 +82,15 @@ export class SinusBotService { return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body))) .then(res => this.checkStatus(res)) .then(res => res.json()) - .then(data => data.token); + .then(data => data.token) + .catch(error => { + logger.error(`Oh oh! Something went wrong while fetching Bearer token.`, error); + throw this.createHttpException( + HttpStatus.UNAUTHORIZED, + `Fetching Bearer token for Sinusbot failed. + Please refresh page or try again later!` + ); + }); } private async fetchDefaultBotId(): Promise { @@ -90,12 +102,12 @@ export class SinusBotService { private consumeTunakillResponse(data: any): TableEntry[] { if (!(data !== null || data[0] !== null || data[0].data !== null)) { - throw Error('Response from SinusBot does not have any data to parse.') + throw Error('Response from SinusBot does not have any data to parse.'); } const response = data[0].data; if (!(response.length > 0)) { - throw Error('User list is empty.') + throw Error('Response from SinusBot does not have any data to parse.'); } return response @@ -119,7 +131,7 @@ export class SinusBotService { headers: { 'Accept': '*/*', 'Content-Type': 'application/json', - 'User-Agent': 'HumeniusTSRankingBackend/0.0.1', + 'User-Agent': 'HumeniusTSRankingBackend/0.0.2', 'Authorization': `Bearer ${bearerToken}` } }; @@ -134,7 +146,9 @@ export class SinusBotService { private checkStatus = response => { if (!response.ok) { - throw Error(response.statusText); + let err = new RequestError(response.errorText); + err.response = response; + throw err; } return response; } diff --git a/frontend/package.json b/frontend/package.json index f2a28b1..87334d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { - "name": "ts-onlinetime-ranks", - "version": "0.1.0", + "name": "ts-onlinetime-ranks-frontend", + "version": "0.0.2", "private": true, "proxy": "https://api.tsotr.humenius.me", "dependencies": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e7c791c..4223f90 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,46 +1,50 @@ import React from 'react'; import './App.scss'; import UserStatsMockService from "./mock/UserStatsMockService"; -import UserStats from "./models/TableEntry"; import UserStatsService from "./services/UserStatsService"; import {findDOMNode} from "react-dom"; import TableEntry from "./models/TableEntry"; interface State { - error?: any, + error?: string, isLoaded: boolean, - users?: UserStats[], - mock?: UserStats[] + users?: TableEntry[], + mock?: TableEntry[] } export default class App extends React.Component { private apiService: UserStatsService = new UserStatsService(); - private mockService: UserStatsService = new UserStatsMockService(); + private mockService = new UserStatsMockService(); state: State = { - error: null, + error: undefined, isLoaded: false, users: undefined, mock: undefined } componentDidMount() { - this.setState({isLoaded: false}) - this.mockService.getStats() - .then(data => this.setState({ - data: data, - mock: data - })); + this.setState({isLoaded: false, mock: this.mockService.getStatsWithoutPromise()}); + // this.mockService.getStats() + // .then(data => this.setState({ + // data: data, + // mock: data + // })) + // .finally(() => { + // }); this.apiService.getStats() .then(data => this.setState({ isLoaded: true, users: data })) - .catch(error => this.setState({ - isLoaded: true, - error: error - })); + .catch(error => { + error.response.json() + .then((err: any) => this.setState({ + isLoaded: true, + error: err.error + })) + }); } componentDidUpdate() { @@ -73,6 +77,8 @@ export default class App extends React.Component { return this.createTableEntries(users); } else if (isLoaded && error != null && mock != null) { return this.createTableEntries(mock); + } else if (mock != null) { + return this.createTableEntries(mock); } } @@ -80,7 +86,7 @@ export default class App extends React.Component { return (

Humenius' TeamSpeak 3-Ranking

- { this.state.error != null ?

Data could not be loaded. Please try again later!

: null} + { this.state.error != null ?

{ this.state.error } Please try again later!

: null} @@ -89,7 +95,7 @@ export default class App extends React.Component { - + {this.renderTableData()}
Online time
diff --git a/frontend/src/mock/UserStatsMockService.ts b/frontend/src/mock/UserStatsMockService.ts index 0f558e2..41de68a 100644 --- a/frontend/src/mock/UserStatsMockService.ts +++ b/frontend/src/mock/UserStatsMockService.ts @@ -19,4 +19,8 @@ export default class UserStatsMockService extends UserStatsService { async getStats(): Promise { return Promise.resolve(this.entries); } + + getStatsWithoutPromise(): TableEntry[] { + return this.entries; + } } diff --git a/frontend/src/models/RequestError.ts b/frontend/src/models/RequestError.ts new file mode 100644 index 0000000..376f0fc --- /dev/null +++ b/frontend/src/models/RequestError.ts @@ -0,0 +1,3 @@ +export default class RequestError extends Error { + response?: any; +} diff --git a/frontend/src/services/UserStatsService.ts b/frontend/src/services/UserStatsService.ts index f256c9f..e41b41d 100644 --- a/frontend/src/services/UserStatsService.ts +++ b/frontend/src/services/UserStatsService.ts @@ -1,4 +1,6 @@ import TableEntry from "../models/TableEntry"; +import RequestError from "../models/RequestError"; + export default class UserStatsService { @@ -13,13 +15,16 @@ export default class UserStatsService { async getStats(): Promise { return fetch(this.apiURL, this.requestInit) - .then(res => this.checkResponse(res)) - .then(data => data.json()) + .then(res => UserStatsService.checkResponse(res)) + .then(data => data.json()); } - private checkResponse(response: any): any { + private static checkResponse(response: any): any { if (!response.ok) { - throw Error(response.statusText); + console.log(response); + let error = new RequestError(response.statusText); + error.response = response; + throw error; } return response; }