Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ node_modules/
config.json
data.json
*.example
.eslintrc.json
.prettierignore
eslint.config.js
scripts/
test/
.github/
README.md
66 changes: 0 additions & 66 deletions .eslintrc.json

This file was deleted.

31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main, modernization]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm test

- name: Build Docker image (amd64)
run: docker build --platform linux/amd64 -t strng:ci .
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
config.json
data.json
node_modules/
node_modules/
# Deploy secrets
scripts/config.sh
13 changes: 11 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
FROM node:lts-alpine
FROM node:24-alpine

WORKDIR /app

ENV NODE_ENV=production

# Install production dependencies only
COPY package*.json ./
RUN npm ci --omit=dev

# Copy application source
COPY server.js ./
COPY server.js lib.js ./
COPY views/ ./views/

# config.json and data.json are not baked in — mount them at runtime:
# docker run -v /host/config.json:/app/config.json \
# -v /host/data.json:/app/data.json \
# -p 3000:3000 strng
#
# NOTE: the container writes to data.json, so it currently runs as root to keep
# writes to the mounted volume working unchanged. Running as a non-root user is
# a recommended follow-up (requires the data volume to be writable by that user).

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/login >/dev/null 2>&1 || exit 1

CMD ["node", "server.js"]
40 changes: 40 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const js = require("@eslint/js");
const globals = require("globals");

module.exports = [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 2022,
sourceType: "commonjs",
globals: {
...globals.node,
},
},
rules: {
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"no-console": "off",
"no-var": "warn",
"no-case-declarations": "off",
"semi-spacing": ["error", { before: false, after: true }],
"max-len": ["error", { code: 90 }],
indent: ["error", 2],
"no-lonely-if": "error",
"no-multiple-empty-lines": ["error", { max: 1 }],
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"require-await": "error",
"prefer-arrow-callback": "error",
},
},
{
files: ["test/**/*.js"],
languageOptions: {
globals: {
...globals.jest,
},
},
},
];
54 changes: 54 additions & 0 deletions lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const crypto = require("crypto");

// Constant-time string comparison to prevent timing attacks
function safeCompare(a, b) {
const bufA = Buffer.from(String(a));
const bufB = Buffer.from(String(b));
if (bufA.length !== bufB.length) {
crypto.timingSafeEqual(bufA, bufA); // keep timing constant
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}

// True if month is in the range [start, end] (wraps around the year)
function monthBetween(month, start, end) {
if (start <= end) {
return month >= start && month <= end;
}
return month >= start || month <= end;
}

// Returns the division abbreviation for the given month, or null if mush
function getCurrentDivision(month, monthDivisions) {
for (const division of monthDivisions) {
if (monthBetween(month, division.start, division.end)) {
return division.abbr;
}
}
return null;
}

// Returns the list of valid division options during a mush month, or null
function getMushOptions(month, mushMonths) {
for (const mush of mushMonths) {
if (monthBetween(month, mush.start, mush.end)) {
return mush.options;
}
}
return null;
}

// Format: <LOC>-<DIV><YY><NNN> — e.g. DCC-S25001
function formatRunNumber(year, division, locAbbr, num) {
const padded = num.toString().padStart(3, "0");
return `${locAbbr}-${division}${year}${padded}`;
}

module.exports = {
safeCompare,
monthBetween,
getCurrentDivision,
getMushOptions,
formatRunNumber,
};
Loading
Loading