11 Commits
Author SHA1 Message Date
john-okeefe 4fe5aa25f8 chore(ci): tag-triggered release workflow and helpers
Release / build-and-push (push) Successful in 1m17s
Add .gitea/workflows/release.yml: on v* tag push (or manual
dispatch) build the Docker image and push
git.linuxhg.com/games-database/games-api:<tag> plus :latest,
then create/update the Gitea Release from the annotated tag
message. Main-branch pushes do nothing so WIP never ships.

Add cliff.toml (conventional-commit grouping for release
notes), Makefile release target (cliff notes into annotated
tag, then push), and ./release wrapper (accepts 1.0 or
v1.0). Release flow: ./release v1.0, matching bookhoard.

Requires a REGISTRY_TOKEN repo Actions secret (PAT with
write:package and write:repository); Gitea's auto token
lacks package scope.
2026-09-05 21:05:20 -04:00
john-okeefe a5ceb721f3 chore(docker): portable image and registry run template
Dockerfile: stop baking secrets into the image. Only NODE_ENV
remains; all runtime config (MONGO_URI, SMTP_*, token
secrets) comes from the environment so one image runs
anywhere. Install curl for container healthchecks. App
variable names untouched (singular JWT_EXPIRE as read by
models/User.js).

docker-compose.example.yml: rewrite as a prod run template
for git.linuxhg.com/games-database/games-api (IMAGE_TAG,
default latest) with restart, env_file .env, and a curl
/health healthcheck. Copy to docker-compose.yml next to
the server .env, then pull and up. Full variable list kept
commented out so values can live in the file instead of
.env if preferred.
2026-09-05 20:37:03 -04:00
john-okeefe 38e6d21585 feat(health): add unauthenticated GET /health endpoint
Returns 200 { status: 'ok' } for container healthchecks and
monitoring. Placed before all /api/* routes so it bypasses
auth; intentionally static with no DB dependency so the
container reports healthy whenever the process is serving.
2026-09-05 20:36:57 -04:00
john-okeefe 689610b434 chore(compose): sync example env with codebase
Add NODE_ENV (used by config/db.js and controllers/auth.js
to switch MONGO_URI/MONGO_DEV_URI and secure cookies).

Drop PATH, BUN_RUNTIME_TRANSPILER_CACHE_PATH, and
BUN_INSTALL_BIN leaked from container runtime env; they are
not read by the app.

Drop JWT_EXPIRES duplicate; app reads JWT_EXPIRE
(models/User.js expiresIn), matching .env.

Restore SECURE=false default (used by utils/sendEmail.js
via yn(Bun.env.SECURE)).

Keep list aligned with Dockerfile and .env keys so the
example stays a valid template for server deployments.
2026-09-05 20:01:30 -04:00
john-okeefe 748616df9d feat(games): accept full Steam store URL in create()
Allow callers to pass either a raw Steam app ID or a full store URL (e.g. https://store.steampowered.com/app/730/...) in req.body.steamId.

In create() in controllers/games.js:

- normalize input into a local steamId variable: if the value includes store.steampowered.com, extract the numeric ID via match(/\d+/)[0], otherwise use the value as-is

- use the normalized steamId for both duplicate checks: user-scoped lookup (steamId + accessedBy.user) and global lookup

This lets users paste a copied store link directly without manually stripping the app ID, while keeping existing raw-ID behavior unchanged.
2026-09-05 19:16:14 -04:00
john-okeefe a9b9fe1047 style(games): reformat controllers/games.js with Prettier
Apply Prettier default formatting across the games controller with no logic changes:

- single -> double quotes, add missing semicolons

- 2-space indentation and consistent line wrapping

- expand dense ternary/decode blocks in create() and update() for readability

This isolates the upcoming create() Steam URL feature so its functional diff stays small and reviewable.
2026-09-05 19:16:06 -04:00
john-okeefe 20c1fd7be9 Ensure API Response is in English
added English to the API URL to ensure the returned JSON is in English. I'm leaving the Headers language in place, but it is not as reliable
2025-06-29 11:50:29 -04:00
john-okeefe 2800d2fb2e sorting now default starting with most recently created 2024-10-04 23:35:00 -04:00
john-okeefe cce705c4f7 for now, cors supports all 2024-09-12 19:33:14 -04:00
john-okeefe ed889d95e6 added docker support 2024-09-12 19:32:45 -04:00
john-okeefe c61d0c40af added docker support 2024-09-12 19:32:09 -04:00
12 changed files with 535 additions and 179 deletions
+15
View File
@@ -0,0 +1,15 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
+112
View File
@@ -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
+2
View File
@@ -130,3 +130,5 @@ dist
.yarn/install-state.gz
.pnp.*
# docker-compose.yml
docker-compose.yml
+41
View File
@@ -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" ]
+33
View File
@@ -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."
+3 -1
View File
@@ -41,7 +41,9 @@ const limiter = rateLimit({
max: 100
})
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), limiter, hpp(), cors(corsOptions))
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), limiter, hpp(), cors())
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }))
app.use('/api/admin/games', adminGames)
app.use('/api/games', games)
+37
View File
@@ -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"
+236 -176
View File
@@ -1,238 +1,298 @@
// noinspection DuplicatedCode
import Game from '../models/Game.js'
import steamScraper from '../scripts/scraper.js'
import asyncHandler from '../middleware/async.js'
import ErrorResponse from '../utils/errorResponse.js'
import {decode, isValid} from "js-base64";
import Game from "../models/Game.js";
import steamScraper from "../scripts/scraper.js";
import asyncHandler from "../middleware/async.js";
import ErrorResponse from "../utils/errorResponse.js";
import { decode, isValid } from "js-base64";
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
const checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
const checkForTwelveRegExp = new RegExp("^[0-9a-fA-F]{12}$");
/**
* games.js
*
* @description :: Server-side logic for managing games.
*/
/**
* gameController.list()
*/
export const list = asyncHandler(async (req, res, next) => {
const data = res.advancedResults.data
if (data[0]?.accessedBy) {
for (let i = 0; i < data.length; i++) {
for (let x = 0; x < data[i].accessedBy.length; x++) {
if (data[i].accessedBy[x].user.toString() !== req.user.id)
data[i].accessedBy.splice(x, 1)
}
}
const data = res.advancedResults.data;
if (data[0]?.accessedBy) {
for (let i = 0; i < data.length; i++) {
for (let x = 0; x < data[i].accessedBy.length; x++) {
if (data[i].accessedBy[x].user.toString() !== req.user.id)
data[i].accessedBy.splice(x, 1);
}
}
return res.status(200).json(res.advancedResults)
})
}
return res.status(200).json(res.advancedResults);
});
/**
* gameController.show()
*/
export const show = asyncHandler(async (req, res, next) => {
const {id} = req.params
const { id } = req.params;
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? "_id"
: "steamId";
const game = await Game.findOne({[gameId]: id})
const game = await Game.findOne({ [gameId]: id });
if (!game)
return next(
new ErrorResponse(`Game not found with id of ${req.params.id}`, 404),
)
if (!game)
return next(
new ErrorResponse(`Game not found with id of ${req.params.id}`, 404),
);
if (
!game.accessedBy
.map((x) => x.user.toString() === req.user.id)
.includes(true)
)
return next(
new ErrorResponse(
`You do not have permission to access Game ID ${req.params.id}`,
401,
),
)
if (
!game.accessedBy
.map((x) => x.user.toString() === req.user.id)
.includes(true)
)
return next(
new ErrorResponse(
`You do not have permission to access Game ID ${req.params.id}`,
401,
),
);
const userGame = await Game.findOne({[gameId]: id}).select(
'-createdBy -__v',
)
const userGame = await Game.findOne({ [gameId]: id }).select(
"-createdBy -__v",
);
for (let i = 0; i < userGame.accessedBy.length; i++) {
if (userGame.accessedBy[i].user.toString() !== req.user.id)
userGame.accessedBy.splice(i, 1)
}
for (let i = 0; i < userGame.accessedBy.length; i++) {
if (userGame.accessedBy[i].user.toString() !== req.user.id)
userGame.accessedBy.splice(i, 1);
}
res.status(200).json({success: true, data: userGame})
})
res.status(200).json({ success: true, data: userGame });
});
/**
* gameController.create()
*/
export const create = asyncHandler(async (req, res, next) => {
let oldGame
let oldGame;
let steamId;
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) : ''
if (req.body.steamId.includes("store.steampowered.com"))
steamId = req.body.steamId.match(/\d+/)[0];
else steamId = req.body.steamId;
req.body.steamId.length > 0
? (oldGame = await Game.findOne({
steamId: req.body.steamId,
'accessedBy.user': req.user.id,
}))
: (oldGame = await Game.findOne({
title: req.body.title,
'accessedBy.user': req.user.id,
}))
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,
))
: "";
if (oldGame)
return next(
new ErrorResponse(`The game ${oldGame.title} already exists in users account.`, 400)
)
steamId.length > 0
? (oldGame = await Game.findOne({
steamId: steamId,
"accessedBy.user": req.user.id,
}))
: (oldGame = await Game.findOne({
title: req.body.title,
"accessedBy.user": req.user.id,
}));
req.body.steamId.length > 0
? (oldGame = await Game.findOne({steamId: req.body.steamId}))
: (oldGame = await Game.findOne({title: req.body.title}))
if (oldGame)
return next(
new ErrorResponse(
`The game ${oldGame.title} already exists in users account.`,
400,
),
);
if (oldGame) {
oldGame.accessedBy.push({
user: req.user.id,
store: req.body.accessedBy[0].store,
playStatus: req.body.accessedBy[0].playStatus,
soundtrack: req.body.accessedBy[0].soundtrack,
rating: req.body.accessedBy[0].rating,
})
steamId.length > 0
? (oldGame = await Game.findOne({ steamId: steamId }))
: (oldGame = await Game.findOne({ title: req.body.title }));
oldGame.lastModifiedBy = req.user.id
if (oldGame) {
oldGame.accessedBy.push({
user: req.user.id,
store: req.body.accessedBy[0].store,
playStatus: req.body.accessedBy[0].playStatus,
soundtrack: req.body.accessedBy[0].soundtrack,
rating: req.body.accessedBy[0].rating,
});
await oldGame.save()
return res.status(200).json({
success: true,
data: oldGame,
})
}
oldGame.lastModifiedBy = req.user.id;
req.body.createdBy = req.user.id
req.body.lastModifiedBy = req.user.id
req.body.accessedBy[0].user = req.user.id
const data =
req.body.scrape === true ? await steamScraper(req.body) : req.body
await oldGame.save();
return res.status(200).json({
success: true,
data: oldGame,
});
}
const game = await Game.create(data)
req.body.createdBy = req.user.id;
req.body.lastModifiedBy = req.user.id;
req.body.accessedBy[0].user = req.user.id;
const data =
req.body.scrape === true ? await steamScraper(req.body) : req.body;
res.status(200).json({
success: true,
data: game,
})
})
const game = await Game.create(data);
res.status(200).json({
success: true,
data: game,
});
});
/**
* gameController.update()
*/
export const update = asyncHandler(async (req, res, next) => {
const {id} = req.params
const { id } = req.params;
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? "_id"
: "steamId";
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) : ''
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,
))
: "";
let game = await Game.findOne({
[gameId]: id,
'accessedBy.user': req.user.id,
})
let game = await Game.findOne({
[gameId]: id,
"accessedBy.user": req.user.id,
});
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
)
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
);
game = await Game.findOneAndUpdate(
{[gameId]: id, 'accessedBy.user': req.user.id},
{
$set: {
series: req.body.series,
intel: req.body.intel,
genre: req.body.genre,
wine: req.body.wine,
lastModifiedBy: req.user.id,
'accessedBy.$.store': req.body.accessedBy[0].store,
'accessedBy.$.playStatus': req.body.accessedBy[0].playStatus,
'accessedBy.$.soundtrack': req.body.accessedBy[0].soundtrack,
'accessedBy.$.rating': req.body.accessedBy[0].rating,
},
},
{new: true, runValidators: true},
)
game = await Game.findOneAndUpdate(
{ [gameId]: id, "accessedBy.user": req.user.id },
{
$set: {
series: req.body.series,
intel: req.body.intel,
genre: req.body.genre,
wine: req.body.wine,
lastModifiedBy: req.user.id,
"accessedBy.$.store": req.body.accessedBy[0].store,
"accessedBy.$.playStatus": req.body.accessedBy[0].playStatus,
"accessedBy.$.soundtrack": req.body.accessedBy[0].soundtrack,
"accessedBy.$.rating": req.body.accessedBy[0].rating,
},
},
{ new: true, runValidators: true },
);
res.status(200).json({
success: true,
data: game,
})
})
res.status(200).json({
success: true,
data: game,
});
});
/**
* gameController.remove()
*/
export const remove = asyncHandler(async (req, res, next) => {
const {id} = req.params
const { id } = req.params;
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? "_id"
: "steamId";
let game = await Game.findOne({
[gameId]: id,
'accessedBy.user': req.user.id,
})
let game = await Game.findOne({
[gameId]: id,
"accessedBy.user": req.user.id,
});
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
)
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
);
if (game.accessedBy.length > 1) {
await Game.findOneAndUpdate(
{[gameId]: id, 'accessedBy.user': req.user.id},
{
$pull: {
accessedBy: {user: req.user.id},
},
},
{new: true, runValidators: true},
)
} else {
await Game.findOneAndDelete({[gameId]: id})
}
if (game.accessedBy.length > 1) {
await Game.findOneAndUpdate(
{ [gameId]: id, "accessedBy.user": req.user.id },
{
$pull: {
accessedBy: { user: req.user.id },
},
},
{ new: true, runValidators: true },
);
} else {
await Game.findOneAndDelete({ [gameId]: id });
}
res.status(200).json({
success: true,
data: {},
})
})
res.status(200).json({
success: true,
data: {},
});
});
+39
View File
@@ -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
+1 -1
View File
@@ -162,7 +162,7 @@ const advancedResults = (model, populate) => async (req, res, next) => {
const sortBy = req.query.sort.split(',').join(' ')
query = query.sort(sortBy)
} else {
query = query.sort('series title')
query = query.sort('-createDate series title')
}
// Pagination
Executable
+15
View File
@@ -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}"
+1 -1
View File
@@ -6,7 +6,7 @@ const steamScraper = async (resData) => {
if (steamId === null || steamId === '') {
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 steamWebUrl = `https://store.steampowered.com/app/${steamId}`
try {