From 8ba34f78a9e24d20756a34a90544a76b67ba80ec Mon Sep 17 00:00:00 2001 From: RHM Date: Thu, 11 Sep 2025 20:13:45 +0200 Subject: [PATCH] better documentation --- backend/package-lock.json | 95 ++++++++++ backend/package.json | 2 + backend/src/api.js | 360 ++++++++++++++++++++------------------ backend/src/db.js | 4 +- backend/src/index.js | 24 ++- 5 files changed, 310 insertions(+), 175 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 295a334..5fbdbf7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,8 @@ "cors": "^2.8.5", "dotenv": "^17.2.2", "express": "^5.1.0", + "joi": "^18.0.1", + "joi-to-swagger": "^6.2.0", "mysql2": "^3.14.5", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" @@ -64,6 +66,54 @@ "openapi-types": ">=7" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", + "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -77,6 +127,12 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -855,6 +911,39 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/joi-to-swagger": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/joi-to-swagger/-/joi-to-swagger-6.2.0.tgz", + "integrity": "sha512-gwfIr1TsbbvZWozB/sFqiD7POFcXeaLKp6QJKGFkVgdom2ie/4f75QQAanZc/Wlbnyk66e6kTZXO28i6pN3oQA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "joi": ">=17.1.1" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -867,6 +956,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", diff --git a/backend/package.json b/backend/package.json index ba2acd3..e9c0bdd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,8 @@ "cors": "^2.8.5", "dotenv": "^17.2.2", "express": "^5.1.0", + "joi": "^18.0.1", + "joi-to-swagger": "^6.2.0", "mysql2": "^3.14.5", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" diff --git a/backend/src/api.js b/backend/src/api.js index 22436c5..b0d5cbb 100644 --- a/backend/src/api.js +++ b/backend/src/api.js @@ -1,7 +1,69 @@ import { Router } from "express"; import { pool } from './db.js'; +import Joi from 'joi'; +import j2s from "joi-to-swagger"; + +export class ApiError extends Error { + constructor(status, message) { + super(message); + this.status = status; + } +} const router = Router(); +const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + +const userSchema = Joi.object({ + username: Joi.string().min(3).max(32).required() +}); + +const createGameSchema = Joi.object({ + user: Joi.number().integer().positive().required(), + is_local: Joi.boolean().default(false) +}); + +const addPlayerSchema = Joi.object({ + user: Joi.number().integer().positive().required() +}); + +const lockGameSchema = Joi.object({ + user: Joi.number().integer().positive().required() +}); + +const turnSchema = Joi.object({ + user: Joi.number().integer().positive().required(), + round_number: Joi.number().integer().min(1).required(), + start_points: Joi.number().integer().min(0).required(), + first_throw: Joi.number().integer().min(0).required(), + second_throw: Joi.number().integer().min(0).required(), + third_throw: Joi.number().integer().min(0).required(), + end_points: Joi.number().integer().min(0).required() +}); + +const schemasByPath = { + "/api/users": userSchema, + "/api/games": createGameSchema, + "/api/games/:id/players": addPlayerSchema, + "/api/games/:id/lock": lockGameSchema, + "/api/games/:id/turns": turnSchema +}; + +export function generateRequestBodies() { + const requestBodies = {}; + + for (const path in schemasByPath) { + const { swagger } = j2s(schemasByPath[path]); + requestBodies[path] = { + content: { + "application/json": { + schema: swagger + } + } + }; + } + + return requestBodies; +} /** * @swagger @@ -24,32 +86,25 @@ router.get("/healthcheck", (req, res) => { * @swagger * /api/users: * post: - * summary: Create a new user or return old user if username already exists - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * username: - * type: string - * example: - * username: "john_doe" + * summary: Create a new user or return existing user if username already exists * responses: - * 201: - * description: User created + * 200: + * description: User created or existing user returned * content: * application/json: * example: * id: 1 * username: "john_doe" + * 400: + * description: Validation error */ -router.post("/users", async (req, res) => { - const { username} = req.body; +router.post("/users", asyncHandler(async (req, res) => { + const { error, value } = userSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + const { username } = value; const [result] = await pool.query( - `INSERT INTO users (username) + `INSERT INTO users (username) VALUES (?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`, [username] @@ -57,7 +112,7 @@ router.post("/users", async (req, res) => { const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]); res.status(200).json(userRow[0]); -}); +})); /** * @swagger @@ -104,31 +159,20 @@ router.get("/games", async (req, res) => { * current_playing_user: 1 * turn_order: null */ -router.get("/games/:id", async (req, res) => { - const [rows] = await pool.query("SELECT * FROM current_games WHERE id = ?", [req.params.id]); - res.json(rows[0] || null); -}); +router.get("/games/:id", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + + const [rows] = await pool.query("SELECT * FROM current_games WHERE id = ?", [gameId]); + if (!rows.length) throw new ApiError(404, "Game not found"); + res.json(rows[0]); +})); /** * @swagger * /api/games: * post: * summary: Create a new game - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: integer - * is_local: - * type: boolean - * default: false - * example: - * user: 1 - * is_local: false * responses: * 201: * description: Game created @@ -145,13 +189,15 @@ router.get("/games/:id", async (req, res) => { * - id: 1 * username: "john_doe" */ -router.post("/games", async (req, res) => { - const { user, is_local = false } = req.body; +router.post("/games", asyncHandler(async (req, res) => { + const { error, value } = createGameSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + + const { user, is_local } = value; const conn = await pool.getConnection(); try { await conn.beginTransaction(); - const [gameResult] = await conn.query( "INSERT INTO current_games (is_open, is_local, current_playing_user) VALUES (?, ?, ?)", [true, is_local, user] @@ -159,7 +205,10 @@ router.post("/games", async (req, res) => { const gameId = gameResult.insertId; - await conn.query("INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)", [gameId, user, true]); + await conn.query( + "INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)", + [gameId, user, true] + ); const [gameRow] = await conn.query("SELECT * FROM current_games WHERE id = ?", [gameId]); const [players] = await conn.query( @@ -173,11 +222,11 @@ router.post("/games", async (req, res) => { res.status(201).json({ game: gameRow[0], players }); } catch (err) { await conn.rollback(); - throw err; + throw new ApiError(500, "Failed to create game"); } finally { conn.release(); } -}); +})); /** * @swagger @@ -190,17 +239,6 @@ router.post("/games", async (req, res) => { * required: true * schema: * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: integer - * example: - * user: 2 * responses: * 201: * description: Player added @@ -214,22 +252,24 @@ router.post("/games", async (req, res) => { * - id: 2 * username: "jane_doe" */ -router.post("/games/:id/players", async (req, res) => { - const { user } = req.body; - const gameId = req.params.id; +router.post("/games/:id/players", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + const { error, value } = addPlayerSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + + const { user } = value; const conn = await pool.getConnection(); + try { await conn.beginTransaction(); - const [gameRows] = await conn.query( "SELECT * FROM current_games WHERE id = ? AND is_open = TRUE AND is_local = FALSE", [gameId] ); - if (gameRows.length === 0) { - return res.status(400).json({ error: "Game is not open or is local" }); - } + if (!gameRows.length) throw new ApiError(400, "Game is not open or is local"); await conn.query( "INSERT IGNORE INTO game_players (game, user, is_creator) VALUES (?, ?, ?)", @@ -247,11 +287,11 @@ router.post("/games/:id/players", async (req, res) => { res.status(201).json({ game: gameId, players }); } catch (err) { await conn.rollback(); - throw err; + throw new ApiError(500, "Failed to add player"); } finally { conn.release(); } -}); +})); /** * @swagger @@ -264,17 +304,6 @@ router.post("/games/:id/players", async (req, res) => { * required: true * schema: * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: integer - * example: - * user: 1 * responses: * 200: * description: Game locked with turn order @@ -283,26 +312,35 @@ router.post("/games/:id/players", async (req, res) => { * example: * message: "Game locked" * turnOrder: [1,2,3] + * 400: + * description: Invalid game ID / Validation error + * 403: + * description: Only the creator can lock the game + * 500: + * description: Failed to lock game */ -router.patch("/games/:id/lock", async (req, res) => { - const { user } = req.body; - const gameId = req.params.id; +router.patch("/games/:id/lock", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + const { error, value } = lockGameSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + + const { user } = value; const conn = await pool.getConnection(); + try { await conn.beginTransaction(); - const [creatorRows] = await pool.query( + const [creatorRows] = await conn.query( "SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE", [gameId, user] ); - if (creatorRows.length === 0) { - return res.status(403).json({ error: "Only the creator can lock the game" }); - } + if (!creatorRows.length) throw new ApiError(403, "Only the creator can lock the game"); const [players] = await conn.query( - `SELECT user FROM game_players WHERE game = ?`, + "SELECT user FROM game_players WHERE game = ?", [gameId] ); @@ -313,7 +351,7 @@ router.patch("/games/:id/lock", async (req, res) => { } await conn.query( - `UPDATE current_games SET is_open = FALSE, turn_order = ? WHERE id = ?`, + "UPDATE current_games SET is_open = FALSE, turn_order = ? WHERE id = ?", [JSON.stringify(turnOrder), gameId] ); @@ -321,11 +359,11 @@ router.patch("/games/:id/lock", async (req, res) => { res.json({ message: "Game locked", turnOrder }); } catch (err) { await conn.rollback(); - throw err; + throw new ApiError(500, "Failed to lock game"); } finally { conn.release(); } -}); +})); /** * @swagger @@ -349,50 +387,18 @@ router.patch("/games/:id/lock", async (req, res) => { * - id: 2 * username: "jane_doe" */ -router.get("/games/:id/players", async (req, res) => { +router.get("/games/:id/players", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + const [players] = await pool.query( `SELECT u.id, u.username FROM game_players gp JOIN users u ON gp.user = u.id WHERE gp.game = ?`, - [req.params.id] + [gameId] ); res.json(players); -}); - -/** - * @swagger - * /api/games/{id}/turns: - * get: - * summary: Get all turns for a game - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: List of turns - * content: - * application/json: - * example: - * - id: 1 - * game: 1 - * user: 1 - * round_number: 1 - * start_points: 501 - * first_throw: 60 - * second_throw: 45 - * third_throw: 36 - * end_points: 360 - */ -router.get("/games/:id/turns", async (req, res) => { - const [rows] = await pool.query( - "SELECT * FROM turns WHERE game = ? ORDER BY round_number", - [req.params.id] - ); - res.json(rows); -}); +})); /** * @swagger @@ -405,35 +411,6 @@ router.get("/games/:id/turns", async (req, res) => { * required: true * schema: * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * user: - * type: integer - * round_number: - * type: integer - * start_points: - * type: integer - * first_throw: - * type: integer - * second_throw: - * type: integer - * third_throw: - * type: integer - * end_points: - * type: integer - * example: - * user: 1 - * round_number: 1 - * start_points: 501 - * first_throw: 60 - * second_throw: 45 - * third_throw: 36 - * end_points: 360 * responses: * 201: * description: Turn recorded @@ -442,10 +419,21 @@ router.get("/games/:id/turns", async (req, res) => { * example: * turnId: 1 * nextPlayer: 2 + * 400: + * description: Validation error / Invalid game ID + * 403: + * description: Not your turn + * 500: + * description: Failed to record turn */ -router.post("/games/:id/turns", async (req, res) => { - const { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = req.body; - const gameId = req.params.id; +router.post("/games/:id/turns", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + + const { error, value } = turnSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + + const { user, round_number, start_points, first_throw, second_throw, third_throw, end_points } = value; const conn = await pool.getConnection(); try { @@ -456,16 +444,12 @@ router.post("/games/:id/turns", async (req, res) => { [gameId] ); - if (gameRows.length === 0) { - return res.status(404).json({ error: "Game not found" }); - } + if (!gameRows.length) throw new ApiError(404, "Game not found"); const game = gameRows[0]; const turnOrder = JSON.parse(game.turn_order || '[]'); - if (game.current_playing_user !== user) { - return res.status(403).json({ error: "Not your turn" }); - } + if (game.current_playing_user !== user) throw new ApiError(403, "Not your turn"); const [result] = await conn.query( `INSERT INTO turns @@ -484,18 +468,45 @@ router.post("/games/:id/turns", async (req, res) => { ); await conn.commit(); - - res.status(201).json({ - turnId: result.insertId, - nextPlayer - }); + res.status(201).json({ turnId: result.insertId, nextPlayer }); } catch (err) { await conn.rollback(); - throw err; + throw new ApiError(500, "Failed to record turn"); } finally { conn.release(); } -}); +})); + +/** + * @swagger + * /api/games/{id}/turns: + * get: + * summary: Get turns of a game + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer + * responses: + * 201: + * description: The turn + * content: + * application/json: + * example: + * turnId: 1 + * nextPlayer: 2 + */ +router.get("/games/:id/turns", asyncHandler(async (req, res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + + const [rows] = await pool.query( + "SELECT * FROM turns WHERE game = ? ORDER BY round_number", + [gameId] + ); + res.json(rows); +})); /** * @swagger @@ -524,9 +535,14 @@ router.post("/games/:id/turns", async (req, res) => { * third_throw: 36 * end_points: 360 */ -router.get("/turns/:id", async (req, res) => { - const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [req.params.id]); - res.json(rows[0] || null); -}); +router.get("/turns/:id", asyncHandler(async (req, res) => { + const turnId = parseInt(req.params.id, 10); + if (isNaN(turnId)) throw new ApiError(400, "Invalid turn ID"); + + const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [turnId]); + if (!rows.length) throw new ApiError(404, "Turn not found"); + + res.json(rows[0]); +})); export default router; diff --git a/backend/src/db.js b/backend/src/db.js index d65ce7c..a5a3111 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -23,9 +23,9 @@ const migrations = [ await conn.query(` CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(32) NOT NULL UNIQUE + username VARCHAR(64) NOT NULL UNIQUE ) - `); + `); // max length is double the actual max length cause of unicode stuff await conn.query(` CREATE TABLE games ( diff --git a/backend/src/index.js b/backend/src/index.js index 6ca2ed7..2bb2400 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -5,7 +5,7 @@ import https from 'https'; import express from 'express'; import cors from 'cors'; import path from 'path'; -import apiRouter from './api.js'; +import apiRouter, { ApiError, generateRequestBodies } from './api.js'; import { initDB } from './db.js'; import { fileURLToPath } from 'url'; @@ -13,6 +13,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.join(path.dirname(__filename), ".."); // going back a dir cuz code is in src/ const app = express(); + const specs = swaggerJsdoc({ definition: { openapi: "3.0.0", @@ -23,6 +24,17 @@ const specs = swaggerJsdoc({ }, apis: ["./src/api.js"], }); +for (const [path, schema] of Object.entries(generateRequestBodies())) { + const swaggerPath = path.replace(/:(\w+)/g, '{$1}'); + if (!specs.paths[swaggerPath]) continue; + + const methodKeys = Object.keys(specs.paths[swaggerPath]); + methodKeys.forEach(method => { + if (["post", "put", "patch", "delete"].includes(method)) { + specs.paths[swaggerPath][method].requestBody = schema; + } + }); +} initDB(); @@ -35,6 +47,16 @@ app.use(express.static(path.join(__dirname, "public"), { app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(specs)); app.use("/api", apiRouter); +// has to be last +app.use((err, _req, res, _next) => { + if (err instanceof ApiError) { + return res.status(err.status).json({ status: "error", error: err.message }); + } + + console.error(err); + res.status(500).json({ status: "error", error: "Internal server error" }) +}) + const server = https.createServer({ key: fs.readFileSync('server.key'), cert: fs.readFileSync('server.cert')