├── backend ├── .prettierignore ├── nest-cli.json ├── tsconfig.build.json ├── .env.example ├── src │ ├── dto │ │ ├── getDataByName.dto.ts │ │ ├── getLegendByName.dto.ts │ │ ├── getDataBySteamID.dto.ts │ │ ├── getDataBySteamURL.dto.ts │ │ ├── getDataByBHID.dto.ts │ │ ├── getDataByClanID.dto.ts │ │ ├── getLegendByID.dto.ts │ │ └── getDataByRankingOptions.dto.ts │ ├── @types │ │ └── api-types │ │ │ └── index.d.ts │ ├── routers │ │ ├── glory │ │ │ ├── glory.module.ts │ │ │ ├── glory.controller.ts │ │ │ └── glory.service.ts │ │ ├── stats │ │ │ ├── stats.module.ts │ │ │ ├── stats.controller.ts │ │ │ └── stats.service.ts │ │ ├── utils │ │ │ ├── utils.module.ts │ │ │ ├── utils.controller.ts │ │ │ └── utils.service.ts │ │ ├── ranked │ │ │ ├── ranked.module.ts │ │ │ ├── ranked.controller.ts │ │ │ └── ranked.service.ts │ │ ├── legends │ │ │ ├── legends.module.ts │ │ │ ├── legends.controller.ts │ │ │ └── legends.service.ts │ │ └── steamdata │ │ │ ├── steamdata.module.ts │ │ │ ├── steamdata.controller.ts │ │ │ └── steamdata.service.ts │ ├── config.ts │ ├── main.ts │ └── app.module.ts ├── .prettierrc ├── tsconfig.json ├── .gitignore ├── eslint.config.mjs └── package.json ├── frontend ├── .prettierignore ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── config.js │ ├── images │ │ └── banner.png │ ├── styles │ │ ├── index.css │ │ └── 404.css │ ├── components │ │ ├── Feature │ │ │ └── Banner.js │ │ ├── Features │ │ │ ├── Banner.js │ │ │ └── FeatureItem.js │ │ ├── Landing │ │ │ ├── Banner.js │ │ │ └── Example.js │ │ └── Layout.js │ ├── index.js │ ├── pages │ │ ├── Landing.js │ │ ├── Features.js │ │ ├── Feature.js │ │ └── NotFound.js │ ├── App.js │ └── features.js ├── .prettierrc ├── .gitignore └── package.json ├── .gitmodules ├── .dockerignore ├── Dockerfile.backend ├── Dockerfile.frontend ├── docker-compose.yml ├── README.md ├── .github └── workflows │ └── build.yml └── LICENSE /backend/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "secrets"] 2 | path = secrets 3 | url = https://github.com/barbarbar338/secrets 4 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | apiVersion: "api", 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "dist"], 3 | "extends": "./tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barbarbar338/bh-open-api/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barbarbar338/bh-open-api/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barbarbar338/bh-open-api/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barbarbar338/bh-open-api/HEAD/frontend/src/images/banner.png -------------------------------------------------------------------------------- /frontend/src/styles/index.css: -------------------------------------------------------------------------------- 1 | .mg-auto { 2 | margin: auto; 3 | } 4 | 5 | .pd-1-5 { 6 | padding: 1.5rem; 7 | } 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/build 2 | backend/dist 3 | node_modules 4 | **/node_modules 5 | npm-debug.log 6 | Dockerfile 7 | *.env* -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Required configuration 2 | BH_API_KEY= 3 | REDIS_URL= 4 | 5 | # No need to change these 6 | TTL_MINS=15 7 | API_PREFIX=/api 8 | PORT=5555 9 | -------------------------------------------------------------------------------- /backend/src/dto/getDataByName.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined } from "class-validator"; 2 | 3 | export class GetDataByNameDTO { 4 | @IsDefined() 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/dto/getLegendByName.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined } from "class-validator"; 2 | 3 | export class GetLegendByNameDTO { 4 | @IsDefined() 5 | legend_name: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/dto/getDataBySteamID.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsNumberString } from "class-validator"; 2 | 3 | export class GetDataBySteamIDDTO { 4 | @IsDefined() 5 | @IsNumberString() 6 | steam_id: string; 7 | } 8 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "endOfLine": "crlf", 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": false, 7 | "tabWidth": 4, 8 | "trailingComma": "all", 9 | "useTabs": true 10 | } 11 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "endOfLine": "crlf", 4 | "semi": true, 5 | "singleQuote": false, 6 | "tabWidth": 4, 7 | "trailingComma": "all", 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.pnp 3 | .pnp.js 4 | /coverage 5 | /build 6 | .DS_Store 7 | .env.local 8 | .env.development.local 9 | .env.test.local 10 | .env.production.local 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | -------------------------------------------------------------------------------- /backend/src/dto/getDataBySteamURL.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsUrl, Matches } from "class-validator"; 2 | 3 | export class GetDataBySteamURLDTO { 4 | @IsDefined() 5 | @IsUrl() 6 | @Matches(/(steamcommunity\.com\/(id|profiles)\/([^\s]+))/i) 7 | steam_url: string; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/Feature/Banner.js: -------------------------------------------------------------------------------- 1 | export default function Banner({ title }) { 2 | return ( 3 |
4 |

Feature:

5 |

{title}

