NodejsRetro Nitro Node.js Express EJS

Retro_tom

New Member
Jul 2, 2024
11
6
Hello Devbest!

I am currently working on a project to enhance my skills in Node.js. I'm developing a CMS specifically for a Habbo retro using Node.js, Express, and EJS. I would love to hear your ideas and suggestions on how I can improve it!
The CMS is being designed to work with Nitro and the Arcturus Emulator.
The template I’m using is temporary and will likely be replaced in the future.

  • Arcturus Morningstar Emulator support 100%
  • Login & Registration 70%
  • Articles 20%
  • Client login SSO 100%

Here are some snippets of the code:


JavaScript:
require('dotenv').config();
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const helmet = require('helmet');
const cors = require('cors');
const sessionMiddleware = require('./app/middlewares/sessionMiddleware');
const addUserInfoToLocals = require('./app/middlewares/userMiddleware');
const routes = require('./app/routes');
const app = express();
app.use(
    helmet({
        contentSecurityPolicy: {
            directives: {
                ...helmet.contentSecurityPolicy.getDefaultDirectives(),
                'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:", "*"],
                'connect-src': ["'self'", "*"],
                'img-src': ["'self'", "*", "data:"],
                'worker-src': ["'self'", "blob:"],
            },
        },
    })
);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'app', 'views'));
app.use(sessionMiddleware);
app.use(addUserInfoToLocals);
app.use('/', routes);
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});
const port = process.env.SERVER_PORT;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

JavaScript:
const { promisify } = require('util');
const connection = require('../config/database');
const {
    userIdSchema,
    authTicketSchema,
    usernameSchema,
    emailSchema,
    updateUserCredentialsSchema,
    userSchema,
    ipSchema,
    dateSchema,
    limitSchema
} = require('../validation/userValidation');
const query = promisify(connection.query).bind(connection);
const addUser = async (username, email, password, registerIp, registerIpC, registerDate) => {
    try {
        // Valideer de invoer met userSchema inclusief passwordSchema
        await userSchema.validateAsync({ username, email, password, registerIp, registerIpC, registerDate });
        // Voer de query uit om de gebruiker toe te voegen met het gehashte wachtwoord
        const result = await query(
            'INSERT INTO users (username, mail, password, ip_register, ip_current, account_created) VALUES (?, ?, ?, ?, ?, ?)',
            [username, email, password, registerIp, registerIpC, registerDate]
        );
        return result.insertId;
    } catch (error) {
        console.error('Error adding user:', error.message);
        throw error;
    }
};
const updateUserAuthTicket = async (userId, authTicket) => {
    try {
        await userIdSchema.validateAsync(userId);
        await authTicketSchema.validateAsync(authTicket);
        const result = await query('UPDATE users SET auth_ticket = ? WHERE id = ?', [authTicket, userId]);
        if (result.affectedRows === 0) {
            throw new Error('No rows updated');
        }
    } catch (error) {
        console.error('Error updating auth_ticket in database:', error.message);
        throw error;
    }
};
const getUserByUsername = async (username) => {
    try {
        await usernameSchema.validateAsync(username);
        const results = await query('SELECT * FROM users WHERE username = ?', [username]);
        return results.length > 0 ? results[0] : null;
    } catch (error) {
        console.error('Error fetching user by username:', error.message);
        throw error;
    }
};
const getUserByEmail = async (email) => {
    try {
        await emailSchema.validateAsync(email);
        const results = await query('SELECT * FROM users WHERE mail = ?', [email]);
        return results.length > 0 ? results[0] : null;
    } catch (error) {
        console.error('Error fetching user by email:', error.message);
        throw error;
    }
};
const updateUserCredentials = async (userId, newUsername, newEmail, newPassword) => {
    try {
        await updateUserCredentialsSchema.validateAsync({ userId, newUsername, newEmail, newPassword });
        let updateFields = [];
        let params = [];
        if (newUsername) {
            updateFields.push('username = ?');
            params.push(newUsername);
        }
        if (newEmail) {
            updateFields.push('mail = ?');
            params.push(newEmail);
        }
        if (newPassword) {
            updateFields.push('password = ?');
            params.push(newPassword);
        }
        params.push(userId);
        const updateQuery = `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`;
        await query(updateQuery, params);
    } catch (error) {
        console.error('Error updating user credentials:', error.message);
        throw error;
    }
};
const validateUsername = async (username, currentUserUsername = null) => {
    try {
        await usernameSchema.validateAsync(username);
        const existingUsername = await getUserByUsername(username);
        if (existingUsername && existingUsername.username !== currentUserUsername) {
            throw new Error('Username is already taken');
        }
    } catch (error) {
        console.error('Error validating username:', error.message);
        throw error;
    }
};
const updateUserLastInfo = async (username, lastIp, lastDate) => {
    try {
        await usernameSchema.validateAsync(username);
        await ipSchema.validateAsync(lastIp);
        await dateSchema.validateAsync(lastDate);
        await query('UPDATE users SET ip_current = ?, last_login = ? WHERE username = ?', [lastIp, lastDate, username]);
    } catch (error) {
        console.error('Error updating user last info:', error.message);
        throw error;
    }
};
const getLatestUsers = async (limit = 3) => {
    try {
        await limitSchema.validateAsync(limit);
        const results = await query('SELECT username, look FROM users ORDER BY id DESC LIMIT ?', [limit]);
        return results;
    } catch (error) {
        console.error('Error fetching latest users:', error.message);
        throw error;
    }
};
module.exports = {
    addUser,
    getUserByUsername,
    getUserByEmail,
    updateUserCredentials,
    validateUsername,
    updateUserLastInfo,
    updateUserAuthTicket,
    getLatestUsers
};

