beatrun-courses-server-docker/index.js
2024-08-29 09:48:27 +05:00

277 lines
7.7 KiB
JavaScript

const express = require("express"),
path = require("path"),
session = require("express-session"),
logger = require("morgan"),
fs = require("fs"),
passport = require("passport"),
SteamStrategy = require("passport-steam").Strategy;
const { JsonDB, Config } = require("node-json-db"),
{ generateRandomString, log } = require("./utils/functions");
const config = require("./config");
const db = new JsonDB(new Config(`data/${config.production ? "main" : "test"}_db`, true, true, "/"));
if (!config.cookieSecret || !config.steamKey) return console.log("Please check that you have filled steamKey and cookieSecret in config file");
if (!fs.existsSync("public/courses/")) fs.mkdirSync("public/courses/");
// Express App
const indexRouter = require("./routes/index"),
keyRouter = require("./routes/key"),
uploadRouter = require("./routes/upload"),
adminRouter = require("./routes/admin"),
apiRouter = require("./routes/api"),
statsRouter = require("./routes/stats");
const app = express();
/**
* Callback to serialize user into session.
* It is called at end of request in authenticate method after successful authentication.
* This method will store necessary data from the authenticated user object (`user`).
* @param {Object} user - The authenticated user.
* @param {Function} done - Callback to signal success or failure for saving session data.
*/
passport.serializeUser((user, done) => {
done(null, user._json);
});
/**
* Callback to deserialize user from session.
* It is called at the start of each request in authenticate method after receiving client's request.
* This method will reconstruct a User object (`user`) based on data stored during `serializeUser` call.
* @param {Object} obj - The serialized user data from session.
* @param {Function} done - Callback to signal success or failure for loading user.
*/
passport.deserializeUser((obj, done) => {
done(null, obj);
});
passport.use(
new SteamStrategy(
{
realm: config.domain,
returnURL: `${config.domain}/auth/return`,
apiKey: config.steamKey,
},
(_, profile, done) => {
return done(null, profile);
},
),
);
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(
session({
secret: config.cookieSecret,
name: "U_SESSION",
resave: true,
saveUninitialized: true,
cookie: {
maxAge: 3600000,
},
}),
);
app.use(passport.initialize());
app.use(passport.session());
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
app.use("/key", keyRouter);
app.use("/upload", uploadRouter);
app.use("/admin", adminRouter);
app.use("/api", apiRouter);
app.use("/stats", statsRouter);
app.get("/auth", passport.authenticate("steam"), () => {});
app.get("/auth/return", passport.authenticate("steam", { failureRedirect: "/" }), (req, res) => res.redirect("/key"));
app.get("/auth/logout", (req, res, next) => {
req.logout(function (err) {
if (err) return next(err);
res.redirect("/key");
});
});
/* catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// Error Handler
app.use(function (err, req, res) {
res.status(err.status || 500);
res.render("error");
});
*/
// Locals
db.getData("/admins").then(data => {
app.locals.admins = data;
});
app.locals = {
config: config,
db: db,
getKey: getKey,
isRatelimited: isRatelimited,
isMultiAccount: isMultiAccount,
};
/**
* Gets a user's key from the database.
*
* Checks if the user already has a key, and returns it if so.
* Otherwise generates a new one.
*
* @param {Object | string} user The user object or SteamID64.
* @returns {Promise<String>} The user's key.
*/
async function getKey(user) {
user = typeof user === "string" ? user : user.steamid;
const keys = await db.getData("/keys");
const key = keys[user];
if (key) {
await log(
`[KEY] User logged in (SteamID: ${user}, Key ${key}).`,
`[KEY] User logged in (SteamID: \`${user}\`, Key \`${key}\`).`,
);
return key;
} else return await _createKey(user);
}
/**
* Creates a new unique key for the given user and saves it to the database.
*
* @param {Object | string} user The OpenID Steam user object or SteamID64.
* @returns {Promise<String>} The new unique key generated for the user.
*/
async function _createKey(user) {
user = typeof user === "string" ? user : user.steamid;
const keys = await db.getData("/keys");
const key = generateRandomString();
const isFound = keys[key];
if (!isFound) {
keys[user] = key;
const now = Date.now();
await log(
`[KEY] New user (SteamID: ${user}, Key: ${key}, TimeCreated: ${new Date(now).toLocaleString("ru-RU")}).`,
`[KEY] New user (SteamID: \`${user}\`, Key: \`${key}\`, TimeCreated: <t:${Math.floor(now / 1000)}:f>).`,
);
await db.push("/keys", keys);
return key;
} else return await _createKey(user);
}
/**
* Checks if an IP address is currently rate limited.
*
* Gets the current rate limits from the database.
* If the IP already has a recent rate limit, returns true.
* Otherwise, saves a new rate limit for the IP and returns false.
*
* @param {string} ip The IP address to check
* @returns {Promise<boolean>} Whether the IP is currently rate limited
*/
async function isRatelimited(ip) {
const rateLimits = await db.getData("/ratelimits");
if (rateLimits[ip] && Date.now() - rateLimits[ip] <= config.rateLimitTime) return true;
rateLimits[ip] = Date.now();
await db.push("/ratelimits", rateLimits);
return false;
}
/**
* Checks if a user is using multiple accounts based on their IP address and Steam ID.
*
* Retrieves the locked accounts and user records from the database. If the user's account is locked, returns true.
* Otherwise, checks if the user has changed their IP address more than the configured `ipChangeTime`. If so, clears the
* user's IP address history and locks the account if the user has changed their IP more than 3 times. Updates the
* database with the new account status and returns the result.
*
* @param {string} ip The IP address of the user.
* @param {string} steamid The Steam ID of the user.
* @returns {Promise<boolean>} Whether the user is using multiple accounts.
*/
async function isMultiAccount(ip, steamid) {
const locked = await db.getData("/locked");
const records = await db.getData("/records");
if (locked[steamid]) return true;
if (!records[steamid])
records[steamid] = {
ips: {
[ip]: true,
},
lastchanged: Date.now(),
};
// Clear IPs if the user has changed their ip more than ipChangeTime
if (Date.now() - records[steamid]["lastchanged"] > config.ipChangeTime) {
records[steamid]["ips"] = [];
records[steamid]["lastchanged"] = Date.now();
}
// Lock account if the user changed their IP more than 3 time in ipChangeTime
if (Object.keys(records[steamid]["ips"]).length > 2) {
locked[steamid] = true;
await db.push("/locked", locked);
return true;
}
await db.push("/records", records);
return false;
}
// HTTP server
const http = require("http");
const port = config.port || 6547;
app.set("port", port);
const server = http.createServer(app);
server.listen(port);
server.on("error", error => {
if (error.syscall !== "listen") throw error;
// handle specific listen errors with friendly messages
switch (error.code) {
case "EACCES":
console.error(`Port ${port} requires elevated privileges`);
process.exit(1);
break;
case "EADDRINUSE":
console.error(`Port ${port} is already in use`);
process.exit(1);
break;
default:
throw error;
}
});
server.on("listening", () => {
const addr = server.address();
const bind = typeof addr === "string" ? "pipe: " + addr : "port: " + addr.port;
console.log("Now listening on " + bind);
});