import crypto from 'crypto' import bodyParser from 'body-parser' import cookieParser from 'cookie-parser' import express, { Request, Router } from 'express' import { StatusCodes } from 'http-status-codes' import { createDatabase, createProblem, createSolution, deleteProblem, deleteSolution, getProblem, getProblems, getSolution, getSolutions, getUser, getUsers, updateProblem, updateSolution, } from './db/database' import { isAdministrator, isStudent, Opaque, Problem as ProblemModel, ProblemId, Solution as SolutionModel, SolutionId, SolutionStat, User as UserModel, UserId, } from '../shared/model' import { initialDatabaseValue } from './db/example-data' import { validateObjectKeys } from '../shared/utils' import { setupOAuth } from './auth' type SessionId = Opaque export interface SessionService { createSession(userId: UserId): SessionId getUserForSession(sid: SessionId): UserId | null } export async function createApiRouter() { const sessionStore: Record = {} const sessions: SessionService = { createSession(userId: UserId) { const sid = crypto.randomBytes(10).toString('hex') as SessionId sessionStore[sid] = userId return sid }, getUserForSession(sid: SessionId) { return sessionStore[sid] ?? null }, } const db = createDatabase(process.env.DATABASE_PATH ?? './db.local.json', initialDatabaseValue) async function getRequestUser(req: Request): Promise { const userId = sessions.getUserForSession(req.cookies.sid) if (!userId) { return null } return await getUser(db, userId) } const r: Router = express.Router() r.use(bodyParser.json()) r.use(cookieParser()) const authRouter: Router = express.Router() setupOAuth(authRouter, db, sessions) r.use('/auth', authRouter) r.get('/api/status', (req, res) => { res.json({ url: req.originalUrl, status: 'ok' }) }) r.get('/api/current-user', async (req, res) => { res.json(await getRequestUser(req)) }) // r.post('/api/login', async (req, res) => { // const { id, name } = req.body // const user = await getUser(db, id) // if (!user) { // res.sendStatus(StatusCodes.FORBIDDEN) // return // } // res.cookie('sid', sessions.createSession(id), { maxAge: 1000 * 60 * 60 * 24 * 7 }) // res.json({ status: 'ok' }) // }) r.post('/api/logout', (req, res) => { res.cookie('sid', '', { expires: new Date() }) res.json({ status: 'ok' }) }) r.get('/api/users', async (req, res) => { const requestUser = await getRequestUser(req) if (!requestUser || !isAdministrator(requestUser.role)) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } const users = await getUsers(db) res.json(users) }) r.get('/api/problems', async (req, res) => { type ProblemWithSolutionsCount = ProblemModel & { solutionsCount?: number } const problems: ProblemWithSolutionsCount[] = await getProblems(db) const solutions = await getSolutions(db) const solutionCounts: Record = {} for (const s of solutions) { solutionCounts[s.forProblem] ||= 0 solutionCounts[s.forProblem]++ } for (const p of problems) { p.solutionsCount = solutionCounts[p.id] || 0 } res.json(problems) }) r.get('/api/problem/:id', async (req, res) => { res.json(await getProblem(db, req.params.id)) }) r.patch('/api/problem/:id', async (req, res) => { const id = req.params.id as ProblemId const user = await getRequestUser(req) // l'utente deve essere loggato if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } // l'utente deve essere un admin o un moderatore if (!isAdministrator(user.role)) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } // uno studente può modificare solo il campo "content" if (user.role === 'moderator' && !validateObjectKeys(req.body, ['content'])) { res.status(StatusCodes.UNAUTHORIZED) res.send(`a moderator can only modify the field "content"`) return } await updateProblem(db, id, req.body) res.send({ status: 'ok' }) }) r.delete('/api/problem/:id', async (req, res) => { const id = req.params.id as ProblemId const user = await getRequestUser(req) if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } if (user.role !== 'admin') { res.status(StatusCodes.UNAUTHORIZED) res.send(`only an admin can delete this entity`) return } await deleteProblem(db, id) res.send({ status: 'ok' }) }) r.post('/api/problem', async (req, res) => { const user = await getRequestUser(req) // l'utente deve essere loggato if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } // Solo un amministratore può inviare nuovi problemi if (user.role !== 'admin' && user.role !== 'moderator') { res.sendStatus(StatusCodes.UNAUTHORIZED) return } // Il contenuto del problema deve essere non vuoto if (req.body.content.trim().length === 0) { res.sendStatus(StatusCodes.UNPROCESSABLE_ENTITY) return } const problemNextId = (await getProblems(db)).map(p => parseInt(p.id)).reduce((acc, v) => Math.max(acc, v), 0) + 1 const id = await createProblem(db, { title: req.body.title ?? `Problema ${problemNextId}`, content: req.body.content, createdBy: user.id, }) res.json(id) }) // questa route ritorna la lista di utenti con associato numero di soluzioni inviate, corrette. r.get('/api/stats', async (req, res) => { let queryUser = (req.query.user ?? null) as UserId | null let solutions = await getSolutions(db) if (queryUser !== null) { solutions = solutions.filter(s => s.sentBy === queryUser) } const stats: Record = {} solutions.forEach(s => { if (!stats[s.sentBy]) { stats[s.sentBy] = { sentSolutionsCount: s.status !== 'pending' ? 1 : 0, correctSolutionsCount: s.status === 'correct' ? 1 : 0, } } else { if (s.status !== 'pending') { stats[s.sentBy].sentSolutionsCount += 1 } if (s.status === 'correct') { stats[s.sentBy].correctSolutionsCount += 1 } } }) return res.json(stats) }) r.get('/api/solutions', async (req, res) => { let queryUser = (req.query.user ?? null) as UserId | null let queryProblem = (req.query.problem ?? null) as ProblemId | null let isPublic = req.query.public === '' const requestUser = await getRequestUser(req) let solutions = await getSolutions(db) // se l'utente non è loggato o se non è un amministratore, mostra solo le soluzioni "visibili" if (!requestUser || !isAdministrator(requestUser.role)) { solutions = solutions.filter(s => s.visible || (requestUser && s.sentBy === requestUser.id)) } // filtra rispetto agli utenti if (queryUser !== null) { solutions = solutions.filter(s => s.sentBy === queryUser) } if (isPublic) { solutions = solutions.filter(s => s.visible) } // filtra rispetto ai problemi if (queryProblem !== null) { solutions = solutions.filter(s => s.forProblem === queryProblem) } res.json(solutions) }) r.get('/api/solution/:id', async (req, res) => { const user = await getRequestUser(req) const solution = await getSolution(db, req.params.id as SolutionId) // la soluzione deve esistere if (solution === null) { res.sendStatus(StatusCodes.NOT_FOUND) return } // uno studente può vedere solo soluzioni visibili o proprie soluzione if (!solution.visible && user && isStudent(user.role) && solution.sentBy !== user.id) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } res.json(solution) }) r.post('/api/solution', async (req, res) => { const user = await getRequestUser(req) if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } await createSolution(db, { sentBy: user.id, forProblem: req.body.forProblem, content: req.body.content, }) res.send({ status: 'ok' }) }) r.patch('/api/solution/:id', async (req, res) => { const id = req.params.id as SolutionId const user = await getRequestUser(req) // l'utente deve essere loggato if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } const solution = await getSolution(db, id) // la soluzione deve esistere if (solution === null) { res.sendStatus(404) return } // uno studente non può modificare una soluzione di un altro utente if (user.role === 'student' && solution.sentBy !== user.id) { res.status(StatusCodes.UNAUTHORIZED) res.send(`a student can only modify its own solution`) return } // uno studente può modificare solo il campo "content" if (user.role === 'student' && !validateObjectKeys(req.body, ['content'])) { res.status(StatusCodes.UNAUTHORIZED) res.send(`a student can only modify the field "content"`) return } // un moderatore può modificare solo i campi "content", "visible", "status" if (user.role === 'moderator' && !validateObjectKeys(req.body, ['content', 'status', 'visible'])) { res.status(StatusCodes.UNAUTHORIZED) res.send(`a moderator can only modify the fields "content", "visible", "status"`) return } await updateSolution(db, id, req.body) res.json({ status: 'ok' }) }) r.delete('/api/solution/:id', async (req, res) => { const id = req.params.id as SolutionId const user = await getRequestUser(req) // l'utente deve essere loggato if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } const solution = await getSolution(db, id) // la soluzione deve esistere if (solution === null) { res.sendStatus(StatusCodes.NOT_FOUND) return } // solo un admin può eliminare le soluzioni degli utenti if (user.role !== 'admin' && solution.sentBy !== user.id) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } await deleteSolution(db, id) res.send({ status: 'ok' }) }) r.get('/api/user/:id', async (req, res) => { const user = await getRequestUser(req) // intanto l'utente deve essere loggato if (!user) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } // solo gli amministratori possono usare questa route if (!isAdministrator(user.role)) { res.sendStatus(StatusCodes.UNAUTHORIZED) return } const requestedUser = await getUser(db, req.params.id) // l'utente richiesto magari deve esistere if (!requestedUser) { res.sendStatus(404) return } res.json(requestedUser) }) return r }