JavaScript:
// controllers/userController.js
const { v4: uuidv4 } = require('uuid');
const { getLatestNews } = require('../models/newsModel');
const { updateUserAuthTicket, updateUserCredentials, validateUsername, getUserByEmail, getLatestUsers } = require('../models/userModel');
const bcrypt = require('bcrypt');
const { hashPassword, validateEmail } = require('../utils/userUtils');
const schema = require('../utils/passwordUtils');
exports.mePage = async (req, res) => {
    try {
        const news = await getLatestNews();
        const users = await getLatestUsers();
        res.render('me', { news, users, errorMessage: '' });
    } catch (error) {
        console.error('Error loading index page:', error.message);
        res.render('me', { news: [], users: [], errorMessage: 'Failed to load content' });
    }
};

exports.settingsPage = (req, res) => {
    res.render('settings', { errorMessage: '' });
};
exports.updateSettings = async (req, res) => {
    const { username, email, currentPassword, newPassword, confirmNewPassword } = req.body;
    try {
        if (!username || !email || !currentPassword) {
            throw new Error('Please fill in all fields!');
        }
        await validateUsername(username, res.locals.user.username);
        if (newPassword && newPassword !== confirmNewPassword) {
            throw new Error('New passwords do not match!');
        }
        if (newPassword && !schema.validate(newPassword)) {
            throw new Error('New password does not meet requirements!');
        }
        const user = res.locals.user;
        const match = await bcrypt.compare(currentPassword, user.password);
        if (!match) {
            throw new Error('Incorrect current password!');
        }
        let hashedNewPassword = user.password;
        if (newPassword) {
            hashedNewPassword = await hashPassword(newPassword);
        }
        if (email !== user.mail) {
            if (!validateEmail(email)) {
                throw new Error('Invalid email address!');
            }
            const existingEmail = await getUserByEmail(email);
            if (existingEmail) {
                throw new Error('Email is already registered');
            }
        }
        await updateUserCredentials(user.id, username, email, hashedNewPassword);
        req.session.username = username;
        req.session.email = email;
        res.redirect('/settings?success=true');
    } catch (error) {
        console.error(`Error updating settings: ${error.message}`);
        res.render('settings', {
            loggedIn: req.session.loggedin,
            errorMessage: error.message
        });
    }
};
exports.indexPage = async (req, res) => {
    try {
        const news = await getLatestNews();
        const users = await getLatestUsers();
        res.render('index', { news, users, errorMessage: '' });
    } catch (error) {
        console.error('Error loading index page:', error.message);
        res.render('index', { news: [], users: [], errorMessage: 'Failed to load content' });
    }
};
exports.clientPage = async (req, res) => {
    const authTicket = `NodeJsRetro-v1-${uuidv4()}`;
    const room = req.params.room || null;
    const friend = req.params.friend || null;
    try {
        await updateUserAuthTicket(res.locals.user.id, authTicket);
        res.render('hotel', { authTicket, room, friend });
    } catch (error) {
        console.error('Error updating auth_ticket:', error.message);
        res.render('hotel', { errorMessage: 'Failed to update auth ticket', room, friend });
    }
};


