Compare commits
15
Commits
d575a4efc5
..
v1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7aa9921b | ||
|
|
964415198a | ||
|
|
0adf0bd51f | ||
|
|
39f1c88255 | ||
|
|
4fe5aa25f8 | ||
|
|
a5ceb721f3 | ||
|
|
38e6d21585 | ||
|
|
689610b434 | ||
|
|
748616df9d | ||
|
|
a9b9fe1047 | ||
|
|
20c1fd7be9 | ||
|
|
2800d2fb2e | ||
|
|
cce705c4f7 | ||
|
|
ed889d95e6 | ||
|
|
c61d0c40af |
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
Makefile
|
||||||
|
helm-charts
|
||||||
|
.env
|
||||||
|
.editorconfig
|
||||||
|
.idea
|
||||||
|
coverage*
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Overrides the default run name (the tagged commit's message) so the Actions
|
||||||
|
# runs list shows "Release v0.1.0" instead.
|
||||||
|
run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}"
|
||||||
|
|
||||||
|
# Publishes the games-api container image to the Gitea container registry AND
|
||||||
|
# creates a Gitea Release whose body is the annotated tag's message (generated
|
||||||
|
# locally by `make release VERSION=...` via git-cliff). Triggered by a version
|
||||||
|
# tag push, or manually via workflow_dispatch with a tag. Pushing to main does
|
||||||
|
# nothing, so work-in-progress commits never ship. Each release publishes two
|
||||||
|
# image tags: the version (e.g. v0.1.0) and "latest".
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag to release (e.g. v0.1.0)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
env:
|
||||||
|
# Resolve the target tag for both triggers: explicit input on manual
|
||||||
|
# dispatch, otherwise the pushed tag ref.
|
||||||
|
TAG: ${{ gitea.event.inputs.tag || gitea.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Full history ensures the tag annotation (the release notes) is present.
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ gitea.event.inputs.tag || gitea.ref }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.linuxhg.com
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
# PAT stored as a repo Actions secret (auto GITHUB_TOKEN lacks package scope in Gitea)
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
# Publishes both the exact version (e.g. v0.1.0) and the movable "latest" tag.
|
||||||
|
# Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml;
|
||||||
|
# pin or roll back by setting IMAGE_TAG in .env.
|
||||||
|
tags: |
|
||||||
|
git.linuxhg.com/games-database/games-api:${{ env.TAG }}
|
||||||
|
git.linuxhg.com/games-database/games-api:latest
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
env:
|
||||||
|
# REGISTRY_TOKEN is reused for release creation because Gitea's auto
|
||||||
|
# GITHUB_TOKEN cannot create releases on this instance. The PAT must
|
||||||
|
# carry write:repository scope. Idempotent: re-runs update an existing
|
||||||
|
# release for this tag instead of failing with 409. On any HTTP error
|
||||||
|
# the API response body is printed so a 403 names the missing scope.
|
||||||
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
REPO: ${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
: "${TAG:?TAG is required}"
|
||||||
|
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
|
||||||
|
AUTH="Authorization: token ${TOKEN}"
|
||||||
|
# Release body = the annotated tag's message (the git-cliff notes).
|
||||||
|
BODY="$(git tag -l --format='%(contents)' "${TAG}")"
|
||||||
|
|
||||||
|
# Tags containing a '-' (e.g. v0.1.0-rc1) are published as pre-releases.
|
||||||
|
PRE="false"; case "${TAG}" in *-*) PRE="true";; esac
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg t "${TAG}" --arg n "${TAG}" --arg b "${BODY}" --argjson p "${PRE}" \
|
||||||
|
'{tag_name:$t, name:$n, body:$b, draft:false, prerelease:$p}')
|
||||||
|
|
||||||
|
# POST/PATCH the release, surfacing Gitea's error message on failure
|
||||||
|
# (e.g. "token does not have write scope") instead of failing silently.
|
||||||
|
api_call() {
|
||||||
|
local method="$1" url="$2" resp code rbody
|
||||||
|
resp="$(curl -sS -w '\n%{http_code}' -X "${method}" \
|
||||||
|
-H "${AUTH}" -H "Content-Type: application/json" \
|
||||||
|
-d "${PAYLOAD}" "${url}")"
|
||||||
|
code="$(printf '%s' "${resp}" | tail -n1)"
|
||||||
|
rbody="$(printf '%s' "${resp}" | sed '$d')"
|
||||||
|
if [ "${code}" -ge 400 ]; then
|
||||||
|
echo "::error::Release API ${code} (${method} ${url}): ${rbody}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
EXISTING_ID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null || true)"
|
||||||
|
if [ -n "${EXISTING_ID}" ]; then
|
||||||
|
api_call PATCH "${API}/${EXISTING_ID}"
|
||||||
|
echo "Updated existing release id=${EXISTING_ID} for ${TAG}"
|
||||||
|
else
|
||||||
|
api_call POST "${API}"
|
||||||
|
echo "Created new release for ${TAG}"
|
||||||
|
fi
|
||||||
@@ -130,3 +130,5 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# docker-compose.yml
|
||||||
|
docker-compose.yml
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# use the official Bun image
|
||||||
|
# see all versions at https://hub.docker.com/r/oven/bun/tags
|
||||||
|
FROM oven/bun:1 AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# curl is needed for container healthchecks (curl -f http://localhost:5000/health)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# install dependencies into temp directory
|
||||||
|
# this will cache them and speed up future builds
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lockb /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# install with --production (exclude devDependencies)
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lockb /temp/prod/
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
# copy node_modules from temp directory
|
||||||
|
# then copy all (non-ignored) project files into the image
|
||||||
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# copy production dependencies and source code into final image
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/ .
|
||||||
|
COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
|
||||||
|
# Runtime config comes from the environment (compose env_file: .env),
|
||||||
|
# not baked in at build time, so one image runs anywhere.
|
||||||
|
ARG NODE_ENV=production
|
||||||
|
ENV NODE_ENV=$NODE_ENV
|
||||||
|
|
||||||
|
# run the app
|
||||||
|
USER bun
|
||||||
|
EXPOSE 5000/tcp
|
||||||
|
ENTRYPOINT [ "bun", "./bin/www.js" ]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.PHONY: help release
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo "Release:"
|
||||||
|
@echo " ./release v0.1.0 - Tag, push, and release (notes auto-generated from commits)"
|
||||||
|
@echo " make release VERSION=v0.1.0 - Same, explicit make form"
|
||||||
|
|
||||||
|
# Create an annotated version tag carrying auto-generated release notes (git-cliff)
|
||||||
|
# and push it. The tag push triggers .gitea/workflows/release.yml, which builds the
|
||||||
|
# image and publishes a Gitea Release whose body is this tag's message. Notes come
|
||||||
|
# entirely from Conventional Commits — no hand-written message required.
|
||||||
|
#
|
||||||
|
# git-cliff's --latest needs the tag to exist to scope the notes, so we create a
|
||||||
|
# throwaway lightweight tag, generate the notes, replace it with an annotated tag,
|
||||||
|
# then push. --cleanup=verbatim keeps the markdown "###" group headers (git's
|
||||||
|
# default cleanup would strip lines starting with "#").
|
||||||
|
#
|
||||||
|
# Requires git-cliff: https://git-cliff.org/install
|
||||||
|
# Usage: make release VERSION=v0.1.0
|
||||||
|
release:
|
||||||
|
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=v0.1.0"; exit 1; }
|
||||||
|
@command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; }
|
||||||
|
@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi
|
||||||
|
@echo "Generating release notes for $(VERSION)..."
|
||||||
|
@git tag "$(VERSION)" HEAD && \
|
||||||
|
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
|
||||||
|
{ git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; }
|
||||||
|
@git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp
|
||||||
|
@git push origin "$(VERSION)"
|
||||||
|
@echo "Pushed $(VERSION) — Gitea Actions will build the image and publish the Release."
|
||||||
@@ -6,10 +6,10 @@ import helmet from 'helmet'
|
|||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import mongoSanitize from 'express-mongo-sanitize'
|
import mongoSanitize from 'express-mongo-sanitize'
|
||||||
import xss from 'xss-clean'
|
import xss from 'xss-clean'
|
||||||
import rateLimit from 'express-rate-limit'
|
|
||||||
import hpp from 'hpp'
|
import hpp from 'hpp'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
import errorHandler from './middleware/error.js'
|
import errorHandler from './middleware/error.js'
|
||||||
|
import { apiLimiter } from './middleware/rateLimit.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -36,12 +36,9 @@ const corsOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const limiter = rateLimit({
|
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), apiLimiter, hpp(), cors())
|
||||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
||||||
max: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), limiter, hpp(), cors(corsOptions))
|
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }))
|
||||||
|
|
||||||
app.use('/api/admin/games', adminGames)
|
app.use('/api/admin/games', adminGames)
|
||||||
app.use('/api/games', games)
|
app.use('/api/games', games)
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# git-cliff configuration — generates the body of each Gitea Release from
|
||||||
|
# Conventional Commits accumulated since the previous tag. Used locally by
|
||||||
|
# `make release` with --latest so only the current tag's section is emitted
|
||||||
|
# (no full history, no header — the Gitea Release title is the tag).
|
||||||
|
# Docs: https://git-cliff.org/docs/configuration
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
header = ""
|
||||||
|
body = """
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}\
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}\
|
||||||
|
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
|
||||||
|
{% endfor %}\
|
||||||
|
{% endfor %}\
|
||||||
|
"""
|
||||||
|
trim = true
|
||||||
|
footer = ""
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = false
|
||||||
|
require_conventional = false
|
||||||
|
split_commits = false
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Features" },
|
||||||
|
{ message = "^fix", group = "Bug Fixes" },
|
||||||
|
{ message = "^perf", group = "Performance" },
|
||||||
|
{ message = "^refactor", group = "Refactor" },
|
||||||
|
{ message = "^docs", group = "Documentation" },
|
||||||
|
{ message = "^test", group = "Tests" },
|
||||||
|
{ message = "^chore|^ci", group = "Miscellaneous Tasks" },
|
||||||
|
{ message = ".*", group = "Other" },
|
||||||
|
]
|
||||||
|
filter_commits = false
|
||||||
|
tag_pattern = "v[0-9].*"
|
||||||
|
sort_commits = "oldest"
|
||||||
@@ -2,6 +2,7 @@ import Game from '../models/Game.js'
|
|||||||
import steamScraper from '../scripts/scraper.js'
|
import steamScraper from '../scripts/scraper.js'
|
||||||
import asyncHandler from '../middleware/async.js'
|
import asyncHandler from '../middleware/async.js'
|
||||||
import ErrorResponse from '../utils/errorResponse.js'
|
import ErrorResponse from '../utils/errorResponse.js'
|
||||||
|
import normalizeSteamId from '../utils/normalizeSteamId.js'
|
||||||
|
|
||||||
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
|
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
|
||||||
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
|
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
|
||||||
@@ -41,8 +42,17 @@ export const show = asyncHandler(async (req, res, next) => {
|
|||||||
*/
|
*/
|
||||||
export const create = asyncHandler(async (req, res, next) => {
|
export const create = asyncHandler(async (req, res, next) => {
|
||||||
let oldGame
|
let oldGame
|
||||||
req.body.steamId.length > 0
|
const steamId = normalizeSteamId(req.body.steamId)
|
||||||
? (oldGame = await Game.findOne({ steamId: req.body.steamId }))
|
if (steamId === null)
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Enter a Steam app ID or a store.steampowered.com URL.',
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
req.body.steamId = steamId
|
||||||
|
steamId.length > 0
|
||||||
|
? (oldGame = await Game.findOne({ steamId }))
|
||||||
: (oldGame = await Game.findOne({ title: req.body.title }))
|
: (oldGame = await Game.findOne({ title: req.body.title }))
|
||||||
|
|
||||||
if (oldGame)
|
if (oldGame)
|
||||||
@@ -56,6 +66,14 @@ export const create = asyncHandler(async (req, res, next) => {
|
|||||||
const data =
|
const data =
|
||||||
req.body.scrape === true ? await steamScraper(req.body) : req.body
|
req.body.scrape === true ? await steamScraper(req.body) : req.body
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.body.scrape === true &&
|
||||||
|
(!data || typeof data !== 'object' || !data.title || !data.frontImage)
|
||||||
|
)
|
||||||
|
return next(
|
||||||
|
new ErrorResponse('Steam lookup failed for that ID/URL.', 400)
|
||||||
|
)
|
||||||
|
|
||||||
if (req.body.scrape === false) {
|
if (req.body.scrape === false) {
|
||||||
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
||||||
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
||||||
@@ -96,6 +114,17 @@ export const update = asyncHandler(async (req, res, next) => {
|
|||||||
|
|
||||||
|
|
||||||
req.body.lastModifiedBy = req.user.id
|
req.body.lastModifiedBy = req.user.id
|
||||||
|
if (req.body.steamId !== undefined) {
|
||||||
|
const normalizedSteamId = normalizeSteamId(req.body.steamId)
|
||||||
|
if (normalizedSteamId === null)
|
||||||
|
return next(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Enter a Steam app ID or a store.steampowered.com URL.',
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
req.body.steamId = normalizedSteamId
|
||||||
|
}
|
||||||
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
||||||
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
||||||
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
|
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
|
||||||
@@ -109,6 +138,14 @@ export const update = asyncHandler(async (req, res, next) => {
|
|||||||
const data =
|
const data =
|
||||||
req.body.scrape === true ? await steamScraper(req.body) : req.body
|
req.body.scrape === true ? await steamScraper(req.body) : req.body
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.body.scrape === true &&
|
||||||
|
(!data || typeof data !== 'object' || !data.title || !data.frontImage)
|
||||||
|
)
|
||||||
|
return next(
|
||||||
|
new ErrorResponse('Steam lookup failed for that ID/URL.', 400)
|
||||||
|
)
|
||||||
|
|
||||||
game = await Game.findOneAndUpdate({ [gameId]: id }, data, {
|
game = await Game.findOneAndUpdate({ [gameId]: id }, data, {
|
||||||
new: true,
|
new: true,
|
||||||
runValidators: true,
|
runValidators: true,
|
||||||
|
|||||||
+162
-88
@@ -1,53 +1,53 @@
|
|||||||
// noinspection DuplicatedCode
|
// noinspection DuplicatedCode
|
||||||
|
|
||||||
import Game from '../models/Game.js'
|
import Game from "../models/Game.js";
|
||||||
import steamScraper from '../scripts/scraper.js'
|
import steamScraper from "../scripts/scraper.js";
|
||||||
import asyncHandler from '../middleware/async.js'
|
import asyncHandler from "../middleware/async.js";
|
||||||
import ErrorResponse from '../utils/errorResponse.js'
|
import ErrorResponse from "../utils/errorResponse.js";
|
||||||
|
import normalizeSteamId from "../utils/normalizeSteamId.js";
|
||||||
import { decode, isValid } from "js-base64";
|
import { decode, isValid } from "js-base64";
|
||||||
|
|
||||||
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
|
const checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
|
||||||
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
|
const checkForTwelveRegExp = new RegExp("^[0-9a-fA-F]{12}$");
|
||||||
/**
|
/**
|
||||||
* games.js
|
* games.js
|
||||||
*
|
*
|
||||||
* @description :: Server-side logic for managing games.
|
* @description :: Server-side logic for managing games.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gameController.list()
|
* gameController.list()
|
||||||
*/
|
*/
|
||||||
export const list = asyncHandler(async (req, res, next) => {
|
export const list = asyncHandler(async (req, res, next) => {
|
||||||
const data = res.advancedResults.data
|
const data = res.advancedResults.data;
|
||||||
if (data[0]?.accessedBy) {
|
if (data[0]?.accessedBy) {
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
for (let x = 0; x < data[i].accessedBy.length; x++) {
|
for (let x = 0; x < data[i].accessedBy.length; x++) {
|
||||||
if (data[i].accessedBy[x].user.toString() !== req.user.id)
|
if (data[i].accessedBy[x].user.toString() !== req.user.id)
|
||||||
data[i].accessedBy.splice(x, 1)
|
data[i].accessedBy.splice(x, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.status(200).json(res.advancedResults)
|
return res.status(200).json(res.advancedResults);
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gameController.show()
|
* gameController.show()
|
||||||
*/
|
*/
|
||||||
export const show = asyncHandler(async (req, res, next) => {
|
export const show = asyncHandler(async (req, res, next) => {
|
||||||
const {id} = req.params
|
const { id } = req.params;
|
||||||
|
|
||||||
const gameId =
|
const gameId =
|
||||||
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
||||||
? '_id'
|
? "_id"
|
||||||
: 'steamId'
|
: "steamId";
|
||||||
|
|
||||||
const game = await Game.findOne({[gameId]: id})
|
const game = await Game.findOne({ [gameId]: id });
|
||||||
|
|
||||||
if (!game)
|
if (!game)
|
||||||
return next(
|
return next(
|
||||||
new ErrorResponse(`Game not found with id of ${req.params.id}`, 404),
|
new ErrorResponse(`Game not found with id of ${req.params.id}`, 404),
|
||||||
)
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!game.accessedBy
|
!game.accessedBy
|
||||||
@@ -59,54 +59,94 @@ export const show = asyncHandler(async (req, res, next) => {
|
|||||||
`You do not have permission to access Game ID ${req.params.id}`,
|
`You do not have permission to access Game ID ${req.params.id}`,
|
||||||
401,
|
401,
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
const userGame = await Game.findOne({ [gameId]: id }).select(
|
const userGame = await Game.findOne({ [gameId]: id }).select(
|
||||||
'-createdBy -__v',
|
"-createdBy -__v",
|
||||||
)
|
);
|
||||||
|
|
||||||
for (let i = 0; i < userGame.accessedBy.length; i++) {
|
for (let i = 0; i < userGame.accessedBy.length; i++) {
|
||||||
if (userGame.accessedBy[i].user.toString() !== req.user.id)
|
if (userGame.accessedBy[i].user.toString() !== req.user.id)
|
||||||
userGame.accessedBy.splice(i, 1)
|
userGame.accessedBy.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({success: true, data: userGame})
|
res.status(200).json({ success: true, data: userGame });
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gameController.create()
|
* gameController.create()
|
||||||
*/
|
*/
|
||||||
export const create = asyncHandler(async (req, res, next) => {
|
export const create = asyncHandler(async (req, res, next) => {
|
||||||
let oldGame
|
let oldGame;
|
||||||
|
|
||||||
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
const steamId = normalizeSteamId(req.body.steamId);
|
||||||
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
if (steamId === null)
|
||||||
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
|
return next(
|
||||||
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
|
new ErrorResponse(
|
||||||
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
|
"Enter a Steam app ID or a store.steampowered.com URL.",
|
||||||
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
|
400,
|
||||||
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
|
),
|
||||||
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
|
);
|
||||||
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
|
req.body.steamId = steamId;
|
||||||
|
|
||||||
req.body.steamId.length > 0
|
if (req.body.shortDesc && isValid(req.body.shortDesc))
|
||||||
|
req.body.shortDesc = decode(req.body.shortDesc);
|
||||||
|
if (req.body.reviews && isValid(req.body.reviews))
|
||||||
|
req.body.reviews = decode(req.body.reviews);
|
||||||
|
if (req.body.summary && isValid(req.body.summary))
|
||||||
|
req.body.summary = decode(req.body.summary);
|
||||||
|
isValid(req.body.systemRequirements?.windows?.minimum)
|
||||||
|
? (req.body.systemRequirements.windows.minimum = decode(
|
||||||
|
req.body.systemRequirements.windows.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.windows?.recommended)
|
||||||
|
? (req.body.systemRequirements.windows.recommended = decode(
|
||||||
|
req.body.systemRequirements.windows.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.mac?.minimum)
|
||||||
|
? (req.body.systemRequirements.mac.minimum = decode(
|
||||||
|
req.body.systemRequirements.mac.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.mac?.recommended)
|
||||||
|
? (req.body.systemRequirements.mac.recommended = decode(
|
||||||
|
req.body.systemRequirements.mac.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.linux?.minimum)
|
||||||
|
? (req.body.systemRequirements.linux.minimum = decode(
|
||||||
|
req.body.systemRequirements.linux.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.linux?.recommended)
|
||||||
|
? (req.body.systemRequirements.linux.recommended = decode(
|
||||||
|
req.body.systemRequirements.linux.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
|
||||||
|
steamId.length > 0
|
||||||
? (oldGame = await Game.findOne({
|
? (oldGame = await Game.findOne({
|
||||||
steamId: req.body.steamId,
|
steamId: steamId,
|
||||||
'accessedBy.user': req.user.id,
|
"accessedBy.user": req.user.id,
|
||||||
}))
|
}))
|
||||||
: (oldGame = await Game.findOne({
|
: (oldGame = await Game.findOne({
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
'accessedBy.user': req.user.id,
|
"accessedBy.user": req.user.id,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
if (oldGame)
|
if (oldGame)
|
||||||
return next(
|
return next(
|
||||||
new ErrorResponse(`The game ${oldGame.title} already exists in users account.`, 400)
|
new ErrorResponse(
|
||||||
)
|
`The game ${oldGame.title} already exists in users account.`,
|
||||||
|
400,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
req.body.steamId.length > 0
|
steamId.length > 0
|
||||||
? (oldGame = await Game.findOne({steamId: req.body.steamId}))
|
? (oldGame = await Game.findOne({ steamId: steamId }))
|
||||||
: (oldGame = await Game.findOne({title: req.body.title}))
|
: (oldGame = await Game.findOne({ title: req.body.title }));
|
||||||
|
|
||||||
if (oldGame) {
|
if (oldGame) {
|
||||||
oldGame.accessedBy.push({
|
oldGame.accessedBy.push({
|
||||||
@@ -115,64 +155,99 @@ export const create = asyncHandler(async (req, res, next) => {
|
|||||||
playStatus: req.body.accessedBy[0].playStatus,
|
playStatus: req.body.accessedBy[0].playStatus,
|
||||||
soundtrack: req.body.accessedBy[0].soundtrack,
|
soundtrack: req.body.accessedBy[0].soundtrack,
|
||||||
rating: req.body.accessedBy[0].rating,
|
rating: req.body.accessedBy[0].rating,
|
||||||
})
|
});
|
||||||
|
|
||||||
oldGame.lastModifiedBy = req.user.id
|
oldGame.lastModifiedBy = req.user.id;
|
||||||
|
|
||||||
await oldGame.save()
|
await oldGame.save();
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: oldGame,
|
data: oldGame,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
req.body.createdBy = req.user.id
|
req.body.createdBy = req.user.id;
|
||||||
req.body.lastModifiedBy = req.user.id
|
req.body.lastModifiedBy = req.user.id;
|
||||||
req.body.accessedBy[0].user = req.user.id
|
req.body.accessedBy[0].user = req.user.id;
|
||||||
const data =
|
const data =
|
||||||
req.body.scrape === true ? await steamScraper(req.body) : req.body
|
req.body.scrape === true ? await steamScraper(req.body) : req.body;
|
||||||
|
|
||||||
const game = await Game.create(data)
|
if (
|
||||||
|
req.body.scrape === true &&
|
||||||
|
(!data || typeof data !== "object" || !data.title || !data.frontImage)
|
||||||
|
)
|
||||||
|
return next(
|
||||||
|
new ErrorResponse("Steam lookup failed for that ID/URL.", 400),
|
||||||
|
);
|
||||||
|
|
||||||
|
const game = await Game.create(data);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: game,
|
data: game,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gameController.update()
|
* gameController.update()
|
||||||
*/
|
*/
|
||||||
export const update = asyncHandler(async (req, res, next) => {
|
export const update = asyncHandler(async (req, res, next) => {
|
||||||
const {id} = req.params
|
const { id } = req.params;
|
||||||
|
|
||||||
const gameId =
|
const gameId =
|
||||||
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
||||||
? '_id'
|
? "_id"
|
||||||
: 'steamId'
|
: "steamId";
|
||||||
|
|
||||||
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
|
if (req.body.shortDesc && isValid(req.body.shortDesc))
|
||||||
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
|
req.body.shortDesc = decode(req.body.shortDesc);
|
||||||
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
|
if (req.body.reviews && isValid(req.body.reviews))
|
||||||
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
|
req.body.reviews = decode(req.body.reviews);
|
||||||
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
|
if (req.body.summary && isValid(req.body.summary))
|
||||||
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
|
req.body.summary = decode(req.body.summary);
|
||||||
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
|
isValid(req.body.systemRequirements?.windows?.minimum)
|
||||||
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
|
? (req.body.systemRequirements.windows.minimum = decode(
|
||||||
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
|
req.body.systemRequirements.windows.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.windows?.recommended)
|
||||||
|
? (req.body.systemRequirements.windows.recommended = decode(
|
||||||
|
req.body.systemRequirements.windows.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.mac?.minimum)
|
||||||
|
? (req.body.systemRequirements.mac.minimum = decode(
|
||||||
|
req.body.systemRequirements.mac.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.mac?.recommended)
|
||||||
|
? (req.body.systemRequirements.mac.recommended = decode(
|
||||||
|
req.body.systemRequirements.mac.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.linux?.minimum)
|
||||||
|
? (req.body.systemRequirements.linux.minimum = decode(
|
||||||
|
req.body.systemRequirements.linux.minimum,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
isValid(req.body.systemRequirements?.linux?.recommended)
|
||||||
|
? (req.body.systemRequirements.linux.recommended = decode(
|
||||||
|
req.body.systemRequirements.linux.recommended,
|
||||||
|
))
|
||||||
|
: "";
|
||||||
|
|
||||||
let game = await Game.findOne({
|
let game = await Game.findOne({
|
||||||
[gameId]: id,
|
[gameId]: id,
|
||||||
'accessedBy.user': req.user.id,
|
"accessedBy.user": req.user.id,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!game)
|
if (!game)
|
||||||
return next(
|
return next(
|
||||||
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
|
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
|
||||||
)
|
);
|
||||||
|
|
||||||
game = await Game.findOneAndUpdate(
|
game = await Game.findOneAndUpdate(
|
||||||
{[gameId]: id, 'accessedBy.user': req.user.id},
|
{ [gameId]: id, "accessedBy.user": req.user.id },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
series: req.body.series,
|
series: req.body.series,
|
||||||
@@ -180,59 +255,58 @@ export const update = asyncHandler(async (req, res, next) => {
|
|||||||
genre: req.body.genre,
|
genre: req.body.genre,
|
||||||
wine: req.body.wine,
|
wine: req.body.wine,
|
||||||
lastModifiedBy: req.user.id,
|
lastModifiedBy: req.user.id,
|
||||||
'accessedBy.$.store': req.body.accessedBy[0].store,
|
"accessedBy.$.store": req.body.accessedBy[0].store,
|
||||||
'accessedBy.$.playStatus': req.body.accessedBy[0].playStatus,
|
"accessedBy.$.playStatus": req.body.accessedBy[0].playStatus,
|
||||||
'accessedBy.$.soundtrack': req.body.accessedBy[0].soundtrack,
|
"accessedBy.$.soundtrack": req.body.accessedBy[0].soundtrack,
|
||||||
'accessedBy.$.rating': req.body.accessedBy[0].rating,
|
"accessedBy.$.rating": req.body.accessedBy[0].rating,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ new: true, runValidators: true },
|
{ new: true, runValidators: true },
|
||||||
)
|
);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: game,
|
data: game,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gameController.remove()
|
* gameController.remove()
|
||||||
*/
|
*/
|
||||||
export const remove = asyncHandler(async (req, res, next) => {
|
export const remove = asyncHandler(async (req, res, next) => {
|
||||||
const {id} = req.params
|
const { id } = req.params;
|
||||||
|
|
||||||
const gameId =
|
const gameId =
|
||||||
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
|
||||||
? '_id'
|
? "_id"
|
||||||
: 'steamId'
|
: "steamId";
|
||||||
|
|
||||||
let game = await Game.findOne({
|
let game = await Game.findOne({
|
||||||
[gameId]: id,
|
[gameId]: id,
|
||||||
'accessedBy.user': req.user.id,
|
"accessedBy.user": req.user.id,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!game)
|
if (!game)
|
||||||
return next(
|
return next(
|
||||||
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
|
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
|
||||||
)
|
);
|
||||||
|
|
||||||
if (game.accessedBy.length > 1) {
|
if (game.accessedBy.length > 1) {
|
||||||
await Game.findOneAndUpdate(
|
await Game.findOneAndUpdate(
|
||||||
{[gameId]: id, 'accessedBy.user': req.user.id},
|
{ [gameId]: id, "accessedBy.user": req.user.id },
|
||||||
{
|
{
|
||||||
$pull: {
|
$pull: {
|
||||||
accessedBy: { user: req.user.id },
|
accessedBy: { user: req.user.id },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ new: true, runValidators: true },
|
{ new: true, runValidators: true },
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
await Game.findOneAndDelete({[gameId]: id})
|
await Game.findOneAndDelete({ [gameId]: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {},
|
data: {},
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
# Prod run template. Copy to docker-compose.yml next to the server .env:
|
||||||
|
# cp docker-compose.example.yml docker-compose.yml
|
||||||
|
# Then: docker compose pull && docker compose up -d
|
||||||
|
#
|
||||||
|
# Required keys in .env (singular JWT_EXPIRE, as read by models/User.js):
|
||||||
|
# ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, JWT_EXPIRE,
|
||||||
|
# MONGO_URI, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD,
|
||||||
|
# FROM_EMAIL, FROM_NAME, SECURE, NODE_ENV
|
||||||
|
# Alternatively, comment out env_file above and uncomment the
|
||||||
|
# environment list below to keep values directly in this file.
|
||||||
|
# environment:
|
||||||
|
# - ACCESS_TOKEN_SECRET=
|
||||||
|
# - REFRESH_TOKEN_SECRET=
|
||||||
|
# - JWT_EXPIRE=
|
||||||
|
# - MONGO_URI=
|
||||||
|
# - SMTP_HOST=
|
||||||
|
# - SMTP_PORT=
|
||||||
|
# - SMTP_USER=
|
||||||
|
# - SMTP_PASSWORD=
|
||||||
|
# - FROM_EMAIL=
|
||||||
|
# - FROM_NAME=
|
||||||
|
# - SECURE=false
|
||||||
|
# - NODE_ENV=
|
||||||
|
services:
|
||||||
|
games-api:
|
||||||
|
image: git.linuxhg.com/games-database/games-api:${IMAGE_TAG:-latest}
|
||||||
|
container_name: games-api
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:5000/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
@@ -162,7 +162,7 @@ const advancedResults = (model, populate) => async (req, res, next) => {
|
|||||||
const sortBy = req.query.sort.split(',').join(' ')
|
const sortBy = req.query.sort.split(',').join(' ')
|
||||||
query = query.sort(sortBy)
|
query = query.sort(sortBy)
|
||||||
} else {
|
} else {
|
||||||
query = query.sort('series title')
|
query = query.sort('-createDate series title')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
// Public auth endpoints get their own brute-force budget (see authLimiter)
|
||||||
|
// and must not consume the shared API budget, otherwise a bad-token storm
|
||||||
|
// can lock the owner out of logging back in.
|
||||||
|
const PUBLIC_AUTH_PATHS = [
|
||||||
|
'/api/auth/login',
|
||||||
|
'/api/auth/register',
|
||||||
|
'/api/auth/forgotpassword',
|
||||||
|
'/api/auth/resetpassword',
|
||||||
|
]
|
||||||
|
|
||||||
|
// Shared budget for real API traffic. Counts failures too (cheap 401s, no DB
|
||||||
|
// hit), but skips CORS preflights, the health probe, and the public auth
|
||||||
|
// endpoints above so junk traffic and login attempts can't drain it.
|
||||||
|
export const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 300,
|
||||||
|
skip: (req) =>
|
||||||
|
req.method === 'OPTIONS' ||
|
||||||
|
req.path === '/health' ||
|
||||||
|
PUBLIC_AUTH_PATHS.some((p) => req.path.startsWith(p)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Strict budget for the login door only. Successful logins are free
|
||||||
|
// (skipSuccessfulRequests), so normal use never notices it — only repeated
|
||||||
|
// failed attempts burn budget. 429 here means "wrong password 20 times in
|
||||||
|
// 15 minutes", never "the API was busy".
|
||||||
|
export const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 20,
|
||||||
|
skipSuccessfulRequests: true,
|
||||||
|
message: {
|
||||||
|
success: false,
|
||||||
|
error: 'Too many login attempts, please try again later.',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Project-attached wrapper around `make release` so you can run:
|
||||||
|
# ./release v0.1.0 (or) ./release 0.1.0
|
||||||
|
# instead of:
|
||||||
|
# make release VERSION=v0.1.0
|
||||||
|
# Lives in the repo (no machine-specific alias needed).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ "$#" -ge 1 ] || { echo "Usage: ./release v0.1.0" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Accept "0.1.0" or "v0.1.0"; ensure the tag starts with 'v' (the workflow
|
||||||
|
# only triggers on v* tags).
|
||||||
|
VERSION="v${1#v}"
|
||||||
|
|
||||||
|
exec make release "VERSION=${VERSION}"
|
||||||
+5
-4
@@ -4,14 +4,15 @@ import express from 'express'
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
import { register, login, getMe, forgotPassword, resetPassword, updateDetails, updatePassword, logout } from '../controllers/auth.js'
|
import { register, login, getMe, forgotPassword, resetPassword, updateDetails, updatePassword, logout } from '../controllers/auth.js'
|
||||||
import { protect } from '../middleware/auth.js'
|
import { protect } from '../middleware/auth.js'
|
||||||
|
import { authLimiter } from '../middleware/rateLimit.js'
|
||||||
|
|
||||||
router.post('/register', register)
|
router.post('/register', authLimiter, register)
|
||||||
router.post('/login', login)
|
router.post('/login', authLimiter, login)
|
||||||
router.get('/logout', logout)
|
router.get('/logout', logout)
|
||||||
router.get('/me', protect, getMe)
|
router.get('/me', protect, getMe)
|
||||||
router.put('/updatedetails', protect, updateDetails)
|
router.put('/updatedetails', protect, updateDetails)
|
||||||
router.post('/forgotpassword', forgotPassword)
|
router.post('/forgotpassword', authLimiter, forgotPassword)
|
||||||
router.put('/resetpassword/:resettoken', resetPassword)
|
router.put('/resetpassword/:resettoken', authLimiter, resetPassword)
|
||||||
router.put('/updatepassword', protect, updatePassword)
|
router.put('/updatepassword', protect, updatePassword)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -33,12 +33,8 @@ const createAdmin = async () => {
|
|||||||
gamesToAccess: [],
|
gamesToAccess: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await user.save((err) => {
|
await user.save()
|
||||||
console.log('Admin user successfully created')
|
console.log('Admin user successfully created')
|
||||||
if (err) {
|
|
||||||
return console.error('Could not save admin user')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return console.error(
|
return console.error(
|
||||||
'Server Error: Could not create admin user' + err.message
|
'Server Error: Could not create admin user' + err.message
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ const steamScraper = async (resData) => {
|
|||||||
if (steamId === null || steamId === '') {
|
if (steamId === null || steamId === '') {
|
||||||
return 'Please enter a valid Steam Id'
|
return 'Please enter a valid Steam Id'
|
||||||
}
|
}
|
||||||
const steamApiUrl = `https://store.steampowered.com/api/appdetails/?appids=${steamId}`
|
const steamApiUrl = `https://store.steampowered.com/api/appdetails/?appids=${steamId}&l=english`
|
||||||
const steamRatingApiUrl = `https://store.steampowered.com/appreviews/${steamId}?json=1`
|
const steamRatingApiUrl = `https://store.steampowered.com/appreviews/${steamId}?json=1`
|
||||||
const steamWebUrl = `https://store.steampowered.com/app/${steamId}`
|
const steamWebUrl = `https://store.steampowered.com/app/${steamId}`
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* normalizeSteamId()
|
||||||
|
*
|
||||||
|
* @description Accepts either a plain numeric Steam app ID or a
|
||||||
|
* store.steampowered.com URL and returns the numeric app ID.
|
||||||
|
* Returns "" when the input is empty/missing (manual non-scrape flow
|
||||||
|
* looks games up by title instead) and `null` when the input is
|
||||||
|
* neither digits nor a recognized Steam Store URL.
|
||||||
|
*/
|
||||||
|
const normalizeSteamId = (value) => {
|
||||||
|
const raw = String(value ?? "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
|
||||||
|
const urlMatch = raw.match(
|
||||||
|
/store\.steampowered\.com\/(?:agecheck\/)?app\/(\d+)/,
|
||||||
|
);
|
||||||
|
if (urlMatch) return urlMatch[1];
|
||||||
|
|
||||||
|
if (/^\d+$/.test(raw)) return raw;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default normalizeSteamId;
|
||||||
Reference in New Issue
Block a user