6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/dto/getDataByBHID.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "class-transformer"; 2 | import { IsDefined, IsNumber } from "class-validator"; 3 | 4 | export class GetDataByBHIDDTO { 5 | @IsDefined() 6 | @Transform((i) => parseInt(i.value as string)) 7 | @IsNumber() 8 | brawlhalla_id: number; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/dto/getDataByClanID.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "class-transformer"; 2 | import { IsDefined, IsNumber } from "class-validator"; 3 | 4 | export class GetDataByClanIDDTO { 5 | @IsDefined() 6 | @Transform((i) => parseInt(i.value as string)) 7 | @IsNumber() 8 | clan_id: number; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Features/Banner.js: -------------------------------------------------------------------------------- 1 | export default function Banner() { 2 | return ( 3 |
4 |

Features

5 |

All features that the system offers you.

6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/@types/api-types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "api-types" { 2 | export interface APIRes { 3 | statusCode: number; 4 | message: string | string[]; 5 | data: T; 6 | error?: string | string[]; 7 | } 8 | 9 | export interface UnknownObject { 10 | [key: string]: unknown; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/routers/glory/glory.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { GloryController } from "./glory.controller"; 3 | import { GloryService } from "./glory.service"; 4 | 5 | @Module({ 6 | controllers: [GloryController], 7 | providers: [GloryService], 8 | exports: [GloryService], 9 | }) 10 | export class GloryModule {} 11 | -------------------------------------------------------------------------------- /backend/src/routers/stats/stats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { StatsController } from "./stats.controller"; 3 | import { StatsService } from "./stats.service"; 4 | 5 | @Module({ 6 | controllers: [StatsController], 7 | providers: [StatsService], 8 | exports: [StatsService], 9 | }) 10 | export class StatsModule {} 11 | -------------------------------------------------------------------------------- /backend/src/routers/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { UtilsController } from "./utils.controller"; 3 | import { UtilsService } from "./utils.service"; 4 | 5 | @Module({ 6 | controllers: [UtilsController], 7 | providers: [UtilsService], 8 | exports: [UtilsService], 9 | }) 10 | export class UtilsModule {} 11 | -------------------------------------------------------------------------------- /Dockerfile.backend: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS backend-builder 2 | 3 | WORKDIR /app 4 | 5 | COPY backend/package.json backend/yarn.lock ./ 6 | RUN yarn install --frozen-lockfile 7 | 8 | COPY backend . 9 | RUN yarn build 10 | 11 | FROM node:lts-alpine 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=backend-builder /app . 16 | 17 | CMD ["npm", "run", "start"] 18 | -------------------------------------------------------------------------------- /backend/src/routers/ranked/ranked.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { RankedController } from "./ranked.controller"; 3 | import { RankedService } from "./ranked.service"; 4 | 5 | @Module({ 6 | controllers: [RankedController], 7 | providers: [RankedService], 8 | exports: [RankedService], 9 | }) 10 | export class RankedModule {} 11 | -------------------------------------------------------------------------------- /backend/src/dto/getLegendByID.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "class-transformer"; 2 | import { IsDefined, IsNumber, Max, Min, NotEquals } from "class-validator"; 3 | 4 | export class GetLegendByIDDTO { 5 | @IsDefined() 6 | @Transform((i) => parseInt(i.value as string)) 7 | @IsNumber() 8 | @Min(3) 9 | @Max(68) 10 | @NotEquals(61) 11 | legend_id: number; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/routers/legends/legends.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { LegendsController } from "./legends.controller"; 3 | import { LegendsService } from "./legends.service"; 4 | 5 | @Module({ 6 | controllers: [LegendsController], 7 | providers: [LegendsService], 8 | exports: [LegendsService], 9 | }) 10 | export class LegendsModule {} 11 | -------------------------------------------------------------------------------- /backend/src/routers/steamdata/steamdata.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { SteamDataController } from "./steamdata.controller"; 3 | import { SteamDataService } from "./steamdata.service"; 4 | 5 | @Module({ 6 | controllers: [SteamDataController], 7 | providers: [SteamDataService], 8 | exports: [SteamDataService], 9 | }) 10 | export class SteamDataModule {} 11 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": "./", 5 | "declaration": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "incremental": true, 9 | "module": "commonjs", 10 | "outDir": "./dist", 11 | "removeComments": true, 12 | "sourceMap": true, 13 | "target": "es2021" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile.frontend: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS frontend-builder 2 | 3 | WORKDIR /app 4 | 5 | COPY frontend/package.json frontend/yarn.lock ./ 6 | RUN yarn install --frozen-lockfile 7 | 8 | COPY frontend . 9 | RUN yarn build 10 | 11 | FROM node:lts-alpine 12 | 13 | RUN npm i -g serve 14 | 15 | WORKDIR /app 16 | 17 | COPY --from=frontend-builder /app/build ./build 18 | 19 | CMD ["serve", "-s", "build"] -------------------------------------------------------------------------------- /backend/src/dto/getDataByRankingOptions.dto.ts: -------------------------------------------------------------------------------- 1 | import { RankedRegion } from "@barbarbar338/bhapi"; 2 | import { IsDefined, IsEnum } from "class-validator"; 3 | 4 | export class GetDataByRankingOptionsDTO { 5 | @IsDefined() 6 | region: RankedRegion; 7 | 8 | @IsDefined() 9 | page: string | number; 10 | 11 | @IsDefined() 12 | @IsEnum(["1v1", "2v2", "seasonal"]) 13 | type: "1v1" | "2v2" | "seasonal"; 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bhapi: 3 | image: ghcr.io/barbarbar338/bhapi:latest 4 | restart: unless-stopped 5 | ports: 6 | - "5555:5555" 7 | environment: 8 | - BH_API_KEY= 9 | - REDIS_URL=redis://redis:6379 # create your own redis server 10 | 11 | bhapi-fe: 12 | image: ghcr.io/barbarbar338/bhapi-fe:latest 13 | restart: unless-stopped 14 | ports: 15 | - "3000:3000" 16 | depends_on: 17 | - bhapi 18 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # Others 33 | *.env* 34 | !.env.example 35 | -------------------------------------------------------------------------------- /backend/src/config.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, num, port, str, url } from "envalid"; 2 | 3 | const env = cleanEnv(process.env, { 4 | API_PREFIX: str({ default: "/api" }), 5 | PORT: port({ default: 5555 }), 6 | TTL_MINS: num({ default: 15 }), 7 | BH_API_KEY: str({ 8 | desc: "API key from Brawlhalla", 9 | }), 10 | REDIS_URL: url({ 11 | desc: "Redis URL", 12 | }), 13 | }); 14 | 15 | export default { 16 | API_PREFIX: env.API_PREFIX, 17 | PORT: env.PORT, 18 | TTL_MINS: env.TTL_MINS, 19 | BH_API_KEY: env.BH_API_KEY, 20 | REDIS_URL: env.REDIS_URL, 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import React from "react"; 3 | import { createRoot } from "react-dom/client"; 4 | 5 | import "bootstrap/dist/css/bootstrap.min.css"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | import "./styles/404.css"; 8 | import "./styles/index.css"; 9 | 10 | import "bootstrap/dist/js/bootstrap.bundle"; 11 | 12 | import App from "./App"; 13 | 14 | window.jQuery = window.$ = $; 15 | 16 | const root = createRoot(document.getElementById("root")); 17 | 18 | root.render( 19 | 20 | 21 | , 22 | ); 23 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Brawlhalla Open API", 3 | "name": "Brawlhalla Open API", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥 Brawlhalla Open API 2 | 3 | - An unofficial API server with no connection to Brawlhalla and its developers, prepared for easier and unlimited access to Brawlhalla API services. ✅ 4 | - See for live version. 5 | 6 | ## 💢 SelfHost Using docker-compose 7 | 8 | ```yml 9 | services: 10 | bhapi: 11 | image: ghcr.io/barbarbar338/bhapi:latest 12 | restart: unless-stopped 13 | ports: 14 | - "5555:5555" 15 | environment: 16 | - BH_API_KEY= 17 | - REDIS_URL=redis://redis:6379 # create your own redis server 18 | 19 | bhapi-fe: 20 | image: ghcr.io/barbarbar338/bhapi-fe:latest 21 | restart: unless-stopped 22 | ports: 23 | - "3000:3000" 24 | depends_on: 25 | - bhapi 26 | ``` 27 | 28 | ## 🔗 Contributing / Issues / Ideas 29 | 30 | - Feel free to use GitHub's features ✨ 31 | -------------------------------------------------------------------------------- /frontend/src/pages/Landing.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import Banner from "../components/Landing/Banner"; 3 | import Example from "../components/Landing/Example"; 4 | import Layout from "../components/Layout"; 5 | 6 | export default class Landing extends Component { 7 | state = { 8 | meta: { 9 | title: "Brawlhalla Open API", 10 | description: 11 | "An unofficial API server with no connection to Brawlhalla and its developers, prepared for easier and unlimited access to Brawlhalla API services.", 12 | }, 13 | }; 14 | render() { 15 | return ( 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Brawlhalla Open API 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/pages/Features.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import Banner from "../components/Features/Banner"; 3 | import FeatureItem from "../components/Features/FeatureItem"; 4 | import Layout from "../components/Layout"; 5 | import FEATURES from "../features"; 6 | 7 | export default class Landing extends Component { 8 | state = { 9 | meta: { 10 | title: "Brawlhalla Open API - Features", 11 | description: "All features that the system offers you.", 12 | }, 13 | }; 14 | handleFeatures() { 15 | return FEATURES.map((featureItemData, i) => { 16 | return ; 17 | }); 18 | } 19 | render() { 20 | return ( 21 | 22 |
23 | 24 |
25 |
26 |
{this.handleFeatures()}
27 |
28 |
29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/styles/404.css: -------------------------------------------------------------------------------- 1 | .container-404 { 2 | min-height: 600px; 3 | margin: auto; 4 | width: auto; 5 | max-width: 460px; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | .path { 11 | stroke-dasharray: 300; 12 | stroke-dashoffset: 300; 13 | animation: dash 4s alternate infinite; 14 | } 15 | .abstract-bg { 16 | animation: scales 3s alternate infinite; 17 | transform-origin: center; 18 | } 19 | .left-sparks { 20 | animation: left-sparks 1.5s alternate infinite; 21 | transform-origin: 150px 156px; 22 | } 23 | .right-sparks { 24 | animation: left-sparks 1.5s alternate infinite; 25 | transform-origin: 310px 150px; 26 | } 27 | @keyframes scales { 28 | from { 29 | transform: scale(0.98); 30 | } 31 | to { 32 | transform: scale(1); 33 | } 34 | } 35 | @keyframes left-sparks { 36 | 0% { 37 | opacity: 0; 38 | } 39 | } 40 | @keyframes dash { 41 | 0%, 42 | 30% { 43 | stroke-dashoffset: 0; 44 | } 45 | 80%, 46 | 100% { 47 | fill: transparent; 48 | stroke-dashoffset: -200; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import prettierEslingPlugin from "eslint-plugin-prettier"; 3 | import globals from "globals"; 4 | import prettier from "prettier"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | const prettierConfig = await prettier.resolveConfig("./.prettierrc"); 8 | 9 | export default tseslint.config( 10 | { 11 | ignores: ["eslint.config.mjs", "**/dist/**/*"], 12 | }, 13 | eslint.configs.recommended, 14 | ...tseslint.configs.recommendedTypeChecked, 15 | { 16 | plugins: { 17 | prettier: prettierEslingPlugin, 18 | }, 19 | rules: { 20 | "prettier/prettier": ["error", prettierConfig], 21 | }, 22 | }, 23 | { 24 | languageOptions: { 25 | globals: { 26 | ...globals.node, 27 | }, 28 | sourceType: "commonjs", 29 | parserOptions: { 30 | projectService: true, 31 | tsconfigRootDir: import.meta.dirname, 32 | }, 33 | }, 34 | }, 35 | { 36 | rules: { 37 | "@typescript-eslint/no-explicit-any": "off", 38 | "@typescript-eslint/no-floating-promises": "warn", 39 | "@typescript-eslint/no-unsafe-argument": "warn", 40 | "@typescript-eslint/no-unsafe-assignment": "warn", 41 | "@typescript-eslint/no-unsafe-return": "warn", 42 | "@typescript-eslint/no-unsafe-member-access": "warn", 43 | }, 44 | }, 45 | ); 46 | -------------------------------------------------------------------------------- /backend/src/routers/steamdata/steamdata.controller.ts: -------------------------------------------------------------------------------- 1 | import { BHIDFromSteamID, SteamData } from "@barbarbar338/bhapi"; 2 | import { CacheTTL } from "@nestjs/cache-manager"; 3 | import { Controller, Get, Query } from "@nestjs/common"; 4 | import { APIRes } from "api-types"; 5 | import { GetDataBySteamIDDTO } from "src/dto/getDataBySteamID.dto"; 6 | import { GetDataBySteamURLDTO } from "src/dto/getDataBySteamURL.dto"; 7 | import { SteamDataService } from "./steamdata.service"; 8 | 9 | @Controller("steamdata") 10 | @CacheTTL(0) // No cache expiration for Steam data since it is static 11 | export class SteamDataController { 12 | constructor(private readonly steamDataService: SteamDataService) {} 13 | 14 | @Get("ping") 15 | public returnPing(): APIRes { 16 | return this.steamDataService.returnPing(); 17 | } 18 | 19 | @Get("id") 20 | public async getSteamDataByID( 21 | @Query() getDataBySteamIDDTO: GetDataBySteamIDDTO, 22 | ): Promise> { 23 | return this.steamDataService.getSteamDataByID(getDataBySteamIDDTO); 24 | } 25 | 26 | @Get("url") 27 | public async getSteamDataByURL( 28 | @Query() getDataBySteamURLDTO: GetDataBySteamURLDTO, 29 | ): Promise> { 30 | return this.steamDataService.getSteamDataByURL(getDataBySteamURLDTO); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/routers/utils/utils.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Clan, 3 | Ranking1v1, 4 | Ranking2v2, 5 | RankingSeasonal, 6 | } from "@barbarbar338/bhapi"; 7 | import { CacheTTL } from "@nestjs/cache-manager"; 8 | import { Controller, Get, Query } from "@nestjs/common"; 9 | import { APIRes } from "api-types"; 10 | import { GetDataByClanIDDTO } from "src/dto/getDataByClanID.dto"; 11 | import { GetDataByRankingOptionsDTO } from "src/dto/getDataByRankingOptions.dto"; 12 | import { UtilsService } from "./utils.service"; 13 | 14 | @Controller("utils") 15 | export class UtilsController { 16 | constructor(private readonly utilsService: UtilsService) {} 17 | 18 | @Get("ping") 19 | @CacheTTL(0) 20 | public returnPing(): APIRes { 21 | return this.utilsService.returnPing(); 22 | } 23 | 24 | @Get("rankings") 25 | public async getRankedDataByRankingOptions( 26 | @Query() getDataByRankingOptionsDTO: GetDataByRankingOptionsDTO, 27 | ): Promise> { 28 | return this.utilsService.getRankedDataByRankingOptions( 29 | getDataByRankingOptionsDTO, 30 | ); 31 | } 32 | 33 | @Get("clan") 34 | public async getDataByClanIDDTO( 35 | @Query() getDataByClanIDDTO: GetDataByClanIDDTO, 36 | ): Promise> { 37 | return this.utilsService.getDataByClanID(getDataByClanIDDTO); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setApiKey } from "@barbarbar338/bhapi"; 2 | import { Logger, ValidationPipe } from "@nestjs/common"; 3 | import { NestFactory } from "@nestjs/core"; 4 | import { 5 | FastifyAdapter, 6 | NestFastifyApplication, 7 | } from "@nestjs/platform-fastify"; 8 | import * as morgan from "morgan"; 9 | import { AppModule } from "./app.module"; 10 | import config from "./config"; 11 | 12 | async function bootstrap() { 13 | Logger.log("Setting Brawlhalla API key", "BHAPI"); 14 | setApiKey(config.BH_API_KEY); 15 | Logger.log("Brawlhalla API key set successfully", "BHAPI"); 16 | 17 | const app = await NestFactory.create( 18 | AppModule, 19 | new FastifyAdapter(), 20 | ); 21 | 22 | app.enableCors({ 23 | origin: "*", 24 | }); 25 | app.use(morgan("dev")); 26 | 27 | app.useGlobalPipes( 28 | new ValidationPipe({ 29 | transform: true, 30 | }), 31 | ); 32 | 33 | app.setGlobalPrefix(config.API_PREFIX); 34 | 35 | await app.listen(config.PORT, "0.0.0.0"); 36 | } 37 | 38 | bootstrap() 39 | .then(() => { 40 | Logger.log( 41 | "Application started successfully on " + config.PORT, 42 | "Main", 43 | ); 44 | }) 45 | .catch((error: Error) => { 46 | Logger.error( 47 | `Error starting application: ${error.message}`, 48 | error.stack, 49 | "Main", 50 | ); 51 | process.exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /frontend/src/components/Landing/Banner.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import config from "../../config"; 3 | import BannerImage from "../../images/banner.png"; 4 | 5 | export default function Banner() { 6 | const apiURL = `${window.location.protocol}//${window.location.host}/${config.apiVersion}/`; 7 | 8 | return ( 9 |
10 | Banner 18 |

Brawlhalla Open API

19 |

20 | An unofficial API server with no connection to Brawlhalla and 21 | its developers, prepared for easier and unlimited access to 22 | Brawlhalla API services. 23 |

24 |

25 | Default API URL:{" "} 26 | 27 | {apiURL} 28 | 29 |

30 | 31 | See Features 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/routers/legends/legends.controller.ts: -------------------------------------------------------------------------------- 1 | import { StaticAllLegends, StaticLegend } from "@barbarbar338/bhapi"; 2 | import { CacheTTL } from "@nestjs/cache-manager"; 3 | import { Controller, Get, Query } from "@nestjs/common"; 4 | import { APIRes } from "api-types"; 5 | import { GetLegendByIDDTO } from "src/dto/getLegendByID.dto"; 6 | import { GetLegendByNameDTO } from "src/dto/getLegendByName.dto"; 7 | import { LegendsService } from "./legends.service"; 8 | 9 | @Controller("legends") 10 | @CacheTTL(0) // No cache expiration for legend details since they are static 11 | export class LegendsController { 12 | constructor(private readonly legendsService: LegendsService) {} 13 | 14 | @Get("ping") 15 | public returnPing(): APIRes { 16 | return this.legendsService.returnPing(); 17 | } 18 | 19 | @Get("all") 20 | public async getAllLegends(): Promise> { 21 | return this.legendsService.getAllLegends(); 22 | } 23 | 24 | @Get("id") 25 | public async getLegendByID( 26 | @Query() getLegendByIDDTO: GetLegendByIDDTO, 27 | ): Promise> { 28 | return this.legendsService.getLegendByID(getLegendByIDDTO); 29 | } 30 | 31 | @Get("name") 32 | public async getLegendByName( 33 | @Query() getLegendByNameDTO: GetLegendByNameDTO, 34 | ): Promise> { 35 | return this.legendsService.getLegendByName(getLegendByNameDTO); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { createKeyv as createKeyvRedis } from "@keyv/redis"; 2 | import { CacheInterceptor, CacheModule } from "@nestjs/cache-manager"; 3 | import { Module } from "@nestjs/common"; 4 | import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core"; 5 | import { RateLimiterGuard, RateLimiterModule } from "nestjs-rate-limit"; 6 | import CONFIG from "./config"; 7 | import { GloryModule } from "./routers/glory/glory.module"; 8 | import { LegendsModule } from "./routers/legends/legends.module"; 9 | import { RankedModule } from "./routers/ranked/ranked.module"; 10 | import { StatsModule } from "./routers/stats/stats.module"; 11 | import { SteamDataModule } from "./routers/steamdata/steamdata.module"; 12 | import { UtilsModule } from "./routers/utils/utils.module"; 13 | 14 | @Module({ 15 | imports: [ 16 | CacheModule.register({ 17 | isGlobal: true, 18 | stores: [createKeyvRedis(CONFIG.REDIS_URL)], 19 | ttl: 1000 * 60 * CONFIG.TTL_MINS, 20 | }), 21 | RateLimiterModule.forRoot({ 22 | points: 60, 23 | duration: 60 * 10, 24 | keyPrefix: "global", 25 | }), 26 | LegendsModule, 27 | StatsModule, 28 | RankedModule, 29 | GloryModule, 30 | SteamDataModule, 31 | UtilsModule, 32 | ], 33 | providers: [ 34 | { provide: APP_GUARD, useClass: RateLimiterGuard }, 35 | { 36 | provide: APP_INTERCEPTOR, 37 | useClass: CacheInterceptor, 38 | }, 39 | ], 40 | }) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "hi@338.rocks", 4 | "name": "Barış DEMİRCİ", 5 | "url": "https://338.rocks" 6 | }, 7 | "browserslist": { 8 | "development": [ 9 | "last 1 chrome version", 10 | "last 1 firefox version", 11 | "last 1 safari version" 12 | ], 13 | "production": [ 14 | ">0.2%", 15 | "not dead", 16 | "not op_mini all" 17 | ] 18 | }, 19 | "dependencies": { 20 | "bootstrap": "^5.3.7", 21 | "jquery": "^3.7.1", 22 | "react": "^19.1.1", 23 | "react-document-meta": "^3.0.0-beta.2", 24 | "react-dom": "^19.1.1", 25 | "react-loader-spinner": "^6.1.6", 26 | "react-router-dom": "^7.8.0", 27 | "react-scripts": "^5.0.1", 28 | "react-toastify": "^11.0.5" 29 | }, 30 | "devDependencies": { 31 | "prettier": "^3.6.2", 32 | "serve": "^14.2.4", 33 | "taze": "^19.1.0" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "license": "MPL-2.0", 42 | "name": "bh-open-api-webpage", 43 | "private": true, 44 | "scripts": { 45 | "build": "react-scripts build", 46 | "dev": "react-scripts start", 47 | "eject": "react-scripts eject", 48 | "format": "prettier --write .", 49 | "start": "serve -s build", 50 | "update": "taze latest -w" 51 | }, 52 | "version": "3.0.0" 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/routers/glory/glory.controller.ts: -------------------------------------------------------------------------------- 1 | import { GloryData } from "@barbarbar338/bhapi"; 2 | import { CacheTTL } from "@nestjs/cache-manager"; 3 | import { Controller, Get, Query } from "@nestjs/common"; 4 | import { APIRes } from "api-types"; 5 | import { GetDataByBHIDDTO } from "src/dto/getDataByBHID.dto"; 6 | import { GetDataByNameDTO } from "src/dto/getDataByName.dto"; 7 | import { GetDataBySteamIDDTO } from "src/dto/getDataBySteamID.dto"; 8 | import { GetDataBySteamURLDTO } from "src/dto/getDataBySteamURL.dto"; 9 | import { GloryService } from "./glory.service"; 10 | 11 | @Controller("glory") 12 | export class GloryController { 13 | constructor(private readonly gloryService: GloryService) {} 14 | 15 | @Get("ping") 16 | @CacheTTL(0) 17 | public returnPing(): APIRes { 18 | return this.gloryService.returnPing(); 19 | } 20 | 21 | @Get("id") 22 | public async getGloryByID( 23 | @Query() getDataByBHIDDTO: GetDataByBHIDDTO, 24 | ): Promise> { 25 | return this.gloryService.getGloryByID(getDataByBHIDDTO); 26 | } 27 | 28 | @Get("steamid") 29 | public async getGloryBySteamID( 30 | @Query() getDataBySteamIDDTO: GetDataBySteamIDDTO, 31 | ): Promise> { 32 | return this.gloryService.getGloryBySteamID(getDataBySteamIDDTO); 33 | } 34 | 35 | @Get("steamurl") 36 | public async getGloryBySteamURL( 37 | @Query() getDataBySteamURLDTO: GetDataBySteamURLDTO, 38 | ): Promise> { 39 | return this.gloryService.getGloryBySteamURL(getDataBySteamURLDTO); 40 | } 41 | 42 | @Get("name") 43 | public async getGloryByName( 44 | @Query() getDataByNameDTO: GetDataByNameDTO, 45 | ): Promise> { 46 | return this.gloryService.getGloryByName(getDataByNameDTO); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/routers/stats/stats.controller.ts: -------------------------------------------------------------------------------- 1 | import { PlayerStats } from "@barbarbar338/bhapi"; 2 | import { CacheTTL } from "@nestjs/cache-manager"; 3 | import { Controller, Get, Query } from "@nestjs/common"; 4 | import { APIRes } from "api-types"; 5 | import { GetDataByBHIDDTO } from "src/dto/getDataByBHID.dto"; 6 | import { GetDataByNameDTO } from "src/dto/getDataByName.dto"; 7 | import { GetDataBySteamIDDTO } from "src/dto/getDataBySteamID.dto"; 8 | import { GetDataBySteamURLDTO } from "src/dto/getDataBySteamURL.dto"; 9 | import { StatsService } from "./stats.service"; 10 | 11 | @Controller("stats") 12 | export class StatsController { 13 | constructor(private readonly statsService: StatsService) {} 14 | 15 | @Get("ping") 16 | @CacheTTL(0) 17 | public returnPing(): APIRes { 18 | return this.statsService.returnPing(); 19 | } 20 | 21 | @Get("id") 22 | public async getStatsByID( 23 | @Query() getDataByBHIDDTO: GetDataByBHIDDTO, 24 | ): Promise> { 25 | return this.statsService.getStatsByID(getDataByBHIDDTO); 26 | } 27 | 28 | @Get("steamid") 29 | public async getStatsBySteamID( 30 | @Query() getDataBySteamIDDTO: GetDataBySteamIDDTO, 31 | ): Promise> { 32 | return this.statsService.getStatsBySteamID(getDataBySteamIDDTO); 33 | } 34 | 35 | @Get("steamurl") 36 | public async getStatsBySteamURL( 37 | @Query() getDataBySteamURLDTO: GetDataBySteamURLDTO, 38 | ): Promise> { 39 | return this.statsService.getStatsBySteamURL(getDataBySteamURLDTO); 40 | } 41 | 42 | @Get("name") 43 | public async getStatsByName( 44 | @Query() getDataByNameDTO: GetDataByNameDTO, 45 | ): Promise> { 46 | return this.statsService.getStatsByName(getDataByNameDTO); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 3 | import { ToastContainer } from "react-toastify"; 4 | import FEATURES from "./features"; 5 | import Feature from "./pages/Feature"; 6 | import Features from "./pages/Features"; 7 | import Landing from "./pages/Landing"; 8 | import NotFound from "./pages/NotFound"; 9 | 10 | export default class App extends Component { 11 | handleFeatureRedirects() { 12 | return FEATURES.map((featureData, i) => { 13 | return ( 14 | } 18 | /> 19 | ); 20 | }); 21 | } 22 | render() { 23 | return ( 24 |
25 | 36 | 37 | 38 | } /> 39 | } /> 40 | {this.handleFeatureRedirects()} 41 | } /> 42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/routers/ranked/ranked.controller.ts: -------------------------------------------------------------------------------- 1 | import { PlayerRanked } from "@barbarbar338/bhapi"; 2 | import { CacheTTL } from "@nestjs/cache-manager"; 3 | import { Controller, Get, Query } from "@nestjs/common"; 4 | import { APIRes } from "api-types"; 5 | import { GetDataByBHIDDTO } from "src/dto/getDataByBHID.dto"; 6 | import { GetDataByNameDTO } from "src/dto/getDataByName.dto"; 7 | import { GetDataBySteamIDDTO } from "src/dto/getDataBySteamID.dto"; 8 | import { GetDataBySteamURLDTO } from "src/dto/getDataBySteamURL.dto"; 9 | import { RankedService } from "./ranked.service"; 10 | 11 | @Controller("ranked") 12 | export class RankedController { 13 | constructor(private readonly rankedService: RankedService) {} 14 | 15 | @Get("ping") 16 | @CacheTTL(0) 17 | public returnPing(): APIRes { 18 | return this.rankedService.returnPing(); 19 | } 20 | 21 | @Get("id") 22 | public async getRankedByID( 23 | @Query() getDataByBHIDDTO: GetDataByBHIDDTO, 24 | ): Promise> { 25 | return this.rankedService.getRankedByID(getDataByBHIDDTO); 26 | } 27 | 28 | @Get("steamid") 29 | public async getRankedBySteamID( 30 | @Query() getDataBySteamIDDTO: GetDataBySteamIDDTO, 31 | ): Promise> { 32 | return this.rankedService.getRankedBySteamID(getDataBySteamIDDTO); 33 | } 34 | 35 | @Get("steamurl") 36 | public async getStatsBySteamURL( 37 | @Query() getDataBySteamURLDTO: GetDataBySteamURLDTO, 38 | ): Promise> { 39 | return this.rankedService.getRankedBySteamURL(getDataBySteamURLDTO); 40 | } 41 | 42 | @Get("name") 43 | public async getRankedByName( 44 | @Query() getDataByNameDTO: GetDataByNameDTO, 45 | ): Promise> { 46 | return this.rankedService.getRankedByName(getDataByNameDTO); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Multi-Arch Docker Images to GHCR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: "!contains(github.event.head_commit.message, '[skip-build]')" 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | 23 | - name: Cache Docker layers 24 | uses: actions/cache@v4 25 | with: 26 | path: /tmp/.buildx-cache 27 | key: ${{ runner.os }}-buildx-${{ github.sha }} 28 | restore-keys: | 29 | ${{ runner.os }}-buildx- 30 | 31 | - name: Log in to GitHub Container Registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Build and push Frontend Docker image 39 | uses: docker/build-push-action@v5 40 | with: 41 | context: . 42 | file: ./Dockerfile.frontend 43 | platforms: linux/amd64,linux/arm64 44 | push: true 45 | tags: ghcr.io/${{ github.repository_owner }}/bhapi-fe:latest 46 | cache-from: type=local,src=/tmp/.buildx-cache 47 | cache-to: type=local,dest=/tmp/.buildx-cache,mode=max 48 | 49 | - name: Build and push Backend Docker image 50 | uses: docker/build-push-action@v5 51 | with: 52 | context: . 53 | file: ./Dockerfile.backend 54 | platforms: linux/amd64,linux/arm64 55 | push: true 56 | tags: ghcr.io/${{ github.repository_owner }}/bhapi:latest 57 | cache-from: type=local,src=/tmp/.buildx-cache 58 | cache-to: type=local,dest=/tmp/.buildx-cache,mode=max 59 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "hi@338.rocks", 4 | "name": "Barış DEMİRCİ", 5 | "url": "https://338.rocks" 6 | }, 7 | "dependencies": { 8 | "@barbarbar338/bhapi": "^1.2.0", 9 | "@dotenvx/dotenvx": "^1.48.4", 10 | "@keyv/redis": "^5.1.0", 11 | "@nestjs/cache-manager": "^3.0.1", 12 | "@nestjs/common": "^11.1.6", 13 | "@nestjs/core": "^11.1.6", 14 | "@nestjs/platform-fastify": "^11.1.6", 15 | "cache-manager": "^7.1.1", 16 | "class-transformer": "^0.5.1", 17 | "class-validator": "^0.14.2", 18 | "envalid": "^8.1.0", 19 | "morgan": "^1.10.1", 20 | "nestjs-rate-limit": "^1.1.1", 21 | "reflect-metadata": "^0.2.2", 22 | "rimraf": "^6.0.1", 23 | "rxjs": "^7.8.2", 24 | "typeorm": "^0.3.25" 25 | }, 26 | "description": "An unofficial API server with no connection to Brawlhalla and its developers, prepared for easier and unlimited access to Brawlhalla API services.", 27 | "devDependencies": { 28 | "@eslint/js": "^9.33.0", 29 | "@nestjs/cli": "^11.0.10", 30 | "@nestjs/schematics": "^11.0.7", 31 | "@types/morgan": "^1.9.10", 32 | "@types/node": "^24.2.1", 33 | "@typescript-eslint/eslint-plugin": "^8.39.0", 34 | "@typescript-eslint/parser": "^8.39.0", 35 | "eslint": "^9.33.0", 36 | "eslint-config-prettier": "^10.1.8", 37 | "eslint-plugin-import": "^2.32.0", 38 | "eslint-plugin-prettier": "^5.5.4", 39 | "globals": "^16.3.0", 40 | "prettier": "^3.6.2", 41 | "taze": "^19.1.0", 42 | "ts-loader": "^9.5.2", 43 | "ts-node": "^10.9.2", 44 | "tsconfig-paths": "^4.2.0", 45 | "typescript": "^5.9.2", 46 | "typescript-eslint": "^8.39.0" 47 | }, 48 | "license": "MPL-2.0", 49 | "name": "bh-open-api", 50 | "private": true, 51 | "resolutions": { 52 | "**/string-width": "4.2.3" 53 | }, 54 | "scripts": { 55 | "build": "nest build", 56 | "dev": "dotenvx run -f .env.dev -- nest start --watch", 57 | "format": "prettier --write .", 58 | "lint": "eslint . --fix", 59 | "prebuild": "rimraf dist", 60 | "start": "dotenvx run -f .env.prod -- node dist/main", 61 | "update": "taze latest -w" 62 | }, 63 | "version": "3.0.1" 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/routers/utils/utils.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BHAPIError, 3 | Clan, 4 | getClanByID, 5 | getRankings, 6 | Ranking1v1, 7 | Ranking2v2, 8 | RankingSeasonal, 9 | } from "@barbarbar338/bhapi"; 10 | import { 11 | HttpStatus, 12 | Injectable, 13 | InternalServerErrorException, 14 | Logger, 15 | } from "@nestjs/common"; 16 | import { APIRes } from "api-types"; 17 | import { GetDataByClanIDDTO } from "src/dto/getDataByClanID.dto"; 18 | import { GetDataByRankingOptionsDTO } from "src/dto/getDataByRankingOptions.dto"; 19 | 20 | @Injectable() 21 | export class UtilsService { 22 | public returnPing(): APIRes { 23 | return { 24 | statusCode: HttpStatus.OK, 25 | message: "Pong!", 26 | data: null, 27 | }; 28 | } 29 | 30 | public async getRankedDataByRankingOptions( 31 | options: GetDataByRankingOptionsDTO, 32 | ): Promise> { 33 | try { 34 | const { data: rankingsData } = await getRankings(options); 35 | 36 | return { 37 | statusCode: HttpStatus.OK, 38 | message: `${options.region}/${options.type}/${options.page} from API`, 39 | data: rankingsData, 40 | }; 41 | } catch (error) { 42 | Logger.error(error, "UtilsService"); 43 | 44 | if (error instanceof BHAPIError) { 45 | if (error.status == 429) 46 | throw new InternalServerErrorException( 47 | `Rate limit exceeded. Please try again later.`, 48 | ); 49 | else 50 | throw new InternalServerErrorException( 51 | `Failed to get rankings for ${options.region}/${options.type}/${options.page}: ${error.message}`, 52 | ); 53 | } else 54 | throw new InternalServerErrorException( 55 | `Failed to get rankings for ${options.region}/${options.type}/${options.page}`, 56 | ); 57 | } 58 | } 59 | 60 | public async getDataByClanID({ 61 | clan_id, 62 | }: GetDataByClanIDDTO): Promise> { 63 | try { 64 | const { data: clanData } = await getClanByID(clan_id); 65 | 66 | return { 67 | statusCode: HttpStatus.OK, 68 | message: `${clanData.clan_name} from API`, 69 | data: clanData, 70 | }; 71 | } catch (error) { 72 | Logger.error(error, "UtilsService"); 73 | 74 | if (error instanceof BHAPIError) { 75 | if (error.status == 429) 76 | throw new InternalServerErrorException( 77 | `Rate limit exceeded. Please try again later.`, 78 | ); 79 | else 80 | throw new InternalServerErrorException( 81 | `Failed to get clan data for ${clan_id}: ${error.message}`, 82 | ); 83 | } else 84 | throw new InternalServerErrorException( 85 | `Failed to get clan data for ${clan_id}`, 86 | ); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/features.js: -------------------------------------------------------------------------------- 1 | const defaultBrawlhallaID = "brawlhalla_id=3145331"; 2 | const defaultSteamID = "steam_id=76561198320003276"; 3 | const defaultSteamURL = "steam_url=https://steamcommunity.com/id/barbarbar338/"; 4 | const defaultLegendID = "legend_id=3"; 5 | const defaultLegendName = "legend_name=cassidy"; 6 | const defaultRankedData = "region=eu&page=1"; 7 | const defaultClanID = "clan_id=960634"; 8 | 9 | const FEATURES = [ 10 | { 11 | title: "Get Glory By ID", 12 | query: `glory/id?${defaultBrawlhallaID}`, 13 | }, 14 | { 15 | title: "Get Glory By Steam ID", 16 | query: `glory/steamid?${defaultSteamID}`, 17 | }, 18 | { 19 | title: "Get Glory By Steam URL", 20 | query: `glory/steamurl?${defaultSteamURL}`, 21 | }, 22 | { 23 | title: "Get Ranked By ID", 24 | query: `ranked/id?${defaultBrawlhallaID}`, 25 | }, 26 | { 27 | title: "Get Ranked By Steam ID", 28 | query: `ranked/steamid?${defaultSteamID}`, 29 | }, 30 | { 31 | title: "Get Ranked By Steam URL", 32 | query: `ranked/steamurl?${defaultSteamURL}`, 33 | }, 34 | { 35 | title: "Get Stats By ID", 36 | query: `stats/id?${defaultBrawlhallaID}`, 37 | }, 38 | { 39 | title: "Get Stats By Steam ID", 40 | query: `ranked/steamid?${defaultSteamID}`, 41 | }, 42 | { 43 | title: "Get Stats By Steam URL", 44 | query: `ranked/steamurl?${defaultSteamURL}`, 45 | }, 46 | { 47 | title: "Get All Legends", 48 | query: `legends/all`, 49 | }, 50 | { 51 | title: "Get Legend By ID", 52 | query: `legends/id?${defaultLegendID}`, 53 | }, 54 | { 55 | title: "Get Legend By Name", 56 | query: `legends/name?${defaultLegendName}`, 57 | }, 58 | { 59 | title: "Get Steam Data By Steam ID", 60 | query: `steamdata/id?${defaultSteamID}`, 61 | path: "/get-steam-data-by-steam-id", 62 | }, 63 | { 64 | title: "Get Steam Data By Steam URL", 65 | query: `steamdata/url?${defaultSteamURL}`, 66 | }, 67 | { 68 | title: "Get Ranked 1v1 Data", 69 | query: `utils/rankings?${defaultRankedData}&type=1v1`, 70 | }, 71 | { 72 | title: "Get Ranked 2v2 Data", 73 | query: `utils/rankings?${defaultRankedData}&type=2v2`, 74 | }, 75 | { 76 | title: "Get Ranked Seasonal Data", 77 | query: `utils/rankings?${defaultRankedData}&type=seasonal`, 78 | }, 79 | { 80 | title: "Get Clan Data", 81 | query: `utils/clan?${defaultClanID}`, 82 | }, 83 | ]; 84 | 85 | export default FEATURES; 86 | -------------------------------------------------------------------------------- /backend/src/routers/steamdata/steamdata.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BHAPIError, 3 | BHIDFromSteamID, 4 | getBHIDFromSteamID, 5 | getSteamDataBySteamID, 6 | getSteamDataByURL, 7 | SteamData, 8 | } from "@barbarbar338/bhapi"; 9 | import { 10 | HttpStatus, 11 | Injectable, 12 | InternalServerErrorException, 13 | Logger, 14 | } from "@nestjs/common"; 15 | import { APIRes } from "api-types"; 16 | import { GetDataBySteamIDDTO } from "src/dto/getDataBySteamID.dto"; 17 | import { GetDataBySteamURLDTO } from "src/dto/getDataBySteamURL.dto"; 18 | 19 | @Injectable() 20 | export class SteamDataService { 21 | public returnPing(): APIRes { 22 | return { 23 | statusCode: HttpStatus.OK, 24 | message: "Pong!", 25 | data: null, 26 | }; 27 | } 28 | 29 | public async getSteamDataByID({ 30 | steam_id, 31 | }: GetDataBySteamIDDTO): Promise> { 32 | try { 33 | const steamData = await getSteamDataBySteamID(steam_id); 34 | const brawlhalla_id = await getBHIDFromSteamID(steam_id); 35 | 36 | return { 37 | statusCode: HttpStatus.OK, 38 | message: `${steamData.name} from API`, 39 | data: { ...steamData, brawlhalla_id }, 40 | }; 41 | } catch (error) { 42 | Logger.error(error, "SteamDataService"); 43 | 44 | if (error instanceof BHAPIError) { 45 | if (error.status == 429) 46 | throw new InternalServerErrorException( 47 | `Rate limit exceeded. Please try again later.`, 48 | ); 49 | else 50 | throw new InternalServerErrorException( 51 | `Failed to get Steam data for Steam ID ${steam_id}: ${error.message}`, 52 | ); 53 | } else 54 | throw new InternalServerErrorException( 55 | `Failed to get Steam data for Steam ID ${steam_id}`, 56 | ); 57 | } 58 | } 59 | 60 | public async getSteamDataByURL({ 61 | steam_url, 62 | }: GetDataBySteamURLDTO): Promise> { 63 | try { 64 | const steamData = await getSteamDataByURL(steam_url); 65 | const brawlhalla_id = await getBHIDFromSteamID(steamData.steam_id); 66 | 67 | return { 68 | statusCode: HttpStatus.OK, 69 | message: `${steamData.name} from API`, 70 | data: { ...steamData, brawlhalla_id }, 71 | }; 72 | } catch (error) { 73 | Logger.error(error, "SteamDataService"); 74 | 75 | if (error instanceof BHAPIError) { 76 | if (error.status == 429) 77 | throw new InternalServerErrorException( 78 | `Rate limit exceeded. Please try again later.`, 79 | ); 80 | else 81 | throw new InternalServerErrorException( 82 | `Failed to get Steam data for Steam URL ${steam_url}: ${error.message}`, 83 | ); 84 | } else 85 | throw new InternalServerErrorException( 86 | `Failed to get Steam data for Steam URL ${steam_url}`, 87 | ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /frontend/src/components/Features/FeatureItem.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { toast } from "react-toastify"; 3 | import config from "../../config"; 4 | 5 | export default class FeatureItem extends Component { 6 | state = { 7 | query: this.props.query, 8 | response: JSON.stringify( 9 | { 10 | foo: "bar", 11 | }, 12 | null, 13 | 4, 14 | ), 15 | }; 16 | handleQueryChange = (e) => { 17 | this.setState({ 18 | query: e.target.value, 19 | }); 20 | }; 21 | handleButtonClick = async () => { 22 | if (!this.state.query || this.state.query.length < 1) 23 | return this.createErrorToast("Specify a request query"); 24 | 25 | const apiURL = `${window.location.protocol}//${window.location.host}/${config.apiVersion}/`; 26 | const request = await fetch(`${apiURL}${this.state.query}`); 27 | const response = await request.json(); 28 | if (!request.ok) return this.createErrorToast(response.message); 29 | this.setState({ 30 | response: JSON.stringify(response, null, 4), 31 | }); 32 | this.createSuccessToast(this.state.query); 33 | }; 34 | createErrorToast(str) { 35 | return toast.error(`⛔ ${str}`, { 36 | position: "bottom-right", 37 | autoClose: 5000, 38 | hideProgressBar: false, 39 | closeOnClick: true, 40 | pauseOnHover: true, 41 | draggable: true, 42 | progress: undefined, 43 | }); 44 | } 45 | createSuccessToast(str) { 46 | return toast.success(`✅ ${str}`, { 47 | position: "bottom-right", 48 | autoClose: 5000, 49 | hideProgressBar: false, 50 | closeOnClick: true, 51 | pauseOnHover: true, 52 | draggable: true, 53 | progress: undefined, 54 | }); 55 | } 56 | render() { 57 | return ( 58 |
59 |
60 |

{this.props.title}

61 |
62 |
63 | 64 | {config.apiVersion}/ 65 | 66 |
67 | 74 |
75 | 81 |
82 |
83 |
84 |
85 |

Response

86 |