You must be registered for see images attach

You must be registered for see images attach

You must be registered for see images attach
 
Last edited:

React

Active Member
Sep 17, 2023
163
58
Hello Devbest!

I am currently working on a project to enhance my skills in Node.js. I'm developing a CMS specifically for a Habbo retro using Node.js, Express, and EJS. I would love to hear your ideas and suggestions on how I can improve it!
The CMS is being designed to work with Nitro and the Arcturus Emulator.
The template I’m using is temporary and will likely be replaced in the future.

  • Arcturus Morningstar Emulator support 100%
  • Login & Registration 70%
  • Articles 20%
  • Client login SSO 100%

Here are some snippets of the code:


JavaScript:
require('dotenv').config();
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const helmet = require('helmet');
const cors = require('cors');
const sessionMiddleware = require('./app/middlewares/sessionMiddleware');
const addUserInfoToLocals = require('./app/middlewares/userMiddleware');
const routes = require('./app/routes');
const app = express();
app.use(
    helmet({
        contentSecurityPolicy: {
            directives: {
                ...helmet.contentSecurityPolicy.getDefaultDirectives(),
                'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:", "*"],
                'connect-src': ["'self'", "*"],
                'img-src': ["'self'", "*", "data:"],
                'worker-src': ["'self'", "blob:"],
            },
        },
    })
);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'app', 'views'));
app.use(sessionMiddleware);
app.use(addUserInfoToLocals);
app.use('/', routes);
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});
const port = process.env.SERVER_PORT;
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

