Skip to content

Commit f80dde4

Browse files
fehmerMiodec
andauthored
chore(self hosting): run selfhosted backend in prod mode (@fehmer) (monkeytypegame#6326)
Co-authored-by: Miodec <[email protected]>
1 parent 7d7118f commit f80dde4

File tree

14 files changed

+116
-75
lines changed

14 files changed

+116
-75
lines changed

.github/workflows/publish-docker-images.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
push: true
4949
tags: ${{ env.BE_REPO }}:latest,${{ steps.bemeta.outputs.tags }}
5050
labels: ${{ steps.bemeta.outputs.labels }}
51+
build-args: |
52+
server_version: {{version}}
5153
5254
- name: Backend publish description
5355
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae

backend/src/anticheat/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
const hasAnticheatImplemented = process.env["BYPASS_ANTICHEAT"] === "true";
2+
13
import {
24
CompletedEvent,
35
KeyStats,
46
} from "@monkeytype/contracts/schemas/results";
7+
import Logger from "../utils/logger";
58

69
export function implemented(): boolean {
7-
return false;
10+
if (hasAnticheatImplemented) {
11+
Logger.warning("BYPASS_ANTICHEAT is enabled! Running without anti-cheat.");
12+
}
13+
return hasAnticheatImplemented;
814
}
915

1016
export function validateResult(
@@ -13,6 +19,7 @@ export function validateResult(
1319
_uaStringifiedObject: string,
1420
_lbOptOut: boolean
1521
): boolean {
22+
Logger.warning("No anticheat module found, result will not be validated.");
1623
return true;
1724
}
1825

@@ -22,5 +29,6 @@ export function validateKeys(
2229
_keyDurationStats: KeyStats,
2330
_uid: string
2431
): boolean {
32+
Logger.warning("No anticheat module found, key data will not be validated.");
2533
return true;
2634
}

backend/src/constants/base-configuration.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const BASE_CONFIGURATION: Configuration = {
9696
xpRewardBrackets: [],
9797
},
9898
leaderboards: {
99+
minTimeTyping: 2 * 60 * 60,
99100
weeklyXp: {
100101
enabled: false,
101102
expirationTimeInDays: 0, // This should atleast be 15
@@ -548,6 +549,12 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema<Configuration> = {
548549
type: "object",
549550
label: "Leaderboards",
550551
fields: {
552+
minTimeTyping: {
553+
type: "number",
554+
label: "Minimum typing time the user needs to get on a leaderboard",
555+
hint: "Typing time in seconds. Change is only applied after restarting the server.",
556+
min: 0,
557+
},
551558
weeklyXp: {
552559
type: "object",
553560
label: "Weekly XP",

backend/src/dal/leaderboards.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import Logger from "../utils/logger";
33
import { performance } from "perf_hooks";
44
import { setLeaderboard } from "../utils/prometheus";
55
import { isDevEnvironment } from "../utils/misc";
6-
import { getCachedConfiguration } from "../init/configuration";
6+
import {
7+
getCachedConfiguration,
8+
getLiveConfiguration,
9+
} from "../init/configuration";
710

811
import { addLog } from "./logs";
912
import { Collection, ObjectId } from "mongodb";
1013
import { LeaderboardEntry } from "@monkeytype/contracts/schemas/leaderboards";
1114
import { omit } from "lodash";
12-
import { DBUser } from "./user";
15+
import { DBUser, getUsersCollection } from "./user";
1316
import MonkeyError from "../utils/error";
1417

1518
export type DBLeaderboardEntry = LeaderboardEntry & {
@@ -269,7 +272,11 @@ export async function update(
269272
};
270273
}
271274

272-
async function createIndex(key: string): Promise<void> {
275+
async function createIndex(
276+
key: string,
277+
minTimeTyping: number,
278+
dropIfMismatch = true
279+
): Promise<void> {
273280
const index = {
274281
[`${key}.wpm`]: -1,
275282
[`${key}.acc`]: -1,
@@ -293,16 +300,41 @@ async function createIndex(key: string): Promise<void> {
293300
$gt: 0,
294301
},
295302
timeTyping: {
296-
$gt: isDevEnvironment() ? 0 : 7200,
303+
$gt: minTimeTyping,
297304
},
298305
},
299306
};
300-
await db.collection("users").createIndex(index, partial);
307+
try {
308+
await getUsersCollection().createIndex(index, partial);
309+
} catch (e) {
310+
if (!dropIfMismatch) throw e;
311+
if (
312+
(e as Error).message.startsWith(
313+
"An existing index has the same name as the requested index"
314+
)
315+
) {
316+
Logger.warning(`Index ${key} not matching, dropping and recreating...`);
317+
318+
const existingIndex = (await getUsersCollection().listIndexes().toArray())
319+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
320+
.map((it) => it.name as string)
321+
.find((it) => it.startsWith(key));
322+
323+
if (existingIndex !== undefined && existingIndex !== null) {
324+
await getUsersCollection().dropIndex(existingIndex);
325+
return createIndex(key, minTimeTyping, false);
326+
} else {
327+
throw e;
328+
}
329+
}
330+
}
301331
}
302332

303333
export async function createIndicies(): Promise<void> {
304-
await createIndex("lbPersonalBests.time.15.english");
305-
await createIndex("lbPersonalBests.time.60.english");
334+
const minTimeTyping = (await getLiveConfiguration()).leaderboards
335+
.minTimeTyping;
336+
await createIndex("lbPersonalBests.time.15.english", minTimeTyping);
337+
await createIndex("lbPersonalBests.time.60.english", minTimeTyping);
306338

307339
if (isDevEnvironment()) {
308340
Logger.info("Updating leaderboards in dev mode...");

backend/src/init/configuration.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ import { identity } from "../utils/misc";
66
import { BASE_CONFIGURATION } from "../constants/base-configuration";
77
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
88
import { addLog } from "../dal/logs";
9-
import { PartialConfiguration } from "@monkeytype/contracts/configuration";
9+
import {
10+
PartialConfiguration,
11+
PartialConfigurationSchema,
12+
} from "@monkeytype/contracts/configuration";
1013
import { getErrorMessage } from "../utils/error";
14+
import { join } from "path";
15+
import { existsSync, readFileSync } from "fs";
16+
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
17+
import { z } from "zod";
1118

1219
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
20+
const SERVER_CONFIG_FILE_PATH = join(
21+
__dirname,
22+
"../backend-configuration.json"
23+
);
1324

1425
function mergeConfigurations(
1526
baseConfiguration: Configuration,
@@ -138,3 +149,20 @@ export async function patchConfiguration(
138149

139150
return true;
140151
}
152+
153+
export async function updateFromConfigurationFile(): Promise<void> {
154+
if (existsSync(SERVER_CONFIG_FILE_PATH)) {
155+
Logger.info(
156+
`Reading server configuration from file ${SERVER_CONFIG_FILE_PATH}`
157+
);
158+
const json = readFileSync(SERVER_CONFIG_FILE_PATH, "utf-8");
159+
const data = parseJsonWithSchema(
160+
json,
161+
z.object({
162+
configuration: PartialConfigurationSchema,
163+
})
164+
);
165+
166+
await patchConfiguration(data.configuration);
167+
}
168+
}

backend/src/init/email-client.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,12 @@ export async function init(): Promise<void> {
4444
Logger.warning(
4545
"No email client configuration provided. Running without email."
4646
);
47-
return;
47+
} else if (process.env["BYPASS_EMAILCLIENT"] === "true") {
48+
Logger.warning("BYPASS_EMAILCLIENT is enabled! Running without email.");
49+
} else {
50+
throw new Error("No email client configuration provided");
4851
}
49-
throw new Error("No email client configuration provided");
52+
return;
5053
}
5154

5255
try {

backend/src/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import "dotenv/config";
22
import * as db from "./init/db";
33
import jobs from "./jobs";
4-
import { getLiveConfiguration } from "./init/configuration";
4+
import {
5+
getLiveConfiguration,
6+
updateFromConfigurationFile,
7+
} from "./init/configuration";
58
import app from "./app";
69
import { Server } from "http";
710
import { version } from "./version";
@@ -30,6 +33,7 @@ async function bootServer(port: number): Promise<Server> {
3033
Logger.info("Fetching live configuration...");
3134
await getLiveConfiguration();
3235
Logger.success("Live configuration fetched");
36+
await updateFromConfigurationFile();
3337

3438
Logger.info("Initializing email client...");
3539
await EmailClient.init();

docker/backend-configuration.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
},
1212
"dailyLeaderboards": {
1313
"enabled": false
14+
},
15+
"leaderboards":{
16+
"minTimeTyping": 0
1417
}
1518
}
1619
}

docker/backend/Dockerfile

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ RUN pnpm deploy --filter backend --prod /prod/backend
2020
## target image
2121
FROM node:20.16.0-alpine3.19
2222

23-
##install wget, used by the applyConfig script
24-
RUN apk update --no-cache && \
25-
apk add --no-cache wget
23+
## get server_version from build-arg, default to UNKNOWN
24+
ARG server_version=UNKNOWN
2625

2726
# COPY to target
2827
COPY --from=builder /prod/backend/node_modules /app/backend/node_modules
@@ -37,10 +36,14 @@ WORKDIR /app/backend/dist
3736
## logs
3837
RUN mkdir -p /app/backend/dist/logs
3938

40-
COPY ["docker/backend/entry-point.sh", "docker/backend/applyConfig.sh", "./"]
39+
COPY ["docker/backend/entry-point.sh", "./"]
4140

42-
#run in dev mode (no anticheat)
43-
ENV MODE=dev
41+
RUN echo "${server_version}" > /app/backend/dist/server.version
42+
43+
#run in prod mode, but don't require anti-cheat or email client
44+
ENV MODE=prod
45+
ENV BYPASS_ANTICHEAT=true
46+
ENV BYPASS_EMAILCLIENT=true
4447

4548
EXPOSE 5005
4649
USER node

docker/backend/applyConfig.sh

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)