WebAPP Exam 2025
This commit is contained in:
8
server/.gitignore
vendored
Normal file
8
server/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
391
server/dao-forum.js
Normal file
391
server/dao-forum.js
Normal file
@ -0,0 +1,391 @@
|
||||
'use strict';
|
||||
|
||||
/* Data Access Object (DAO) module for accessing forum data */
|
||||
|
||||
const db = require('./db');
|
||||
const dayjs = require("dayjs");
|
||||
const crypto = require("crypto");
|
||||
|
||||
exports.getUserById = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'SELECT * FROM User WHERE id=?';
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else if (row === undefined)
|
||||
resolve({ error: 'User not found.' });
|
||||
else {
|
||||
const user = { id: row.ID, username: row.Username, type: row.Type}
|
||||
resolve(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.getUserSecret = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'SELECT Secret FROM User WHERE id=?';
|
||||
db.get(sql, [id], (err, row) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else if (row === undefined)
|
||||
resolve({ error: 'Secret not found.' });
|
||||
else {
|
||||
const secret = row.Secret;
|
||||
resolve(secret);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.getUser = (username, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'SELECT * FROM User WHERE username=?';
|
||||
db.get(sql, [username], (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (row === undefined) {
|
||||
resolve(false);
|
||||
}
|
||||
else {
|
||||
const user = { id: row.ID, username: row.Username, type: row.Type};
|
||||
|
||||
crypto.scrypt(password, row.Salt, 64, function (err, hashedPassword) {
|
||||
if (err) reject(err);
|
||||
if (!crypto.timingSafeEqual(Buffer.from(row.Password, 'hex'), hashedPassword))
|
||||
resolve(false);
|
||||
else
|
||||
resolve(user);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const convertPostFromDbRecord = (dbRecord) => {
|
||||
const post = {};
|
||||
post.id = dbRecord.ID;
|
||||
post.title = dbRecord.Title;
|
||||
post.authorid = dbRecord.AuthorID;
|
||||
post.maxcomments = dbRecord.MaxComments;
|
||||
post.publication = dbRecord.Publication;
|
||||
post.text = dbRecord.Text;
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
const convertCommentFromDbRecord = (dbRecord) => {
|
||||
const comment = {};
|
||||
comment.id = dbRecord.ID;
|
||||
comment.text = dbRecord.Text;
|
||||
comment.publication = dbRecord.Publication;
|
||||
comment.authorid = dbRecord.AuthorID;
|
||||
comment.postid = dbRecord.PostID;
|
||||
return comment;
|
||||
}
|
||||
|
||||
// This function retrieves the whole list of posts from the database.
|
||||
|
||||
exports.listPosts = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "SELECT * FROM Post";
|
||||
db.all(sql, (err,rows) => {
|
||||
if (err) { reject(err); }
|
||||
|
||||
const posts = rows.map((e) => {
|
||||
const post = convertPostFromDbRecord(e);
|
||||
return post;
|
||||
});
|
||||
resolve(posts);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.listCommentsOfPost = (PostID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "SELECT * FROM Comment AS C WHERE C.PostID = ?";
|
||||
db.all(sql, [PostID], (err,rows) => {
|
||||
if (err) { reject(err); }
|
||||
|
||||
const comments = rows.map((e) => {
|
||||
const comment = convertCommentFromDbRecord(e);
|
||||
return comment;
|
||||
});
|
||||
resolve(comments);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.listAnonCommentsOfPost = (PostID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = "SELECT * FROM Comment AS C WHERE C.PostID = ?";
|
||||
db.all(sql, [PostID], (err,rows) => {
|
||||
if (err) { reject(err); }
|
||||
|
||||
const comments = rows.filter( (e) => e.AuthorID == null)
|
||||
.map((e) => {
|
||||
const comment = convertCommentFromDbRecord(e);
|
||||
return comment;
|
||||
});
|
||||
comments.forEach( c => console.log(c.text));
|
||||
resolve(comments);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
exports.getNumCommentsOfPost = (PostID) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT COUNT(*) AS N FROM Comment AS C WHERE C.PostID=?';
|
||||
db.get(sql,[PostID], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Post not found.'});
|
||||
} else {
|
||||
const num = row.N;
|
||||
resolve(num);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.getPost = (id) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT * FROM Post WHERE id=?';
|
||||
db.get(sql,[id], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Post not found.'});
|
||||
} else {
|
||||
const post = convertPostFromDbRecord(row);
|
||||
resolve(post);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.getComment = (id) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT * FROM Comment WHERE ID=?';
|
||||
db.get(sql,[id], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Comment not found.'});
|
||||
} else {
|
||||
const comment = convertCommentFromDbRecord(row);
|
||||
resolve(comment);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.getAuthorOfPost = (PostID) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT Username FROM User as U, Post as P WHERE P.ID = ? AND P.AuthorID = U.ID';
|
||||
db.get(sql,[PostID], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Author not found.'});
|
||||
} else {
|
||||
const author = row.Username;
|
||||
resolve(author);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.getAuthorOfComment = (CommentID) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT Username FROM User as U, Comment as C WHERE C.ID = ? AND C.AuthorID = U.ID';
|
||||
db.get(sql,[CommentID], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Author not found.'});
|
||||
} else {
|
||||
const author = row.Username;
|
||||
resolve(author);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.getAuthorIDOfPost = (PostID) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT AuthorID FROM Post as P WHERE P.ID = ?';
|
||||
db.get(sql,[PostID], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Author not found.'});
|
||||
} else {
|
||||
const id = row.AuthorID;
|
||||
resolve(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.getAuthorIDOfComment = (CommentID) => {
|
||||
return new Promise((resolve, reject)=>{
|
||||
const sql = 'SELECT AuthorID FROM Comment as C WHERE C.ID = ?';
|
||||
db.get(sql,[CommentID], (err,row)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Author not found.'});
|
||||
} else {
|
||||
const id = row.AuthorID;
|
||||
resolve(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.createPost = (post) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("POST DAO:"+post.text);
|
||||
const sql = 'INSERT INTO Post (Title, AuthorID, MaxComments, Publication, Text) VALUES(?, ?, ?, ?, ?)';
|
||||
db.run(sql, [post.title, post.authorid, post.maxcomments, post.publication, post.text], function(err){
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(exports.getPost(this.lastID));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.createComment = (comment) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'INSERT INTO Comment (Text, Publication, AuthorID, PostID) VALUES(?, ?, ?, ?)';
|
||||
db.run(sql, [comment.text, comment.publication, comment.authorid, comment.postid], function(err){
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(exports.getComment(this.lastID));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.setInteresting = (UserID, CommentID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'INSERT INTO Interesting(UserID, CommentID) VALUES(?, ?)';
|
||||
db.run(sql, [UserID, CommentID], function(err){
|
||||
if (err){
|
||||
reject(err);
|
||||
}
|
||||
resolve(exports.isUserInterested(UserID, CommentID));
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.getNumInterested = (CommentID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'SELECT COUNT(UserID) AS N FROM Interesting AS I WHERE I.CommentID = ?'
|
||||
db.get(sql, [CommentID], (err,row) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve({error: 'Comment not found.'});
|
||||
} else {
|
||||
const num = row.N;
|
||||
resolve(num);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Return true if user is interested to comment, otherwise return false
|
||||
exports.isUserInterested = (UserID, CommentID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'SELECT * FROM Interesting AS I WHERE I.CommentID = ? AND I.UserID=?'
|
||||
db.get(sql, [CommentID, UserID], (err,row) => {
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
if( row == undefined){
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.deletePost = (PostID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() =>{
|
||||
db.run('DELETE FROM Interesting as I, Comment as C WHERE I.CommentID = C.ID AND C.PostID = ? ', [PostID], (err) =>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
.run('DELETE FROM Comment as C WHERE C.PostID = ?', [PostID], (err)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
.run('DELETE FROM Post as P WHERE P.ID = ?',[PostID], (err)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
resolve(exports.listPosts());
|
||||
});
|
||||
}
|
||||
|
||||
exports.deleteComment = (CommentID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() =>{
|
||||
db.run('DELETE FROM Interesting as I WHERE I.CommentID = ?', [CommentID], (err) =>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
.run('DELETE FROM Comment as C WHERE C.ID = ?', [CommentID], (err)=>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
exports.deleteInteresting = (UserID,CommentID) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() =>{
|
||||
db.run('DELETE FROM Interesting as I WHERE I.CommentID = ? AND I.UserID= ?', [CommentID, UserID], (err) =>{
|
||||
if(err){
|
||||
reject(err);
|
||||
}
|
||||
})
|
||||
});
|
||||
resolve(exports.isUserInterested(UserID, CommentID));
|
||||
});
|
||||
}
|
||||
|
||||
exports.editComment = (commentID, text) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sql = 'UPDATE Comment SET Text=? WHERE ID = ?';
|
||||
db.run(sql, [text, commentID], function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
if (this.changes !== 1) {
|
||||
resolve({ error: 'Comment not found.' });
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
11
server/db.js
Normal file
11
server/db.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const sqlite = require('sqlite3');
|
||||
|
||||
// open the database
|
||||
|
||||
const db = new sqlite.Database('test.db', (err) => {
|
||||
if (err) throw(err);
|
||||
});
|
||||
|
||||
module.exports = db;
|
||||
353
server/index.js
Normal file
353
server/index.js
Normal file
@ -0,0 +1,353 @@
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const morgan = require('morgan'); // logging middleware
|
||||
const { check, validationResult, oneOf } = require('express-validator'); // validation middleware
|
||||
const cors = require('cors');
|
||||
|
||||
const forumDao = require('./dao-forum');
|
||||
const dayjs = require('dayjs');
|
||||
const app = express();
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
|
||||
const corsOptions = {
|
||||
origin: 'http://localhost:5173',
|
||||
credentials: true,
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
/* Passport */
|
||||
|
||||
const passport = require('passport'); // authentication middleware
|
||||
const LocalStrategy = require('passport-local'); // authentication strategy (username and password)
|
||||
|
||||
const base32 = require('thirty-two');
|
||||
const TotpStrategy = require('passport-totp').Strategy; // totp
|
||||
|
||||
passport.use(new LocalStrategy(async function verify(username, password, callback) {
|
||||
const user = await forumDao.getUser(username, password)
|
||||
if(!user)
|
||||
return callback(null, false, 'Incorrect username or password');
|
||||
|
||||
return callback(null, user); // NOTE: user info in the session (all fields returned by userDao.getUser, i.e, id, username, name)
|
||||
}));
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function (user, callback) {
|
||||
|
||||
return callback(null, user);
|
||||
});
|
||||
|
||||
|
||||
const session = require('express-session');
|
||||
/* The secret field is used to hash the session with HMAC */
|
||||
app.use(session({
|
||||
secret: "very l0ng s3cr3t used h4sh with HM4C the sessi0n",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}));
|
||||
|
||||
app.use(passport.authenticate('session'));
|
||||
/* If user does not exist the db will simply throw an error and everything will be fine */
|
||||
passport.use(new TotpStrategy(
|
||||
async function (user, done) {
|
||||
const secret = await forumDao.getUserSecret(user.id);
|
||||
if(!secret)
|
||||
return done(null, false, {message: "TOTP is not enabled for this account"});
|
||||
return done(null, base32.decode(secret), 30);
|
||||
})
|
||||
)
|
||||
|
||||
const isLoggedIn = (req, res, next) => {
|
||||
if(req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).json({error: 'Not authorized'});
|
||||
}
|
||||
|
||||
/* This one is used because we still need to serve even anonymous user in some queries */
|
||||
/* If Authenticated is True otherwise is False and therefore anonymous */
|
||||
const isLoggedInOrAnon = (req, res) => {
|
||||
if(req.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isTotp(req, res, next) {
|
||||
if(req.session.method === 'totp')
|
||||
return next();
|
||||
return res.status(401).json({ error: 'Missing TOTP authentication'});
|
||||
}
|
||||
// Created to check this not as a middleware
|
||||
function isLoggedInTotp(req, res){
|
||||
if(req.session.method === 'totp')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxTitleLength = 160;
|
||||
const maxCommentLength = 1000;
|
||||
const maxTextLength = 1500;
|
||||
|
||||
const errorFormatter = ({ location, msg, param, value, nestedErrors }) => {
|
||||
return `${location}[${param}]: ${msg}`;
|
||||
};
|
||||
|
||||
app.get('/api/posts', async (req, res) => {
|
||||
const isLogged = isLoggedInOrAnon(req);
|
||||
try{
|
||||
|
||||
let posts = await forumDao.listPosts();
|
||||
posts = posts.map( async (p) => {
|
||||
let comments;
|
||||
/* If authorid is null the Author of the comment is not searched at all
|
||||
The field author exists only if there is a valid author id */
|
||||
if( isLogged == true ){
|
||||
comments = await forumDao.listCommentsOfPost(p.id);
|
||||
comments = comments.map( async (c) => {
|
||||
/* numInterested and isInterested should be visible only to authenticated users. */
|
||||
/* isInterested should be true ONLY for the exact user that has marked the comment interesting */
|
||||
c.numInterested = await forumDao.getNumInterested(c.id);
|
||||
c.isInterested = await forumDao.isUserInterested(req.user.id, c.id);
|
||||
if(c.authorid != null){
|
||||
c.author = await forumDao.getAuthorOfComment(c.id);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
}
|
||||
else{
|
||||
comments = await forumDao.listAnonCommentsOfPost(p.id);
|
||||
}
|
||||
|
||||
p.comments = await Promise.all(comments);
|
||||
p.author = await forumDao.getAuthorOfPost(p.id);
|
||||
p.numComments = await forumDao.getNumCommentsOfPost(p.id);
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
const postsWithComments = await Promise.all(posts);
|
||||
res.json(postsWithComments);
|
||||
}
|
||||
catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Create a new post (Only authenticated user) */
|
||||
/* INPUT: Title, Text, Max number of comments */
|
||||
app.post('/api/posts', isLoggedIn, [check('title').isLength({min:1, max: maxTitleLength}).withMessage(`The text of the post MUST be >=1 and <= ${maxTitleLength}`),
|
||||
check('text').isLength({min:1, max: maxTextLength}).withMessage(`The title of the post MUST be >=1 and <= ${maxTextLength}`),
|
||||
check('max').isInt({min:-1}).withMessage("The max number of comments should be specificed and >= -1")],async (req, res) => {
|
||||
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
|
||||
const post = {title: req.body.title, text: req.body.text, publication: dayjs().format("YYYY-MM-DD HH:mm:ss"), maxcomments: req.body.max, authorid: req.user.id};
|
||||
try{
|
||||
const result = await forumDao.createPost(post);
|
||||
res.status(201).json(result);
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Create a new comment related to a post (Anyone) */
|
||||
/* INPUT: Post ID, Text*/
|
||||
app.post('/api/posts/:id/comments', [ check('id').isInt({min: 1}).withMessage("Post ID MUST be an integer >= 1"),
|
||||
check('text').isLength({min: 1, max: maxCommentLength}).withMessage(`The text of the comment MUST be >=1 and <= ${maxCommentLength}`) ],async (req,res) => {
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
const comment = {text: req.body.text, publication: dayjs().format("YYYY-MM-DD HH:mm:ss") ,authorid: req.user == null ? null : req.user.id , postid: req.params.id};
|
||||
try{
|
||||
const checkPost = await forumDao.getPost(req.params.id);
|
||||
const numcomments = await forumDao.getNumCommentsOfPost(req.params.id);
|
||||
if(checkPost.maxcomments != -1 && numcomments == checkPost.maxcomments){
|
||||
return res.status(400).json({error: "Reached max number of comments"});
|
||||
}
|
||||
const result = await forumDao.createComment(comment);
|
||||
res.status(201).json(result);
|
||||
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Toggle true/false Interesting flag on comment (Only authenticated and owner) */
|
||||
//Handled as POST/DELETE requests because we are creating a new record in a table.
|
||||
app.post('/api/posts/:id/comments/:commentId/interest', isLoggedIn, [check('id').isInt({min:1}).withMessage("Post ID MUST be an integer >= 1"),
|
||||
check('commentId').isInt({min:1}).withMessage("Comment ID MUST be an integer >= 1")], async (req,res) => {
|
||||
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
try{
|
||||
const result = await forumDao.setInteresting(req.user.id,req.params.commentId);
|
||||
res.status(201).json({});
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.delete('/api/posts/:id/comments/:commentId/interest', [check('id').isInt({min:1}).withMessage("Post ID MUST be an integer >= 1"),
|
||||
check('commentId').isInt({min:1}).withMessage("Comment ID MUST be an integer >= 1")], async (req,res) => {
|
||||
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
try{
|
||||
const result = await forumDao.deleteInteresting(req.user.id,req.params.commentId);
|
||||
res.status(201).json(result);
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Edit comment (Only authenticated and owner) */
|
||||
/* INPUT: Text */
|
||||
app.patch('/api/posts/:id/comments/:commentId', isLoggedIn,[check('id').isInt({min:1}).withMessage("Post ID MUST be an integer >= 1"),
|
||||
check('commentId').isInt({min:1}).withMessage("Comment ID MUST be an integer >= 1"),
|
||||
check('text').isLength({min: 1, max: maxCommentLength}).withMessage(`The text of the comment MUST be >=1 and <= ${maxCommentLength}`)], async (req, res) => {
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
|
||||
try{
|
||||
// Check if user.id is the author id of comment
|
||||
const authorid = await forumDao.getAuthorIDOfComment(req.params.commentId);
|
||||
if( !isLoggedInTotp(req, res) && req.user.id != authorid )
|
||||
return res.status(401).json({error: 'Not authorized'});
|
||||
const result = await forumDao.editComment(req.params.commentId, req.body.text);
|
||||
/* Must be 200 and not 204, because 204 implies empty body and the client want a json eachtime even empty*/
|
||||
res.status(200).json({});
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Delete selected post (Only authenticated totp and owner)*/
|
||||
/* INPUT: Post ID */
|
||||
app.delete('/api/posts/:id', isLoggedIn, [check('id').isInt({min:1}).withMessage("Post ID MUST be an integer >= 1")], async (req, res) => {
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
try{
|
||||
const authorid = await forumDao.getAuthorIDOfPost(req.params.id);
|
||||
if( !isLoggedInTotp(req,res) && req.user.id != authorid )
|
||||
return res.status(401).json({error: 'Not authorized'});
|
||||
|
||||
const checkPost = await forumDao.getPost(req.params.id);
|
||||
if( checkPost.error != undefined ){
|
||||
return res.status(404).json({error: checkPost.error})
|
||||
}
|
||||
const result = await forumDao.deletePost(req.params.id);
|
||||
res.status(200).json({});
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
/* Delete selected comment (Only authenticated and owner)*/
|
||||
|
||||
app.delete('/api/posts/:id/comments/:commentId',isLoggedIn, [check('id').isInt({min:1}).withMessage("Post ID MUST be an integer >= 1"),
|
||||
check('commentId').isInt({min:1}).withMessage("Comment ID MUST be an integer >= 1")], async (req, res) => {
|
||||
const errors = validationResult(req).formatWith(errorFormatter); // format error message
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json( errors.errors ); // error message is sent back as a json with the error info
|
||||
}
|
||||
try{
|
||||
const authorid = await forumDao.getAuthorIDOfComment(req.params.commentId);
|
||||
if( !isLoggedInTotp(req, res) && req.user.id != authorid )
|
||||
return res.status(401).json({error: 'Not authorized'});
|
||||
|
||||
const checkComment = await forumDao.getComment(req.params.commentId);
|
||||
if( checkComment.error != undefined ){
|
||||
return res.status(404).json({error: checkComment.error})
|
||||
}
|
||||
const result = await forumDao.deleteComment(req.params.commentId);
|
||||
res.status(200).json({});
|
||||
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
res.status(503).json({error: 'Database error'});
|
||||
}
|
||||
});
|
||||
|
||||
function clientUserInfo(req) {
|
||||
const user=req.user;
|
||||
return {id: user.id, username: user.username, canDoTotp: user.secret? true: false, isTotp: req.session.method === 'totp'};
|
||||
}
|
||||
|
||||
/* POST /api/sessions
|
||||
Used to login */
|
||||
app.post('/api/sessions', function(req, res, next) {
|
||||
passport.authenticate('local', (err, user, info) => {
|
||||
if (err)
|
||||
return next(err);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: info});
|
||||
}
|
||||
req.login(user, (err) => {
|
||||
if (err)
|
||||
return next(err);
|
||||
|
||||
return res.json(req.user);
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
/* Perform OTP (only after login) */
|
||||
app.post('/api/login-totp', isLoggedIn,
|
||||
passport.authenticate('totp'),
|
||||
function(req, res) {
|
||||
req.session.method = 'totp';
|
||||
res.json({otp: 'authorized'});
|
||||
}
|
||||
);
|
||||
|
||||
/* GET /api/sessions/current
|
||||
This route checks whether the user is logged in or not. */
|
||||
app.get('/api/sessions/current', (req, res) => {
|
||||
if(req.isAuthenticated()) {
|
||||
res.status(200).json(req.user);}
|
||||
else
|
||||
res.status(401).json({error: 'Not authenticated'});
|
||||
});
|
||||
|
||||
/* DELETE /api/session/current
|
||||
This route is used for loggin out the current user. */
|
||||
app.delete('/api/sessions/current', isLoggedIn, (req, res) => {
|
||||
req.logout(() => {
|
||||
res.status(200).json({});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const PORT = 3001;
|
||||
// Activate the server
|
||||
app.listen(PORT, (err) => {
|
||||
if (err)
|
||||
console.log(err);
|
||||
else
|
||||
console.log(`Server listening at http://localhost:${PORT}`);
|
||||
});
|
||||
2330
server/package-lock.json
generated
Normal file
2330
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
server/package.json
Normal file
24
server/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.1",
|
||||
"express-validator": "^7.2.1",
|
||||
"morgan": "^1.10.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-totp": "^0.0.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"thirty-two": "^1.0.2"
|
||||
}
|
||||
}
|
||||
118
server/scripts/initdb.js
Normal file
118
server/scripts/initdb.js
Normal file
@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
const db = require('../db')
|
||||
const crypto = require('crypto')
|
||||
const dayjs = require('dayjs')
|
||||
const insertHashedPwd = (UserID, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hashLen = 64;
|
||||
const salt = crypto.randomBytes(16).toString("hex");
|
||||
const sql = "UPDATE User SET password=?, salt=? WHERE User.ID=?"
|
||||
crypto.scrypt(password,salt, hashLen, function(err,hash){
|
||||
if(err) reject(err);
|
||||
db.run(sql,[hash.toString("hex"),salt,UserID], function(err){
|
||||
if(err)
|
||||
reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const createDB = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE "User" (
|
||||
"ID" INTEGER NOT NULL UNIQUE,
|
||||
"Username" TEXT NOT NULL,
|
||||
"Password" TEXT NOT NULL,
|
||||
"Salt" TEXT NOT NULL,
|
||||
"Secret" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
PRIMARY KEY("ID" AUTOINCREMENT));`)
|
||||
/* At least five users, 2 are Administrators*/
|
||||
.run(`INSERT INTO User(Username, Password, Salt, Secret, Type) VALUES('AdminUno', 'placeholder', 'placeholder', 'LXBSMDTMSP2I5XFXIYRGFVWSFI', 'administrator')`)
|
||||
.run(`INSERT INTO User(Username, Password, Salt, Secret, Type) VALUES('AdminDue', 'ph', 'ph', 'LXBSMDTMSP2I5XFXIYRGFVWSFI', 'administrator')`)
|
||||
.run(`INSERT INTO User(Username, Password, Salt, Type) VALUES('UtenteUno', 'ph','ph', 'viewer')`)
|
||||
.run(`INSERT INTO User(Username, Password, Salt, Type) VALUES('UtenteDue', 'ph','ph', 'viewer')`)
|
||||
.run(`INSERT INTO User(Username, Password, Salt, Type) VALUES('UtenteTre', 'ph','ph', 'viewer')`)
|
||||
.run(`CREATE TABLE "Post" (
|
||||
"Title" TEXT NOT NULL UNIQUE,
|
||||
"ID" INTEGER NOT NULL UNIQUE,
|
||||
"AuthorID" INTEGER NOT NULL,
|
||||
"MaxComments" INTEGER,
|
||||
"Publication" INTEGER NOT NULL,
|
||||
"Text" TEXT NOT NULL,
|
||||
PRIMARY KEY("ID" AUTOINCREMENT),
|
||||
FOREIGN KEY("AuthorID") REFERENCES "User"("ID"));`)
|
||||
/* Four users including two administrators should have published two posts each */
|
||||
/*Post of AdminUno*/
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 1',1,'2025-06-06 12:20:05','Lorem ipsum dolor sit amet consectetur adipiscing elit. ', -1)`)
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 2',1,'2025-06-12 10:59:01','Quisque faucibus ex sapien vitae pellentesque sem placerat.', -1)`)
|
||||
/*Post of AdminDue*/
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 3',2,'2025-06-18 08:23:12','In id cursus mi pretium tellus duis convallis.', -1)`)
|
||||
.run(`INSERT INTO Post (Title, AuthorID, MaxComments, Publication, Text) VALUES('Example post 4',2,2,'2025-06-01 23:20:11','Tempus leo eu aenean sed diam urna tempor.')`)
|
||||
/*Post of UtenteUno */
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 5',3,'2025-06-09 07:20:05','Pulvinar vivamus fringilla lacus nec metus bibendum egestas.', -1)`)
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 6',3,'2025-06-20 07:25:59','Iaculis massa nisl malesuada lacinia integer nunc posuere.', -1)`)
|
||||
/*Post of UtenteDue */
|
||||
.run(`INSERT INTO Post (Title, AuthorID, MaxComments, Publication, Text) VALUES('Example post 7',4, 3, '2025-06-03 13:20:05','Ut hendrerit semper vel class aptent taciti sociosqu.')`)
|
||||
.run(`INSERT INTO Post (Title, AuthorID, Publication, Text, MaxComments) VALUES('Example post 8',4,'2025-06-15 16:20:00','Ad litora torquent per conubia nostra inceptos himenaeos.', -1)`)
|
||||
.run(`CREATE TABLE "Comment" (
|
||||
"ID" INTEGER NOT NULL UNIQUE,
|
||||
"Text" TEXT NOT NULL,
|
||||
"Publication" TEXT NOT NULL,
|
||||
"AuthorID" INTEGER,
|
||||
"PostID" INTEGER NOT NULL,
|
||||
PRIMARY KEY("ID" AUTOINCREMENT),
|
||||
FOREIGN KEY("AuthorID") REFERENCES "User"("ID"),
|
||||
FOREIGN KEY("PostID") REFERENCES "Post"("ID"));`)
|
||||
// Comments for PostID 4 (AdminDue's post with MaxComments = 2)
|
||||
// This post should have 1 comment (one less than the limit)
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('This post has a comment limit, interesting!', '2025-06-05 14:35:10', 3, 4);`) // UtenteUno comments on AdminDue's post
|
||||
// Comments ensuring each user has comments from other users (2-3 comments each)
|
||||
//Comments from AdminUno (ID: 1) on other users' posts
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Good point!', '2025-06-18 09:02:45', 1, 5);`) // AdminUno on UtenteUno's post
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Enjoyed reading this.', '2025-06-01 23:59:01', 1, 7);`) // AdminUno on UtenteDue's post
|
||||
// Comments from AdminDue (ID: 2) on other users' posts
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Nice work!', '2025-06-11 04:17:30', 2, 6);`) // AdminDue on UtenteUno's post
|
||||
// Comments from UtenteUno (ID: 3) on other users' posts (already one on PostID 4)
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Totally agree!', '2025-06-14 10:05:00', 3, 2);`) // UtenteUno on AdminUno's post
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Great thoughts.', '2025-06-03 01:50:55', 3, 3);`) // UtenteUno on AdminDue's post
|
||||
// Comments from UtenteDue (ID: 4) on other users' posts
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Well said.', '2025-06-19 16:30:20', 4, 1);`) // UtenteDue on AdminUno's post
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('This is very helpful!', '2025-06-02 21:12:05', 4, 5);`) // UtenteDue on UtenteUno's post
|
||||
// Comments from UtenteTre (ID: 5) on other users' posts
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Appreciate your perspective.', '2025-06-16 12:08:50', 5, 3);`) // UtenteTre on AdminDue's post
|
||||
.run(`INSERT INTO Comment(Text, Publication, AuthorID, PostID) VALUES('Inspiring content.', '2025-06-10 06:25:30', 5, 8);`) // UtenteTre on UtenteDue's post
|
||||
// Anonymous comments
|
||||
.run(`INSERT INTO Comment(Text, Publication, PostID) VALUES('Appreciate my anonymous perspective.', '2025-06-04 17:00:00', 1);`)
|
||||
.run(`INSERT INTO Comment(Text, Publication, PostID) VALUES('Dont appreciate my anonymous perspective.', '2025-06-17 22:45:10', 1);`)
|
||||
.run(`INSERT INTO Comment(Text, Publication, PostID) VALUES('Dont appreciate my anonymous perspective.', '2025-06-13 03:00:00', 3);`)
|
||||
.run(`INSERT INTO Comment(Text, Publication, PostID) VALUES('Totally an anonymous comment, you will never know the author!', '2025-06-13 03:00:00', 7);`)
|
||||
// Create Interesting table
|
||||
.run(`CREATE TABLE "Interesting" (
|
||||
"UserID" INTEGER NOT NULL,
|
||||
"CommentID" INTEGER NOT NULL,
|
||||
PRIMARY KEY("UserID", "CommentID"),
|
||||
FOREIGN KEY("CommentID") REFERENCES "Comment"("ID"),
|
||||
FOREIGN KEY("UserID") REFERENCES "User"("ID"));`).run(`INSERT INTO Interesting(UserID,CommentID) VALUES(1,1)`)
|
||||
.run(`INSERT INTO Interesting(UserID,CommentID) VALUES(2,1)`)
|
||||
.run(`INSERT INTO Interesting(UserID,CommentID) VALUES(1,2)`)
|
||||
.run(`INSERT INTO Interesting(UserID,CommentID) VALUES(1,3)`)
|
||||
.run(`INSERT INTO Interesting(UserID,CommentID) VALUES(2,2)`);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
//Ugly but ensures that the passwords are updated after the creation of db
|
||||
createDB().then(
|
||||
insertHashedPwd(1,"PasswordAdmin1").then(
|
||||
insertHashedPwd(2,"PasswordAdmin2").then(
|
||||
insertHashedPwd(3,"PasswordUtente1").then(
|
||||
insertHashedPwd(4,"PasswordUtente2").then(
|
||||
insertHashedPwd(5,"PasswordUtente3")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
BIN
server/test.db
Normal file
BIN
server/test.db
Normal file
Binary file not shown.
18
server/test.js
Normal file
18
server/test.js
Normal file
@ -0,0 +1,18 @@
|
||||
const forumDao = require('./dao-forum.js');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/*/forumDao.listPosts().then(console.log);
|
||||
|
||||
forumDao.listCommentsOfPost(1).then(console.log);
|
||||
|
||||
forumDao.getNumCommentsOfPost(1).then(console.log);
|
||||
forumDao.getPost(1).then(console.log);
|
||||
forumDao.getAuthorOfPost(1).then(console.log);*/
|
||||
|
||||
//forumDao.createPost({title: "Post Creato",authorid: 1, maxcomments: null, publication: "2023-03-02", text: "Testo post creato"}).then(console.log);
|
||||
//forumDao.createComment({text: "Commento creato 2", publication:"2024-02-01",authorid: 1, postid: 1}).then(console.log);
|
||||
//forumDao.listCommentsOfPost(1).then(console.log);
|
||||
//forumDao.setInterestingComment(1,2).then(console.log);
|
||||
//forumDao.deletePost(2).then(console.log)
|
||||
//forumDao.deleteComment(2).then(console.log)
|
||||
//forumDao.listCommentsOfPost(3).then(console.log);
|
||||
Reference in New Issue
Block a user