JavaScript:
const { promisify } = require('util');
const connection = require('../config/database');
const {
    userIdSchema,
    authTicketSchema,
    usernameSchema,
    emailSchema,
    updateUserCredentialsSchema,
    userSchema,
    ipSchema,
    dateSchema,
    limitSchema
} = require('../validation/userValidation');
const query = promisify(connection.query).bind(connection);
const addUser = async (username, email, password, registerIp, registerIpC, registerDate) => {
    try {
        // Valideer de invoer met userSchema inclusief passwordSchema
        await userSchema.validateAsync({ username, email, password, registerIp, registerIpC, registerDate });
        // Voer de query uit om de gebruiker toe te voegen met het gehashte wachtwoord
        const result = await query(
            'INSERT INTO users (username, mail, password, ip_register, ip_current, account_created) VALUES (?, ?, ?, ?, ?, ?)',
            [username, email, password, registerIp, registerIpC, registerDate]
        );
        return result.insertId;
    } catch (error) {
        console.error('Error adding user:', error.message);
        throw error;
    }
};
const updateUserAuthTicket = async (userId, authTicket) => {
    try {
        await userIdSchema.validateAsync(userId);
        await authTicketSchema.validateAsync(authTicket);
        const result = await query('UPDATE users SET auth_ticket = ? WHERE id = ?', [authTicket, userId]);
        if (result.affectedRows === 0) {
            throw new Error('No rows updated');
        }
    } catch (error) {
        console.error('Error updating auth_ticket in database:', error.message);
        throw error;
    }
};
const getUserByUsername = async (username) => {
    try {
        await usernameSchema.validateAsync(username);
        const results = await query('SELECT * FROM users WHERE username = ?', [username]);
        return results.length > 0 ? results[0] : null;
    } catch (error) {
        console.error('Error fetching user by username:', error.message);
        throw error;
    }
};
const getUserByEmail = async (email) => {
    try {
        await emailSchema.validateAsync(email);
        const results = await query('SELECT * FROM users WHERE mail = ?', [email]);
        return results.length > 0 ? results[0] : null;
    } catch (error) {
        console.error('Error fetching user by email:', error.message);
        throw error;
    }
};
const updateUserCredentials = async (userId, newUsername, newEmail, newPassword) => {
    try {
        await updateUserCredentialsSchema.validateAsync({ userId, newUsername, newEmail, newPassword });
        let updateFields = [];
        let params = [];
        if (newUsername) {
            updateFields.push('username = ?');
            params.push(newUsername);
        }
        if (newEmail) {
            updateFields.push('mail = ?');
            params.push(newEmail);
        }
        if (newPassword) {
            updateFields.push('password = ?');
            params.push(newPassword);
        }
        params.push(userId);
        const updateQuery = `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`;
        await query(updateQuery, params);
    } catch (error) {
        console.error('Error updating user credentials:', error.message);
        throw error;
    }
};
const validateUsername = async (username, currentUserUsername = null) => {
    try {
        await usernameSchema.validateAsync(username);
        const existingUsername = await getUserByUsername(username);
        if (existingUsername && existingUsername.username !== currentUserUsername) {
            throw new Error('Username is already taken');
        }
    } catch (error) {
        console.error('Error validating username:', error.message);
        throw error;
    }
};
const updateUserLastInfo = async (username, lastIp, lastDate) => {
    try {
        await usernameSchema.validateAsync(username);
        await ipSchema.validateAsync(lastIp);
        await dateSchema.validateAsync(lastDate);
        await query('UPDATE users SET ip_current = ?, last_login = ? WHERE username = ?', [lastIp, lastDate, username]);
    } catch (error) {
        console.error('Error updating user last info:', error.message);
        throw error;
    }
};
const getLatestUsers = async (limit = 3) => {
    try {
        await limitSchema.validateAsync(limit);
        const results = await query('SELECT username, look FROM users ORDER BY id DESC LIMIT ?', [limit]);
        return results;
    } catch (error) {
        console.error('Error fetching latest users:', error.message);
        throw error;
    }
};
module.exports = {
    addUser,
    getUserByUsername,
    getUserByEmail,
    updateUserCredentials,
    validateUsername,
    updateUserLastInfo,
    updateUserAuthTicket,
    getLatestUsers
};

JavaScript:
// controllers/userController.js
const { v4: uuidv4 } = require('uuid');
const { getLatestNews } = require('../models/newsModel');
const { updateUserAuthTicket, updateUserCredentials, validateUsername, getUserByEmail, getLatestUsers } = require('../models/userModel');
const bcrypt = require('bcrypt');
const { hashPassword, validateEmail } = require('../utils/userUtils');
const schema = require('../utils/passwordUtils');
exports.mePage = async (req, res) => {
    try {
        const news = await getLatestNews();
        const users = await getLatestUsers();
        res.render('me', { news, users, errorMessage: '' });
    } catch (error) {
        console.error('Error loading index page:', error.message);
        res.render('me', { news: [], users: [], errorMessage: 'Failed to load content' });
    }
};

