Merge branch 'hotfix/fix-bearer-token'
All checks were successful
continuous-integration/drone/tag Build is passing

This commit is contained in:
2020-07-12 13:30:26 +02:00
13 changed files with 169 additions and 71 deletions

View File

@@ -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

View File

@@ -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"]

View File

@@ -7,6 +7,6 @@ COPY package-lock.json .
RUN npm install
COPY . .
EXPOSE 3000
EXPOSE 3500
CMD ["npm", "run", "start:${ENVIRONMENT}"]

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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;

View File

@@ -25,19 +25,23 @@ export class SinusBotService {
private bearer: string;
public async fetchStats(): Promise<TableEntry[]> {
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<string> {
@@ -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;
}

View File

@@ -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": {

View File

@@ -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 (
<div className="App">
<p className="title">Humenius' TeamSpeak 3-Ranking</p>
{ this.state.error != null ? <p className="error-message">Data could not be loaded. Please try again later!</p> : null}
{ this.state.error != null ? <p className="error-message"> { this.state.error } Please try again later!</p> : null}
<table>
<thead>
<tr>
@@ -89,7 +95,7 @@ export default class App extends React.Component {
<th>Online time</th>
</tr>
</thead>
<tbody className={this.state.error != null ? "blurred" : undefined}>
<tbody className={this.state.error != null || !this.state.isLoaded ? "blurred" : undefined}>
{this.renderTableData()}
</tbody>
</table>

View File

@@ -19,4 +19,8 @@ export default class UserStatsMockService extends UserStatsService {
async getStats(): Promise<TableEntry[]> {
return Promise.resolve(this.entries);
}
getStatsWithoutPromise(): TableEntry[] {
return this.entries;
}
}

View File

@@ -0,0 +1,3 @@
export default class RequestError extends Error {
response?: any;
}

View File

@@ -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<TableEntry[]> {
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;
}