diff --git a/app.js b/app.js index 1a8caea..02ec2bc 100644 --- a/app.js +++ b/app.js @@ -11,14 +11,20 @@ const path = require('path'); // Core Node.js module to handle and transform fil const cookieParser = require('cookie-parser'); // Middleware to parse and set cookies in request objects const logger = require('morgan'); // HTTP request logger middleware for node.js const session = require('express-session'); -var flash = require('connect-flash'); - -// DB Modules -const sqlite3 = require('sqlite3').verbose(); -const db = new sqlite3.Database('./db/minitwit.db'); // todo This needs to be set up in an init function with error handling instead -const fs = require('fs'); -const sqlFilePath = path.join(__dirname, 'db', 'schema.sql'); -const sql = fs.readFileSync(sqlFilePath, 'utf8'); + +const flash = require('connect-flash'); + + +// Initialize database schema +const db = require('./db/database'); + +db.initSchema() + .then(() => { + console.log('Database schema initialized successfully.'); + }) + .catch((err) => { + console.error('Error initializing database schema:', err); + }); // Import routers for different paths diff --git a/db/database.js b/db/database.js new file mode 100644 index 0000000..1e4080b --- /dev/null +++ b/db/database.js @@ -0,0 +1,33 @@ +const sqlite3 = require('sqlite3').verbose(); +const fs = require('fs'); +const path = require('path'); + +class Database { + constructor() { + if (!Database.instance) { + this.db = new sqlite3.Database('./db/minitwit.db'); + Database.instance = this; + } + return Database.instance; + } + + getDb() { + return this.db; + } + + async initSchema() { + const sqlFilePath = path.join(__dirname, 'schema.sql'); + const sql = fs.readFileSync(sqlFilePath, 'utf8'); + return new Promise((resolve, reject) => { + this.db.exec(sql, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} + +module.exports = new Database(); diff --git a/routes/login.js b/routes/login.js index 57d5883..8e04fdd 100644 --- a/routes/login.js +++ b/routes/login.js @@ -3,6 +3,7 @@ var router = express.Router(); const bcrypt = require('bcrypt'); const sqlite3 = require('sqlite3').verbose(); var session = require('express-session'); +const db = require('../db/database'); router.use(session({ secret: 'devving-and-opssing', @@ -16,18 +17,22 @@ router.use(function (req, res, next) { next(); }) -// Database connection -let db = new sqlite3.Database('./db/minitwit.db', sqlite3.OPEN_READWRITE, (err) => { - if (err) { - console.error(err.message); - } else { - console.log('Connected to the minitwit.db'); - } -}); +// If user is logged in, return id and username from session, otherwise empty user +// Use the returned value to populate views +function getUserCredentialsFromSession(req) { + if (req.session.username) { + return { + user: { + id: req.session.username.id, + username: req.session.username.username + } + } + } return { user: {} } +} /* GET login page. */ router.get('/', function (req, res) { - const g = { user: req.session.username }; + const g = getUserCredentialsFromSession(req); res.render('login', { title: 'Login', g: g }); }); @@ -42,13 +47,13 @@ router.post('/', (req, res, next) => { // Check user in DB const sql = 'SELECT * FROM user WHERE username = ?'; - db.get(sql, [username], (err, user) => { + db.getDb().get(sql, [username], (err, user) => { if (err) { console.error(err.message); return res.status(500).send('Server error'); } if (!user) { - req.flash('error', 'Invalid username or password'); + req.flash('error', 'Invalid username'); res.redirect('/login'); } else { bcrypt.compare(password, user.pw_hash, (err, result) => { @@ -65,8 +70,8 @@ router.post('/', (req, res, next) => { req.flash('success', 'You were logged in'); return res.redirect('/'); } else { - // Passwords dont match - return res.status(400).send('Invalid username or password'); + req.flash('error', 'Invalid password'); + return res.redirect('/login'); } }); } diff --git a/routes/register.js b/routes/register.js index c24427a..36af11d 100644 --- a/routes/register.js +++ b/routes/register.js @@ -1,12 +1,37 @@ var express = require('express'); var router = express.Router(); const bcrypt = require('bcrypt'); +const session = require('express-session'); const UserService = require('../services/userService'); const userService = new UserService(); +router.use(session({ + secret: 'devving-and-opssing', + resave: false, + saveUninitialized: true +})); + + +router.use(function (req, res, next) { + res.locals.success_messages = req.flash('success'); + res.locals.error_messages = req.flash('error'); + next(); +}) + +function getUserCredentialsFromSession(req) { + if (req.session.username) { + return { + user: { + id: req.session.username.id, + username: req.session.username.username + } + } + } return { user: {} } +} + /* GET register page. */ router.get('/', function (req, res, next) { - const g = { user: req.session.username }; + const g = getUserCredentialsFromSession(req); res.render('register', { title: 'Register', g: g }); }); @@ -25,7 +50,29 @@ const validateEmail = (email) => { }; router.post('/', async function (req, res, next) { - const { username, email, password } = req.body; + const { username, email, password, password2 } = req.body; + + const validEmail = validateEmail(email); + + if (password != password2) { + req.flash('error', 'The two passwords do not match') + return res.redirect('/register') + } + + if (!username) { + req.flash('error', 'You have to enter a username') + return res.redirect('/register') + } + + if (!validEmail) { + req.flash('error', 'You have to enter a valid email address') + return res.redirect('/register') + } + + if (!password) { + req.flash('error', 'You have to enter a password') + return res.redirect('/register') + } const validEmail = validateEmail(email); @@ -38,15 +85,20 @@ router.post('/', async function (req, res, next) { const emailExists = await userService.getUserIdByEmailIfExists(email); const usernameExists = await userService.getUserIdByUsernameIfExists(username); - if (emailExists || usernameExists) { - req.flash('error', 'User already exists'); + if (emailExists) { + req.flash('error', 'That email is taken'); + return res.redirect('/register') + } + + if (usernameExists) { + req.flash('error', 'The username is already taken') return res.redirect('/register') } const hashedPassword = await bcrypt.hash(password, 10); await userService.registerUser(username, email, hashedPassword); - req.flash('success', 'You were registered, please login') + req.flash('success', 'You were successfully registered and can login now') return res.redirect('/login'); } catch (err) { console.error(err); diff --git a/routes/timeline.js b/routes/timeline.js index e7e038f..8b2b4e6 100644 --- a/routes/timeline.js +++ b/routes/timeline.js @@ -11,6 +11,17 @@ router.use(session({ saveUninitialized: true })); +function getUserCredentialsFromSession(req) { + if (req.session.username) { + return { + user: { + id: req.session.username.id, + username: req.session.username.username + } + } + } return { user: {} } +} + router.use(function (req, res, next) { res.locals.success_messages = req.flash('success'); res.locals.error_messages = req.flash('error'); @@ -52,7 +63,8 @@ router.post('/add_message', requireAuth, async function (req, res, next) { const messageContent = req.body.text; const currentDate = Math.floor(new Date().getTime() / 1000); await userService.addMessage(userId, messageContent, currentDate); - req.flash('success', 'Nice tweet!'); + req.flash('success', 'Your message was recorded'); + res.redirect('/') } catch (error) { console.log(error); @@ -60,16 +72,15 @@ router.post('/add_message', requireAuth, async function (req, res, next) { } }) -router.get('/logout', function (req, res) { +router.get('/logout', requireAuth, function (req, res) { req.session.destroy(); res.redirect('/public'); }); router.get('/', requireAuth, async function (req, res, next) { try { - const userId = req.session.username.id; - const g = { user: req.session.username }; - let messages = await userService.getMessagesFromUserAndFollowedUsers(userId); + const g = getUserCredentialsFromSession(req); + let messages = await userService.getMessagesFromUserAndFollowedUsers(g.user.id); res.render('timeline', { endpoint: 'timeline', title: `${g.user.username}'s timeline`, @@ -84,13 +95,9 @@ router.get('/', requireAuth, async function (req, res, next) { router.get('/public', async function (req, res, next) { try { - - const g = { - user: req.session.username - }; + const g = getUserCredentialsFromSession(req); let messages = await userService.getPublicTimelineMessages(); res.render('timeline', { - endpoint: 'timeline', title: `Public Timeline`, messages: formatMessages(messages), g: g, @@ -104,29 +111,26 @@ router.get('/public', async function (req, res, next) { -router.get('/:username', requireAuth, async function (req, res, next) { +router.get('/:username', async function (req, res, next) { try { - const who_id = req.session.username.id; - const g = { - user: { - id: req.session.username.id, - username: req.session.username.username - } - }; + const g = getUserCredentialsFromSession(req); const whom_username = req.params.username; const whom_id = await userService.getUserIdByUsername(whom_username); const profile_user = { user: { - id: whom_id, + id: whom_id.user_id, username: whom_username } }; - const followed = await userService.isFollowing(who_id, whom_id); + let followed = false; + if (g) { + followed = await userService.isFollowing(g.user.id, whom_id.user_id); + } - let messages = await userService.getMessagesByUserId(whom_id); + let messages = await userService.getMessagesByUserId(whom_id.user_id); res.render('timeline', { endpoint: 'user', @@ -145,38 +149,16 @@ router.get('/:username', requireAuth, async function (req, res, next) { router.get('/:username/follow', requireAuth, async function (req, res, next) { try { - const who_id = req.session.username.id; - const g = { - user: { - id: req.session.username.id, - username: req.session.username.username - - } - }; + const g = getUserCredentialsFromSession(req); const whom_username = req.params.username; const whom_id = await userService.getUserIdByUsername(whom_username); - const profile_user = { - user: { - id: whom_id, - username: whom_username - } - }; - let messages = await userService.getMessagesByUserId(whom_id); + await userService.followUser(g.user.id, whom_id.user_id) - const followed = await userService.followUser(who_id, whom_id); req.flash('success', `You are now following ${whom_username}`) - // TODO: implement flashes - // res.redirect(`/${whom_username}`); - res.render('timeline', { - endpoint: 'user', - title: `${whom_username}'s Timeline`, - messages: formatMessages(messages), - g: g, - profile_user: profile_user, - followed: followed, - }); + res.redirect(`/${whom_username}`); + } catch (error) { console.error(error.message); res.status(500).send('Server error'); @@ -185,38 +167,16 @@ router.get('/:username/follow', requireAuth, async function (req, res, next) { router.get('/:username/unfollow', requireAuth, async function (req, res, next) { try { - const who_id = req.session.username.id; - const g = { - user: { - id: req.session.username.id, - username: req.session.username.username - - } - }; + const g = getUserCredentialsFromSession(req); const whom_username = req.params.username; const whom_id = await userService.getUserIdByUsername(whom_username); - const profile_user = { - user: { - id: whom_id, - username: whom_username - } - }; - let messages = await userService.getMessagesByUserId(whom_id); + await userService.unfollowUser(g.user.id, whom_id.user_id); - const followed = await userService.unfollowUser(who_id, whom_id); req.flash('success', `You have unfollowed ${whom_username}`) - // TODO: implement flashes - // res.redirect(`/${whom_username}`); - res.render('timeline', { - endpoint: 'user', - title: `${whom_username}'s Timeline`, - messages: formatMessages(messages), - g: g, - profile_user: profile_user, - followed: followed, - }); + res.redirect(`/${whom_username}`); + } catch (error) { console.error(error.message); res.status(500).send('Server error'); diff --git a/services/userService.js b/services/userService.js index 309e721..d45ab64 100644 --- a/services/userService.js +++ b/services/userService.js @@ -1,21 +1,12 @@ -const sqlite3 = require('sqlite3').verbose(); +const db = require('../db/database'); class UserService { - constructor() { - this.db = new sqlite3.Database('./db/minitwit.db', sqlite3.OPEN_READWRITE, (err) => { - if (err) { - console.error(err.message); - } else { - console.log('Added db connection from user service'); - } - }); - } async addMessage(userId, messageContent, currentDate) { const flagged = 0; const sql = `INSERT INTO message (author_id, text, pub_date, flagged) VALUES (?, ?, ?, ?)`; return new Promise((resolve, reject) => { - this.db.run(sql, [userId, messageContent, currentDate, flagged], (err) => { + db.getDb().run(sql, [userId, messageContent, currentDate, flagged], (err) => { if (err) { reject(err); } else { @@ -33,7 +24,7 @@ class UserService { ORDER BY message.pub_date DESC LIMIT 50`; return new Promise((resolve, reject) => { - this.db.all(sql, [id], (err, messages) => { + db.getDb().all(sql, [id], (err, messages) => { if (err) { reject(err); } else { @@ -56,7 +47,7 @@ class UserService { ORDER BY message.pub_date DESC LIMIT 50`; return new Promise((resolve, reject) => { - this.db.all(sql, [userId, userId], (err, messages) => { + db.getDb().all(sql, [userId, userId], (err, messages) => { if (err) { reject(err); } else { @@ -74,7 +65,7 @@ class UserService { ORDER BY message.pub_date DESC LIMIT 50`; return new Promise((resolve, reject) => { - this.db.all(sql, [], (err, messages) => { + db.getDb().all(sql, [], (err, messages) => { if (err) { reject(err); } else { @@ -88,7 +79,7 @@ class UserService { const sql = `SELECT user_id FROM user WHERE user.username = ?`; return new Promise((resolve, reject) => { - this.db.get(sql, [username], (err, row) => { + db.getDb().get(sql, [username], (err, row) => { if (err) { reject(err); } else { @@ -101,7 +92,7 @@ class UserService { async getUserByUsername(username) { const sql = 'SELECT * FROM user WHERE username = ?'; return new Promise((reject, resolve) => { - this.db.get(sql, [username], (err, row) => { + db.getDb().get(sql, [username], (err, row) => { if (err) { console.error(err.message); reject(err); @@ -115,11 +106,11 @@ class UserService { const sql = `SELECT user_id FROM user WHERE user.username = ?`; return new Promise((resolve, reject) => { - this.db.get(sql, [username], (err, row) => { + db.getDb().get(sql, [username], (err, id) => { if (err) { reject(err); } else { - resolve(row.user_id); + resolve(id); } }); }); @@ -129,11 +120,11 @@ class UserService { const sql = `SELECT user_id FROM user WHERE user.email = ?`; return new Promise((resolve, reject) => { - this.db.get(sql, [email], (err, row) => { + db.getDb().get(sql, [email], (err, id) => { if (err) { reject(err); } else { - resolve(row.user_id); + resolve(id); } }); }); @@ -143,7 +134,7 @@ class UserService { const sql = `SELECT user_id FROM user WHERE user.email = ?`; return new Promise((resolve, reject) => { - this.db.get(sql, [email], (err, row) => { + db.getDb().get(sql, [email], (err, row) => { if (err) { reject(err); } else { @@ -153,11 +144,11 @@ class UserService { }); } - async isFollowing(userId, followedId) { + async isFollowing(who_id, whom_id) { const sql = `SELECT * FROM follower WHERE who_id = ? AND whom_id = ?`; return new Promise((resolve, reject) => { - this.db.get(sql, [userId, followedId], (err, row) => { + db.getDb().get(sql, [who_id, whom_id], (err, row) => { if (err) { reject(err); } @@ -176,7 +167,7 @@ class UserService { from follower f where follower.who_id = ?` return new Promise((resolve, reject) => { - this.db.all(sql, [userId], (err, followed) => { + db.getDb().all(sql, [userId], (err, followed) => { if (err) { reject(err); } else { @@ -190,7 +181,7 @@ class UserService { async followUser(userId, followedId) { const sql = `INSERT INTO follower (who_id, whom_id) VALUES (?, ?)`; return new Promise((resolve, reject) => { - this.db.run(sql, [userId, followedId], (err) => { + db.getDb().run(sql, [userId, followedId], (err) => { if (err) { reject(err); } else { @@ -203,7 +194,7 @@ class UserService { async unfollowUser(userId, followedId) { const sql = `DELETE FROM follower WHERE who_id = ? AND whom_id = ?`; return new Promise((resolve, reject) => { - this.db.run(sql, [userId, followedId], (err) => { + db.getDb().run(sql, [userId, followedId], (err) => { if (err) { reject(err); } else { @@ -216,7 +207,7 @@ class UserService { async registerUser(username, email, hash) { const sql = `INSERT INTO user (username, email, pw_hash) VALUES (?, ?, ?)`; return new Promise((resolve, reject) => { - this.db.run(sql, [username, email, hash], (err) => { + db.getDb().run(sql, [username, email, hash], (err) => { if (err) { console.error(err.message); reject(err); diff --git a/views/layout.pug b/views/layout.pug index 0f6e89a..b0330e0 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -7,7 +7,7 @@ html div(class='page') h1 Maxitwit div(class='navigation') - if g && g.user + if g.user.id a(href=('/')) my timeline | a(href=('/public')) public timeline | a(href=('/logout')) sign out #{g.user.username} diff --git a/views/timeline.pug b/views/timeline.pug index f25ef9d..babdd8a 100644 --- a/views/timeline.pug +++ b/views/timeline.pug @@ -2,7 +2,7 @@ extends layout.pug block content h2= title - if g.user + if g.user.id if endpoint === 'user' .followstatus if g.user.id === profile_user.user.id