exports.settingsPage = (req, res) => {
    res.render('settings', { errorMessage: '' });
};
exports.updateSettings = async (req, res) => {
    const { username, email, currentPassword, newPassword, confirmNewPassword } = req.body;
    try {
        if (!username || !email || !currentPassword) {
            throw new Error('Please fill in all fields!');
        }
        await validateUsername(username, res.locals.user.username);
        if (newPassword && newPassword !== confirmNewPassword) {
            throw new Error('New passwords do not match!');
        }
        if (newPassword && !schema.validate(newPassword)) {
            throw new Error('New password does not meet requirements!');
        }
        const user = res.locals.user;
        const match = await bcrypt.compare(currentPassword, user.password);
        if (!match) {
            throw new Error('Incorrect current password!');
        }
        let hashedNewPassword = user.password;
        if (newPassword) {
            hashedNewPassword = await hashPassword(newPassword);
        }
        if (email !== user.mail) {
            if (!validateEmail(email)) {
                throw new Error('Invalid email address!');
            }
            const existingEmail = await getUserByEmail(email);
            if (existingEmail) {
                throw new Error('Email is already registered');
            }
        }
        await updateUserCredentials(user.id, username, email, hashedNewPassword);
        req.session.username = username;
        req.session.email = email;
        res.redirect('/settings?success=true');
    } catch (error) {
        console.error(`Error updating settings: ${error.message}`);
        res.render('settings', {
            loggedIn: req.session.loggedin,
            errorMessage: error.message
        });
    }
};
exports.indexPage = async (req, res) => {
    try {
        const news = await getLatestNews();
        const users = await getLatestUsers();
        res.render('index', { news, users, errorMessage: '' });
    } catch (error) {
        console.error('Error loading index page:', error.message);
        res.render('index', { news: [], users: [], errorMessage: 'Failed to load content' });
    }
};
exports.clientPage = async (req, res) => {
    const authTicket = `NodeJsRetro-v1-${uuidv4()}`;
    const room = req.params.room || null;
    const friend = req.params.friend || null;
    try {
        await updateUserAuthTicket(res.locals.user.id, authTicket);
        res.render('hotel', { authTicket, room, friend });
    } catch (error) {
        console.error('Error updating auth_ticket:', error.message);
        res.render('hotel', { errorMessage: 'Failed to update auth ticket', room, friend });
    }
};


You must be registered for see images attach

You must be registered for see images attach

You must be registered for see images attach
Nice work, hopefully you'll manage to finish this! it is similar to a "ChocolateyCMS" that was released on here before.

Will this be used for you to run a retro or just to release?
 

Retro_tom

New Member
Jul 2, 2024
11
6
Nice work, hopefully you'll manage to finish this! it is similar to a "ChocolateyCMS" that was released on here before.

Will this be used for you to run a retro or just to release?
Thank you! Right now, I'm mainly concerned with the code and what I can improve. The layout is temporary, and I will create a different one.

My last project was Braincms. Many people used it, but the code was, how do I put it, 😅...

I am proud that many people used it, but I am definitely not proud of the code.
 

React

Active Member
Sep 17, 2023
163
58
Thank you! Right now, I'm mainly concerned with the code and what I can improve. The layout is temporary, and I will create a different one.

My last project was Braincms. Many people used it, but the code was, how do I put it, 😅...
No no, I wasn't criticising you - sorry if it sounded like I was.

You're right, design is not important - as long as the backend is stable/up too scratch.

You're the creator of Brain CMS? - I used to use it/made stupid edits of it that I won't even put here, I'd only become a victim to bullying if I did 🤣 .
 
Last edited:

Retro_tom

New Member
Jul 2, 2024
11
6
No no, I wasn't criticising you - sorry if it sounded like I was.

You're right, design is not important - as long as the backend is stable/up too scratch.

You're the creator of Brain CMS? - I used to use it/made stupid events of it that I won't even put here, I'd only become a victim to bullying if I did 🤣 .
Yes, that's correct. My old account was Retro ripper, but it was linked through Facebook, and that doesn't work here anymore.
 

React

Active Member
Sep 17, 2023
163
58
Yes, that's correct. My old account was Retro ripper, but it was linked through Facebook, and that doesn't work here anymore.
Ahh I see, good to see you're back around & most importantly back in the game! good luck with development, I will be following :)
 

boz

