Changed file structure to implement the frontend properly and created frontend template

This commit is contained in:
Gu://em_ 2025-05-12 16:10:41 +02:00
parent feceab03b1
commit 32c0ffd715
41 changed files with 3784 additions and 0 deletions

145
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,145 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vscode files
.vscode
# Data
data/*
# Temporary
tests/

View file

@ -0,0 +1,19 @@
{
"port": 8000,
"users": {
"admin": {
"username": "admin",
"password": "admin"
}
},
"database": {
"type": "sqlite"
},
"auth": {
"JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY",
"tokenExpiry": "1h"
}
}

35
backend/package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "wf-radio-backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js",
"start-auto": "pnpx nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.5.2",
"dependencies": {
"ajv": "^8.17.1",
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.9.1",
"dompurify": "^3.2.4",
"express": "^5.1.0",
"jsdom": "^26.0.0",
"jsonwebtoken": "^9.0.2",
"marked": "^15.0.7",
"mysql": "^2.18.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"bcrypt",
"better-sqlite3"
]
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}

1884
backend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

38
backend/server.js Normal file
View file

@ -0,0 +1,38 @@
// --- Imports ---
const express = require("express");
const app = express();
const configManager = require("./src/utils/configManager");
const { connectDatabase, initDatabase } = require('./src/database/index');
// --- Load configuration ---
const config = configManager.loadConfig();
// --- Body parsing ---
app.use(express.json()); // Necessary to parse JSON bodies
// Database connection
(async () => {
// --- Database connection ---
await connectDatabase();
await initDatabase();
// --- Routing ---
app.use("/", require("./src/routes/index"));
app.use("/mods", require("./src/routes/mods"));
app.use("/users", require("./src/routes/users"));
app.use("/list", require("./src/routes/list"));
app.use("/login", require("./src/routes/login"));
})();
// --- Launch ---
const port = config.port;
app.listen(port, () => {
console.log("Server listening on port " + port + "...");
})

View file

@ -0,0 +1,17 @@
const handleError = require("../middleware/errors");
const authService = require("../services/authService");
async function login(req, res) {
try {
const username = req.body.username;
const email = req.body.email;
const password = req.body.password
const token = await authService.login(username, email, password);
res.json({ token });
} catch (err) {
handleError(err, res);
}
}
module.exports = { login };

View file

@ -0,0 +1,14 @@
const index_service = require("../services/indexService");
const handleError = require("../middleware/errors");
async function getVersion(req, res) {
try {
const query_result = await index_service.getVersion();
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
module.exports = { getVersion };

View file

View file

@ -0,0 +1,68 @@
const handleError = require("../middleware/errors");
const mod_service = require("../services/modService");
const { authorizeModModification, authenticateToken } = require("../middleware/auth");
async function listMods(req, res) {
try {
// Query
const query_result = await mod_service.getAllMods();
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function createMod(req, res) {
try {
// Authenticate
await authenticateToken(req);
// Query
const mod_data = req.body;
const user = req.token_infos.username;
const query_result = await mod_service.createMod(mod_data, user);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function modifyMod(req, res) {
try {
// Authorize
authorizeModModification(req);
// Query
const mod_data = req.body;
const query_result = await mod_service.modifyMod(mod_data);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function getModByName(req, res) {
try {
// Query
const name = req.params.name
const query_result = await mod_service.getModByName(name);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function deleteMod(req, res) {
try {
// Authorize
authorizeModModification(req);
// Query
const name = req.params.name
const query_result = await mod_service.deleteMod(name);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
module.exports = { listMods, getModByName, createMod, modifyMod, deleteMod };

View file

@ -0,0 +1,63 @@
const handleError = require("../middleware/errors");
const user_service = require("../services/userService")
const { authorizeUserModification } = require("../middleware/auth");
async function listUsers(req, res) {
try {
// Query
const query_result = await user_service.getAllUsers();
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function getUserByName(req, res) {
try {
// Query
const query_result = await user_service.getUserByName(req.params.name);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function createUser(req, res) {
try {
// Query
const query_result = await user_service.createUser(req.body);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function modifyUser(req, res) {
try {
// Query
const diff_data = req.body;
const query_result = await user_service.modifyUser(diff_data);
res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
async function deleteUser(req, res) {
try {
// Authenticate
await authorizeUserModification(req);
// Query
const user = req.params.name;
const token_user = req.token_infos
const query_result = await user_service.deleteUser(user, token_user);
return res.json(query_result);
} catch (error) {
handleError(error, res);
}
}
module.exports = { listUsers, getUserByName, createUser, modifyUser, deleteUser };

View file

@ -0,0 +1,112 @@
const MySQLDatabase = require("./mysql");
const SQLiteDatabase = require("./sqlite");
const { getConfig } = require("../utils/configManager");
let db;
async function connectDatabase() {
// Get config
const config = await getConfig();
// Choose database type
if (config.database.type === "mysql") {
db = new MySQLDatabase(config.database);
} else if (config.database.type === "sqlite") {
db = new SQLiteDatabase(config.database);
} else {
throw new Error("Invalid database type: ", config.database.type);
}
// Connect
await db.connect();
return db;
}
// Setups the database by creating the tables and the default objects
async function initDatabase() {
if (!db) {
throw new Error("Database is not connected");
}
// --- Users ---
// Uers table
db.exec(`CREATE TABLE IF NOT EXISTS Users (
username TINYTEXT PRIMARY KEY,
display_name TINYTEXT NOT NULL,
email TINYTEXT NOT NULL,
password TINYTEXT NOT NULL,
profile_picture LONGTEXT,
role TINYTEXT NOT NULL,
settings LONGTEXT
);`);
// --- Mods ---
// Mods table
db.exec(`CREATE TABLE IF NOT EXISTS Mods (
name TINYTEXT PRIMARY KEY,
display_name TINYTEXT NOT NULL,
author TINYTEXT NOT NULL,
description TINYTEXT NOT NULL,
FOREIGN KEY (author) REFERENCES Users(username)
);`);
// Mods complementary infos
db.exec(`CREATE TABLE IF NOT EXISTS ModInfos (
mod TINYTEXT PRIMARY KEY,
full_description TEXT NOT NULL,
license TINYTEXT,
custom_license TEXT,
links TEXT,
creation_date TINYTEXT NOT NULL,
downloads_count INT NOT NULL,
FOREIGN KEY (mod) REFERENCES Mods(name)
);`);
// Mods tags
db.exec(`CREATE TABLE IF NOT EXISTS ModTags (
mod TINYTEXT NOT NULL,
tag TINYTEXT NOT NULL,
FOREIGN KEY (mod) REFERENCES Mods(name)
);`);
// Mods versions
db.exec(`CREATE TABLE IF NOT EXISTS ModVersions (
mod TINYTEXT NOT NULL,
version_number TINYTEXT NOT NULL,
channel TINYTEXT NOT NULL,
changelog TEXT NOT NULL,
release_date TINYTEXT NOT NULL,
game_version TINYTEXT NOT NULL,
platform TINYTEXT NOT NULL,
environment TINYTEXT NOT NULL,
url TINYTEXT NOT NULL,
FOREIGN KEY (mod) REFERENCES Mods(name)
);`);
// User favorites (mods)
db.exec(`CREATE TABLE IF NOT EXISTS UserFavoriteMods (
username TINYTEXT NOT NULL,
mod TINYTEXT NOT NULL,
FOREIGN KEY (username) REFERENCES Users(username),
FOREIGN KEY (mod) REFERENCES Mods(name)
);`);
}
function getDatabase() {
return db;
}
module.exports = { getDatabase, connectDatabase, initDatabase };

View file

@ -0,0 +1,30 @@
class MySQLDatabase {
constructor(config) {
const mysql = require("mysql2/promise");
this.config = config;
}
async connect() {
this.db = await mysql.createConnection({
host: this.config.host,
user: this.config.user,
password: this.config.password,
database: this.config.database,
});
console.log("Connected to MySQL");
}
async close() {
await this.db.end();
}
async query(sql, params) {
throw new Error("Not implemented"); //TODO
const [results] = await this.db.execute(sql, params);
return results;
}
}
module.exports = MySQLDatabase;

View file

@ -0,0 +1,70 @@
const sqlite = require("better-sqlite3");
class SQLiteDatabase {
constructor(config) {
this.config = config;
this.db = null;
}
async connect() {
try {
this.db = new sqlite("./data/sqlite.db");
// this.db.pragma("journal_mode = WAL");
console.log("Connected to SQLite");
} catch (err) {
console.error("Error connecting to SQLite database: ", err);
process.exit(1);
}
}
async close() {
if (this.db && this.db.open) {
this.db.close();
console.debug("Closed database connection");
}
}
async query(sql, params = []) {
try {
if (params.length > 0) {
return this.db.prepare(sql).all(params);
} else {
return this.db.prepare(sql).all();
}
} catch (err) {
console.error("Error executing prepared query:", err);
throw err;
}
}
async exec(sql) {
try {
return this.db.exec(sql);
} catch (err) {
console.error("Error executing statement:", err)}
}
async prepare(sql, params = []) {
try {
if (params.length > 0) {
return this.db.prepare(sql).run(params);
} else {
return this.db.prepare(sql);
}
} catch (err) {
console.error("Error executing prepared statement:", err)}
}
async exists(table, attribute, value) {
try {
return this.db.prepare(`SELECT COUNT(*) FROM ${table} WHERE ${attribute} = ?`).get(value)['COUNT(*)'] > 0;
} catch (err) {
console.error("Error checking item existence");
}
}
}
module.exports = SQLiteDatabase;

View file

@ -0,0 +1,85 @@
const { getModByName } = require("../services/modService");
const { getModpackByName } = require("../services/modpackService");
const { getUserByName } = require("../services/userService");
const { verifyToken } = require("../utils/crypto");
const AppError = require("../utils/appError");
async function authenticateToken(req) {
const token = req.header("Authorization");
if (!token) {
throw new AppError(401, "Missing authorization header", "Unauthorized");
}
try {
req.token_infos = await verifyToken(token);
console.debug("Authorizing token from", req.token_infos);
} catch (err) {
throw new AppError(403, "Forbidden: Error verifying the authorization token");
}
return req.token_infos;
}
async function authorizeModModification(req) {
// Auth token
await authenticateToken(req);
// Get mod infos
if (!req.params || !req.params.name) {
throw new AppError(400, "No mod name was scpecified", "Bad request");
}
const mod_name = req.params.name;
const mod = await getModByName(mod_name);
if (!mod) {
throw new AppError(404, "No mod was found with this name", "Not found");
}
// Authorize
if ( mod.author != req.token_infos.username) {
throw new AppError(401, "Mod author differs from current user", "Unauthorized");
}
}
async function authorizeModpackModification(req) {
// Auth token
await authenticateToken(req);
// Get mod infos
if (!req.params || !req.params.name) {
throw new AppError(400, "No mod name was scpecified", "Bad request");
}
const modpack_name = req.params.name;
const modpack = await getModpackByName(modpack_name);
if (!modpack) {
throw new AppError(404, "No mod was found with this name", "Not found");
}
// Authorize
if ( modpack.author != req.token_infos.username) {
throw new AppError(401, "Mod author differs from current user", "Unauthorized");
}
}
async function authorizeUserModification(req) {
// Auth token
await authenticateToken(req);
// Get mod infos
if (!req.params || !req.params.name) {
throw new AppError(400, "No mod name was scpecified", "Bad request");
}
const user_name = req.params.name;
const user = await getUserByName(user_name);
if (!user) {
throw new AppError(404, "No user was found with this name", "Not found");
}
// Authorize
if ( user.username != req.token_infos.username) {
throw new AppError(401, "User to modify differs from current user", "Unauthorized");
}
}
module.exports = { authenticateToken, authorizeModModification, authorizeModpackModification, authorizeUserModification };

View file

@ -0,0 +1,31 @@
const AppError = require("../utils/appError");
const handleError = (err, res) => {
// Send error infos
if (err instanceof AppError) {
// Log
if (err.statusCode == 500) {
console.error("Error:", err.message);
if (err.debugMsg) {
console.debug(" >", err.debugMsg);
}
}
// Response
return res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// Default error
console.error("Error:", err.message);
res.status(500).json({
message: 'Internal server error',
status: 500
});
}
module.exports = handleError;

View file

@ -0,0 +1,12 @@
const configManager = require("../utils/configManager");
async function getVersion() {
const version = await configManager.getVersion();
const res = {
version: version
};
return res;
}
module.exports = { getVersion }

192
backend/src/models/mod.js Normal file
View file

@ -0,0 +1,192 @@
const { getDatabase } = require('../database/index');
const AppError = require('../utils/appError');
const db = getDatabase();
// --- Get ---
async function getAllMods() {
return await db.query("SELECT name, display_name, author, description FROM Mods");
}
async function getModByName(name) {
return await db.query("SELECT name, display_name, author FROM Mods WHERE name = ?;", [name]);
}
async function getModFullInfos(name) {
// Query
const base_infos = db.query(`SELECT * FROM Mods WHERE name = ?`, [name]);
const other_infos = db.query(`SELECT full_description, license, links, creation_date, downloads_count
FROM ModInfos WHERE name = ?`, [name]);
const tags = getModTags(name);
// Merge
const res = {...await base_infos, ...await other_infos, ...tags};
return res;
}
async function listVersions(mod_name) {
return await db.query("SELECT * FROM ModVersions WHERE mod = ?", [mod_name]);
}
async function getVersionByNumber(mod_name, version_number) {
return await db.query(`SELECT * FROM ModVersions
WHERE mod = ?
AND version_number = ?;`,
[mod_name, version_number]);
}
async function getVersion(mod_name, version_number, game_version, platform, environment) {
return await db.query(`SELECT * FROM ModVersions
WHERE mod = ?
AND version_number = ?
AND game_version = ?
AND platform = ?
AND environment = ?;`,
[mod_name, version_number, game_version, platform, environment]);
}
// --- Create ---
async function createMod(name, display_name, author, description, mod_infos) {
// Extract infos
const { full_description, license, links, creation_date, tags } = mod_infos;
// Mods table
await db.prepare("INSERT INTO Mods (name, display_name, author, description) \
VALUES (?, ?, ?, ?)",
[name, display_name, author, description]);
// ModInfos table
await db.prepare(`INSERT INTO ModInfos (mod, full_description, license, links, creation_date, downloads_count)
VALUES (?, ?, ?, ?, ?, ?)`,
[name, full_description, license.type, links.toString(), creation_date, 0]);
// Tags
if (tags) {
const tags_proc = addTags(name, tags, []);
}
// License
if (license.type == "custom") {
await db.prepare(`UPDATE ModInfos SET custom_license = ?
WHERE mod = ?`,
[license.content, name]);
}
// Await
if (tags) {
await tags_proc;
}
return;
}
async function addVersion(mod, version_number, channel, changelog, release_date, game_version, platform, environment, url) {
await db.prepare(`INSERT INTO ModVersions (mod, version_number, channel, changelog, release_date, game_version, environment, platform, url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`,
[mod, version_number, channel, changelog, release_date, game_version, environment, platform, url]);
return;
}
async function addTags(mod, tags) {
// Add asynchronously
const promises = tags.map(async (tag) => {
db.query(`INSERT INTO ModTags (mod, tag)
VALUES (?, ?);`,
[mod, tag]);
});
await Promise.all(promises);
return;
}
// --- Update ---
async function updateMod(name, display_name, author, description) {
if (display_name) {
await updateModAttributes(name, "display_name", display_name);
}
if (author) {
await updateModAttribute(nale, "author", author);
}
if (description) {
await updateModAttribute(name, "description", description)
}
}
// --- Delete ---
async function deleteMod(name) {
await db.prepare("DELETE FROM Mods WHERE name = ?", [name]);
return;
}
async function deleteVersion(name, version_number, game_version, platform, environment) {
await db.prepare(`DELETE FROM ModVersions WHERE mod = ?
AND version_number = ?
AND game_version = ?
AND platform = ?
AND environment = ?;`,
[name, version_number, game_version, platform, environment]);
return;
}
async function deleteTags(mod, tags) {
// Remove asynchronously
const promises = tags.map(async (tag) => {
db.query(`DELETE FROM ModTags
WHERE mod = ? AND tag = ?;`, [mod, tag]);
});
await Promise.all(promises);
return;
}
// --- Utils ---
async function updateModAttribute(name, attribute, value) {
await db.prepare(`UPDATE Mods SET ${attribute} = ? WHERE name = ?`, [value, name]);
return;
}
async function updateModInfosAttribute(name, attribute, value) {
await db.prepare(`UPDATE ModInfos SET ${attribute} = ? WHERE name = ?`, [value, name]);
return;
}
async function exists(name) {
return db.exists("Mods", "name", name);
}
async function containsVersion(name, version_number, game_version, platform, environment) {
throw new AppError(501, "Not implemented");
// return db.exists("Mods", "name", name);
}
async function containsTag(name, tag) {
throw new AppError(501, "Not implemented");
// return db.exists("Mods", "name", name);
}
// --- Exports ---
module.exports = { getAllMods, getModByName, getModFullInfos,
listVersions, getVersionByNumber, getVersion,
createMod, addVersion, addTags,
updateMod,
deleteMod, deleteVersion, deleteTags,
exists };

View file

125
backend/src/models/user.js Normal file
View file

@ -0,0 +1,125 @@
const { getDatabase } = require('../database/index');
const AppError = require('../utils/appError');
const db = getDatabase();
// --- Get ---
async function getAllUsers() {
return db.query("SELECT username, display_name, email, profile_picture FROM Users");
}
async function getUserByName(name) {
return await db.query("SELECT username, display_name, profile_picture, role FROM Users WHERE username = ?;", [name]);
}
async function getUserByEmail(email) {
return await db.query("SELECT email, username FROM Users WHERE email = ?;", [email]);
}
async function getFullUserInfos(name) {
return await db.query("SELECT username, display_name, email, profile_picture, role, settings FROM Users WHERE username = ?;", [name]);
}
async function getUserPassword(name) {
return await db.query("SELECT username, password FROM Users WHERE username = ?;", [name]);
}
async function exists(name) {
return await db.exists("Users", "username", name);
}
// --- Create ---
async function createUser( username, email, password, displayName, profilePicture, settings ) {
// Create user
await db.prepare(`INSERT INTO Users (username, email, password, display_name, role )
VALUES (?, ?, ?, ?, ? )`,
[username, email, password, displayName, "user"]);
// Handle nullable fields
if (profilePicture) {
await updateUserAttribute(username, "profile_picture", profilePicture);
}
if (settings) {
await updateUserAttribute(username, "settings", settings);
}
return;
}
async function addFavoriteMods(username, favs) {
const promises = favs.map(async (mod) => {
db.query(`INSERT INTO UserFavoriteMods
(username, mod) VALUES (?, ?);`,
[username, mod]);
});
await Promise.all(promises);
return;
}
// --- Update ---
async function updateUser(username, display_name, email, profile_picture, settings) {
if (display_name) {
await updateUserAttribute(username, "display_name", display_name);
}
if (email) {
await updateUserAttribute(username, "email", email);
}
if (profile_picture) {
await updateUserAttribute(username, "profile_picture", profile_picture)
}
if (settings) {
await updateUserAttribute(username, "settings", settings);
}
}
async function updateUserPassword(username, password) {
await db.prepare(`UPDATE Users SET password = ? WHERE username = ?`, [password, username]);
}
async function updateUserAttribute(username, attribute, value) {
await db.prepare(`UPDATE Users SET ${attribute} = ? WHERE username = ?`, [value, username]);
return;
}
// --- Delete ---
async function deleteUser(username) {
await db.prepare("DELETE FROM Users WHERE username = ?", [username]);
return;
}
async function deleteFavoriteMods(username, favs) {
const promises = favs.map(async (mod) => {
db.query(`DELETE FROM UserFavoriteMods
WHERE username = ? AND mod = ?;`, [username, mod]);
});
// Await
await Promise.all(promises);
return;
}
// --- Exports ---
module.exports = { getAllUsers, getUserByName, getUserByEmail, getFullUserInfos, getUserPassword,
createUser, addFavoriteMods,
updateUser,
deleteUser, deleteFavoriteMods,
exists }

View file

@ -0,0 +1,11 @@
const express = require("express");
const controller = require("../controllers/index");
const router = express.Router();
router.get('/version', async (res, req) => {
controller.getVersion(res, req);
});
module.exports = router;

View file

@ -0,0 +1,24 @@
const express = require("express");
const { listMods } = require("../controllers/mods");
// const { listModpacks } = require("../controllers/modpacks");
const { listUsers } = require("../controllers/users");
const router = express.Router();
// List mods
router.get("/mods", async (req,res) => {
listMods(req, res);
});
// List modpacks
// router.get("/modpacks", async (req,res) => {
// listModpacks(req, res);
// });
// List users
router.get("/users", async (req,res) => {
listUsers(req, res);
});
module.exports = router;

View file

@ -0,0 +1,11 @@
const express = require("express");
const controller = require("../controllers/auth");
const router = express.Router();
// Login
router.post("/", async (req, res) => {
controller.login(req, res);
});
module.exports = router;

View file

View file

@ -0,0 +1,27 @@
const express = require("express");
const controller = require("../controllers/mods");
const router = express.Router();
// Create a mod
router.post("/", async (req, res) => {
controller.createMod(req, res);
});
// Modify mod
router.put("/:name", async (req,res) => {
controller.modifyMod(req,res);
});
// Get mod infos
router.get("/:name", async (req,res) => {
controller.getModByName(req, res);
});
// Delete mod
router.delete("/:name", async (req,res) => {
controller.deleteMod(req, res);
});
module.exports = router;

View file

@ -0,0 +1,27 @@
const express = require("express");
const controller = require("../controllers/users");
const router = express.Router();
// List users
router.get("/", async (req,res) => {
controller.listUsers(req,res);
});
// Create a user
router.post("/", async (req, res) => {
controller.createUser(req, res);
})
// Get user infos
router.get("/:name", async (req,res) => {
controller.getUserByName(req, res);
});
// Delete user
router.delete("/:name", async (req,res) => {
controller.deleteUser(req, res);
});
module.exports = router;

View file

@ -0,0 +1,21 @@
const Ajv = require("ajv");
const ajv = new Ajv();
// --- Schemas ---
const AuthUserSchema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 3, maxLength: 30 },
},
required: ['email', 'password'],
additionalProperties: false
};
const validateAuthUserData = ajv.compile(AuthUserSchema);
// --- Exports ---
module.exports = { validateAuthUserData, validateAuthNodeData };

View file

@ -0,0 +1,21 @@
const Ajv = require("ajv");
const ajv = new Ajv();
// --- Schemas ---
//TODO
const newModSchema = {
type: 'object',
properties: {
name: { type: 'string'},
},
required: ['name'],
additionalProperties: false
};
const validateNewModData = ajv.compile(newModSchema);
// --- Exports ---
module.exports = { validateNewModData };

View file

@ -0,0 +1,21 @@
const Ajv = require("ajv");
const ajv = new Ajv();
// --- Schemas ---
//TODO
const newModpackSchema = {
type: 'object',
properties: {
name: { type: 'string'},
},
required: ['name'],
additionalProperties: false
};
const validateNewModpackData = ajv.compile(newModpackSchema);
// --- Exports ---
module.exports = { validateNewModpackData };

View file

@ -0,0 +1,23 @@
const Ajv = require("ajv");
const ajv = new Ajv();
// --- Schemas ---
//TODO
const newUserSchema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string' },
password: { type: 'string', minLength: 3, maxLength: 30 },
},
required: ['name', 'email', 'password'],
additionalProperties: false
};
const validateNewUserData = ajv.compile(newUserSchema);
// --- Exports ---
module.exports = { validateNewUserData };

View file

@ -0,0 +1,85 @@
const userModel = require("../models/user");
const AppError = require("../utils/appError");
const cryptoUtils = require("../utils/crypto");
const configManager = require("../utils/configManager");
const validate = require("../utils/validate_legacy");
const JWT_Secret = configManager.getJWTSecret();
async function login(username, email, password) {
// Check for null
if (!(username || email) || !password) {
throw new AppError(400, "Bad request", "missing credentials");
}
// Get user data
let user_get;
if (email) { // If matches email
user_get = await userModel.getUserByEmail(email);
} else
if (username) { // if matches username
user_get = await userModel.getUserByName(username);
} else {
console.debug("Failed finding user, weird...")
throw new AppError(401, "Unauthorized", "Invalid credentials");
}
// Check if user exists
if (!user_get || user_get.length == 0) {
// throw new AppError(401, "Unauthorized: No user with this name");
throw new AppError(401, "Unauthorized", "Invalid credentials");
}
// Just in case
if (user_get.length > 1) {
throw new AppError(500, "Internal server error", "Found multiple users with this name or email, please contact administration");
}
const user = user_get[0];
// Get user password
const saved_password_get = await userModel.getUserPassword(user.username);
// Check if retrieved password sucessfully
if (!saved_password_get || saved_password_get.length == 0) {
throw new AppError(500, "Unable to retrieve user password");
}
saved_password = saved_password_get[0].password;
// Check if retrieved password sucessfully again
if (!saved_password) {
throw new AppError(500, "Unable to retrieve user password");
}
// Check if passwords match
const passwords_match = await cryptoUtils.passwordsMatch(password, saved_password)
if (!passwords_match) {
// throw new AppError(401, "Unauthorized: Invalid password");
console.debug(password, "differs from", saved_password);
throw new AppError(401, "Unauthorized", "Invalid credentials");
}
const payload = { type: "user",
username: user.username,
email: user.email,
role: user.role };
const token = await cryptoUtils.signToken(payload);
return token;
// // Check if passwords match
// const passwords_match = await bcrypt.compare(password, user[0].password);
// if (!passwords_match) {
// // throw new AppError(401, "Unauthorized: Invalid password");
// console.debug("Password doesn't match")
// throw new AppError(401, "Unauthorized", "Invalid credentials");
// }
// return jwt.sign({ username: user[0].username, role: user[0].role }, await JWT_Secret);
}
// function authorizeRole(user, roles) {
// if (!user || !roles.includes(user.role)) {
// throw new AppError(401, "Unauthorized: You don't have the necessary permissions to access this resource");
// }
// }
module.exports = { login };

View file

@ -0,0 +1,7 @@
const model = require("../models/index");
async function getVersion() {
return model.getVersion();
}
module.exports = { getVersion }

View file

@ -0,0 +1,164 @@
const model = require("../models/mod");
const AppError = require("../utils/appError");
const { validateModData } = require("../utils/validate_legacy");
const { mdToHtml } = require("../utils/convert");
const { sanitizeModData } = require("../utils/sanitize");
// --- Get ---
async function getAllMods() {
return model.getAllMods();
}
async function getModByName(name) {
const res = model.getModByName(name);
if (res.length == 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found");
}
return res[0];
}
async function getFullModInfos(name) {
const res = model.getFullModInfos(name);
if (res.length == 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found");
}
return res[0];
}
async function getModVersion(infos) {
const { mod, version_number, game_version, platform, environment} = infos;
const res = model.getModVersion(mod, version_number, game_version, platform, environment);
if (res.length == 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found");
}
return res[0];
}
// --- Create ---
async function createMod(mod_data, author) {
// Check body validity
//TODO
console.warn("Skipping validity checks for createMod");
// await validateModData(mod_data);
// Generate data
const { name, display_name, description, mod_infos } = mod_data;
mod_infos.full_description = await mdToHtml(mod_infos.full_description); // Convert
await sanitizeModData(mod_data); // Sanitize
//TODO
mod_infos.creation_date = 0
// Write changes to database
await model.createMod(name, display_name, author, description, mod_infos);
// Return
return getModByName(name);
}
async function addVersion(version_data) {
// Validate
//TODO
console.warn("Skipping validity checks for addVersion");
// Generate data
const { mod_name, version_number, channel, changelog, game_version,
platform, environment, url } = version_data; // Split
changelog = await mdToHtml(changelog); // Convert
await sanitizeModData(mod_data); // Sanitize
const release_date = (new Date()).toLocaleDateString();
// Write changes
await model.addVersion(mod_name, version_number, channel, changelog,
release_date, game_version, platform, environment, url); // Database
// Return
return await model.getModVersion(mod_name, version_number, game_version, platform, environment );
}
async function addTags(mod, tags) {
// Validate
//TODO
console.warn("Skipping validity checks for addTags");
// Write changes
await model.addTags(mod, tags);
// Return
const { tags:res } = await model.getFullModInfos(mod);
return { "mod": mod, "tags": res};
}
// --- Update ---
async function updateMod(diff_data) {
//TODO
throw new AppError(501, "Not implemented");
}
// Delete
async function deleteMod(name) {
// Check existence
const mod = await model.getModByName(name);
if (!mod) {
throw new AppError(404, "No was found with this name", "Not found")
}
// Authorize
// TODO move outside of this function
if (mod.author != mod.user) {
throw new AppError(403, "You don't have the necessary permissions to execute this action", "Forbidden");
}
// Write changes to database
await model.deleteMod(name);
// Return
return mod;
}
async function deleteVersion(version_infos) {
// Validate
// TODO
// Generate data
const res = await getModVersion(version_infos);
const { mod, version_number, game_version, platform, environment} = version_infos;
// Write changes to db
await model.deleteVersion(mod, version_number, game_version, platform, environment);
// Return
return res;
}
async function deleteTags(mod, tags) {
// Validate (check existence)
//TODO
console.warn("Skipping validity checks for deleteTags");
// Wites changes to db
await model.deleteTags(mod, tags);
// Return
const { tags:res } = await model.getFullModInfos(mod);
return { "mod": mod, "tags": res};
}
module.exports = { getAllMods, getModByName, getFullModInfos,
createMod, addTags, addVersion,
updateMod,
deleteMod, deleteTags, deleteVersion };

View file

View file

@ -0,0 +1,46 @@
const model = require("../models/user");
const AppError = require("../utils/appError");
const cryptoUtils = require("../utils/crypto");
const { validateUserData } = require("../utils/validate_legacy");
const { sanitizeUserData } = require("../utils/sanitize");
async function getAllUsers() {
return await model.getAllUsers();
}
async function getUserByName(name) {
const res = await model.getUserByName(name);
return res[0];
}
async function createUser(user_data) {
// Check body validity
// TODO
// Sanitize
// TODO
// Gather data
const { username, email, password, display_name, profile_picture, settings } = user_data
const password_hash = await cryptoUtils.hashPassword(password);
await model.createUser(username, email, password_hash, display_name, null, null);
return model.getUserByName(username);
}
async function deleteUser(name, token_user) {
// Check existence
const exists = await model.exists(name);
if (!exists) {
throw new AppError(404, "Cannot find user with this name", "Not found");
}
const res = await model.getUserByName(name);
await model.deleteUser(name);
return res;
}
module.exports = { getAllUsers, getUserByName, createUser, deleteUser };

View file

@ -0,0 +1,27 @@
class AppError extends Error {
constructor(statusCode, message, status = "", debugMsg = "") {
super(message);
this.statusCode = statusCode;
this.debugMsg = debugMsg;
// Get status
if (status === "") {
if (statusCode.toString().startsWith("4")) {
this.status = "Fail";
} else {
this.status = "Error";
}
} else {
this.status = status;
}
}
}
exports.tryCatch = (controller) => async (req, res, next) => {
try {
await controller(req, res, next);
} catch(err) {
next(err);
}
}
module.exports = AppError;

View file

@ -0,0 +1,89 @@
// --- Define constants ---
// Imports
const fs = require("fs");
const path = require("path");
const { version } = require("../../package.json");
// Var decalaration
const config_folder = "config";
const config_file_name = "config.json"
// Global variables
let config = {};
// --- Default config ---
const default_config = {
"port": 8000,
"users": {
"admin": {
"username": "admin",
"password": "admin"
}
},
"database": {
"type": "sqlite"
},
"auth" : {
"JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY",
"tokenExpiry": "1h"
}
}
// --- Functions ---
function loadConfig() {
let user_config;
// Parse
try {
// Get user config
user_config = JSON.parse(fs.readFileSync(path.resolve(path.join(config_folder, config_file_name))));
// Warns
if (!user_config.auth || !user_config.auth.JWT_secret) {
console.warn("WARNING: No JWT secret provided, using the default one. Please note that using the default secret is a major security risk.")
}
// Merge default and user configs (default values)
config = { ...default_config, ...user_config };
}
catch (err) {
// Error messages
console.debug("Error:", err)
console.error("Error loading configuration, using the default settings");
console.debug("Search path:", path.resolve("./"));
console.debug("Config file:", path.resolve(path.join(config_folder, config_file_name)))
config = default_config;
}
return config;
}
async function getConfig() {
return config;
}
async function getJWTSecret() {
return config.auth.JWT_secret || process.env.JWT_secret;
}
async function getVersion() {
// Could be done with process.env.npm_package_version
// but may not work without without npm
return version;
}
// Exports
module.exports = { loadConfig, getConfig, getVersion, getJWTSecret };

View file

@ -0,0 +1,11 @@
const marked = require("marked");
async function mdToHtml(md_content) {
if (md_content) {
return marked.parse(md_content);
} else {
return "";
}
}
module.exports = { mdToHtml };

View file

@ -0,0 +1,61 @@
// --- Imports ---
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const { getConfig, getJWTSecret } = require("./configManager");
// --- Config ---
// Declarations
let JWT_Secret;
let token_expiry;
// Constant values
const saltRounds = 12;
// Load
(async () => {
const config = await getConfig();
JWT_Secret = await getJWTSecret();
token_expiry = config.auth.tokenExpiry;
signature_algorithm = config.auth.signatureAlgorithm;
})();
// --- Functions ---
async function hashPassword(passwd) {
const hash = bcrypt.hashSync(passwd, saltRounds);
return hash;
}
async function passwordsMatch(password, hashed_password) {
return await bcrypt.compare(password, hashed_password);
}
async function signToken(payload, options = null) {
if (options == null) {
return jwt.sign(payload, JWT_Secret, { expiresIn: token_expiry, });
}
else {
return jwt.sign(payload, JWT_Secret, options);
}
}
function verifyToken(token) {
return new Promise( async (resolve, reject) => {
await jwt.verify( token, JWT_Secret, (err, user) => {
if (err) {
reject(err);
} else {
resolve(user);
}
});
});
}
// --- Exports ---
module.exports = { passwordsMatch, hashPassword, verifyToken, signToken };

View file

@ -0,0 +1,19 @@
const createDOMPurify = require("dompurify");
const { JSDOM } = require("jsdom");
// Initialize
const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window);
async function sanitizeText(text) {
return DOMPurify.sanitize(text);
}
async function sanitizeModData(mod_data) {
console.warn("Skipping sanitanization (not implemented)");
// mod_data.displayName = await sanitizeText(mod_data.displayName);
// mod_data.otherInfos.description = await sanitizeText(mod_data.otherInfos.description);
// mod_data.otherInfos.changelogs = await sanitizeText(mod_data.otherInfos.changelogs);
}
module.exports = { sanitizeText, sanitizeModData };

View file

@ -0,0 +1,51 @@
// --- Imports ---
const AppError = require("./appError");
// --- Functions ---
async function validateNewModData(mod_data) {
throw new AppError(501, "Not implemented");
//TODO
// try {
// node_schemas.validateNewModData(node_data);
// } catch (err) {
// throw new AppError(400, "Missing or invalid fields", "Bad request", err);
// }
}
async function validateNewUserData(user_data) {
throw new AppError(501, "Not implemented");
//TODO
// try {
// node_schemas.validateNewUserData(node_data);
// } catch (err) {
// throw new AppError(400, "Missing or invalid fields", "Bad request", err);
// }
}
async function validateCretendials(identifier, password) {
throw new AppError(501, "Not implemented");
}
// --- Utils ---
async function isEmail(text) {
const email_regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
return email_regex.test(text);
}
async function isID(text) {
const id_regex = /[a-zA-Z0-9_]+/;
return id_regex.test(text);
}
module.exports = { validateNewModData, validateNewUserData, isEmail, isID };

View file

@ -0,0 +1,98 @@
const mod_model = require("../models/mod");
const user_model = require("../models/user");
const AppError = require("./appError");
async function validateModData(mod_data) {
//TODO WIP
// Check fields existence
const not_null = mod_data &&
Object.keys(mod_data).length == 5 &&
mod_data.name &&
mod_data.displayName &&
mod_data.author &&
mod_data.versions != null;
// mod_data.otherInfos != null &&
// Object.keys(mod_data.otherInfos).length == 0 &&
// mod_data.otherInfos.description != null &&
// mod_data.otherInfos.links != null &&
// mod_data.otherInfos.tags != null &&
// mod_data.otherInfos.screenshots != null &&
// mod_data.otherInfos.license != null &&
// mod_data.otherInfos.changelogs != null;
if (!not_null) {
console.debug("Item is missing expected fields:", mod_data);
throw new AppError(400, "Bad request", "Missing expected fields");
}
// Check fields format (check if sanitized)
const is_valid_name = /^[a-zA-Z0-9_]+$/.test(mod_data.name);
const is_valid_displayName = true;
// const is_valid_displayName = /^[a-zA-Z0-9_]+$/.test(mod_data.name); // Temporary
// const
const is_valid = is_valid_name && is_valid_displayName;
if (!is_valid) {
console.debug("Fields are not following the expected formats");
throw new AppError(400, "Bad request", "The provided fields don't match the expected format");
}
// Check if mod already exists
const exists = await mod_model.exists(mod_data.name);
if (exists) {
console.debug("Error: Item already exists");
throw new AppError(403, "Forbidden", "Content with this name already exists");
}
}
async function validateUserData(user_data) {
throw new AppError(501, "Not implemented");
//TODO
// Check fields existence
// ...
if (!not_null) {
console.debug("Missing expected fields:", mod_data);
throw new AppError(400, "Bad request: Missing expected fields");
}
// Check fields format (check if sanitized)
const is_valid_username = /^[a-zA-Z0-9_]+$/.test(user_data.username);
// const is_valid_email = ...
// ...
const is_valid = is_valid_username && is_valid_email;
if (!is_valid) {
console.debug("Fields are not following the expected formats");
throw new AppError(400, "Bad request: The provided fields don't match the expected format");
}
// Check if user already exists
const exists = await user_model.exists(user_data.username);
if (exists) {
console.debug("Error: User already exists");
throw new AppError(403, "Forbidden: User with this name already exists");
}
}
async function validateCretendials(identifier, password) {
throw new AppError(501, "Not implemented");
}
async function isEmail(text) {
const email_regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
return email_regex.test(text);
}
async function isID(text) {
const id_regex = /[a-zA-Z0-9_]+/;
return id_regex.test(text);
}
module.exports = { validateModData, validateUserData, isEmail, isID };