don daddy
Mar 23, 2021
181
83
It’s nice to see you’re trying to learn your skills by working on habbo-related content, the look the cms just looks like chocolatey CMS. Good luck with it all though :)
 

LeChris

https://habbo.codes/
Sep 30, 2013
2,780
1,385
Nice work, hopefully you'll manage to finish this! it is similar to a "ChocolateyCMS" that was released on here before.

Will this be used for you to run a retro or just to release?
No it’s not. Chocolatey didn’t use NodeJS because he just edited the direct transpiled code from Habbo. It used Laravel that returned html, which is inefficient and terrible practice.

This project uses NodeJS and an actual template engine, which is far more effective. I wish you luck, have you considered trying a SPA library?
 

Retro_tom

New Member
Jul 2, 2024
11
6
No it’s not. Chocolatey didn’t use NodeJS because he just edited the direct transpiled code from Habbo. It used Laravel that returned html, which is inefficient and terrible practice.

This project uses NodeJS and an actual template engine, which is far more effective. I wish you luck, have you considered trying a SPA library?
No, I didn't think about that! But I will take a look at it.
 

boz

don daddy
Mar 23, 2021
181
83
No it’s not. Chocolatey didn’t use NodeJS because he just edited the direct transpiled code from Habbo. It used Laravel that returned html, which is inefficient and terrible practice.

This project uses NodeJS and an actual template engine, which is far more effective. I wish you luck, have you considered trying a SPA library?
I think he (and I) more meant the styling of the cms itself but like he mentioned this is just one certain theme so I guess all that can be changed easily.
 

LeChris

https://habbo.codes/
Sep 30, 2013
2,780
1,385
I think he (and I) more meant the styling of the cms itself but like he mentioned this is just one certain theme so I guess all that can be changed easily.
The style is Habbo.com - the dev of chocolatey literally copied and pasted habbo’s production code
 

Laravel

Elite Donator
Nov 29, 2020
43
50
Could I recommend you trialing a Node framework like NestJS?
You will write less code, boilerplate and build out a stronger codebase with a bigger audience?

Not criticism, this is awesome for something innovative and away from PHP, but there are frameworks that give you everything you need :)
 

Retro_tom

New Member
Jul 2, 2024
11
6
Could I recommend you trialing a Node framework like NestJS?
You will write less code, boilerplate and build out a stronger codebase with a bigger audience?

Not criticism, this is awesome for something innovative and away from PHP, but there are frameworks that give you everything you need :)
Thank you for your response! I am going to rewrite the code. I will use React for the frond and NestJS for the backend.
 

LeChris

https://habbo.codes/
Sep 30, 2013
2,780
1,385
Could I recommend you trialing a Node framework like NestJS?
You will write less code, boilerplate and build out a stronger codebase with a bigger audience?

Not criticism, this is awesome for something innovative and away from PHP, but there are frameworks that give you everything you need :)
imo starting out from scratch is the best way to go because you can really enjoy the benefits of frameworks later on. if you immediately start with a framework you'll miss out on understanding the problems it solves

NestJS is very good though and is extremely mature.
 

Laravel

Elite Donator
Nov 29, 2020
43
50
I mean you can enjoy all of the same functionality using NextJS and NuxtJS directly too with their server side handling.

I would recommend, trialing each and finding which one is easy to build/use and expand :)
 

LeChris

https://habbo.codes/
Sep 30, 2013
2,780
1,385
I mean you can enjoy all of the same functionality using NextJS and NuxtJS directly too with their server side handling.

I would recommend, trialing each and finding which one is easy to build/use and expand :)
They can do similar stuff at the surface level but they're built for very different purposes and shouldn't be correlated. They're not competing libraries because they solve entirely different use cases and can even be combined on a monolith if you wish.

Nest is way more versatile than Next could ever hope to be and is amazing for backend use cases of all varieties. Next is extremely opinionated and has a hard time proving it's own use case sometimes (I use both, but do question the benefits of moving my react code to ssr with the limitations of ssr)
 

Users who are viewing this thread

Top