From 2ddf43c1398eacecf0d8af8d896fa59336207f74 Mon Sep 17 00:00:00 2001 From: Wini_Fy Date: Mon, 9 Feb 2026 16:36:50 +0700 Subject: [PATCH] feat: implement user authentication with database integration --- models/User.js | 57 ++++++++ public/js/home-form-handler.js | 256 +++++++++++++++++++++++++++++++++ routes/auth.js | 214 +++++++++++++++++++++++++-- views/auth/forgot-password.ejs | 235 ++++++++++++++++++++++++++++++ views/auth/login.ejs | 75 ++++------ views/auth/register.ejs | 256 +++++++++++++++++++++++++++++++++ views/auth/reset-password.ejs | 239 ++++++++++++++++++++++++++++++ 7 files changed, 1276 insertions(+), 56 deletions(-) create mode 100644 models/User.js create mode 100644 public/js/home-form-handler.js create mode 100644 views/auth/forgot-password.ejs create mode 100644 views/auth/register.ejs create mode 100644 views/auth/reset-password.ejs diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..bc02f2f --- /dev/null +++ b/models/User.js @@ -0,0 +1,57 @@ +const mongoose = require("mongoose"); +const bcrypt = require("bcryptjs"); + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + trim: true, + }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + password: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + role: { + type: String, + enum: ["admin", "manager", "editor"], + default: "admin", + }, + resetPasswordToken: String, + resetPasswordExpires: Date, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +// Hash password before saving +userSchema.pre("save", async function (next) { + if (!this.isModified("password")) return next(); + + try { + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); + } catch (err) { + next(err); + } +}); + +// Method to compare password +userSchema.methods.comparePassword = async function (candidatePassword) { + return await bcrypt.compare(candidatePassword, this.password); +}; + +module.exports = mongoose.model("User", userSchema); diff --git a/public/js/home-form-handler.js b/public/js/home-form-handler.js new file mode 100644 index 0000000..52fae50 --- /dev/null +++ b/public/js/home-form-handler.js @@ -0,0 +1,256 @@ +// Home Form Handler - Convert form inputs to JSON before submission +document.addEventListener('DOMContentLoaded', function() { + const form = document.querySelector('form[action="/admin/home/update"]'); + if (!form) return; + + form.addEventListener('submit', function(e) { + e.preventDefault(); + + try { + // Hero + const heroData = { + title: document.getElementById('heroTitle')?.value || '', + subtitle: document.getElementById('heroSubtitle')?.value || '', + description: document.getElementById('heroDescription')?.value || '', + primaryButton: { + label: document.getElementById('heroPrimaryButtonLabel')?.value || '', + href: document.getElementById('heroPrimaryButtonHref')?.value || '', + }, + secondaryButton: { + label: document.getElementById('heroSecondaryButtonLabel')?.value || '', + href: document.getElementById('heroSecondaryButtonHref')?.value || '', + }, + backgroundImage: document.getElementById('heroBackgroundImage')?.value || '', + videoUrl: document.getElementById('heroVideoUrl')?.value || '', + }; + document.getElementById('heroJson').value = JSON.stringify(heroData); + + // Why Choose Us + const whyChooseUsItems = []; + let index = 0; + while (document.getElementById(`whyChooseUsIcon_${index}`)) { + whyChooseUsItems.push({ + icon: document.getElementById(`whyChooseUsIcon_${index}`)?.value || '', + title: document.getElementById(`whyChooseUsTitle_${index}`)?.value || '', + description: document.getElementById(`whyChooseUsItemDescription_${index}`)?.value || '', + }); + index++; + } + + const whyChooseUsFeatures = []; + index = 0; + while (document.getElementById(`whyChooseUsFeature_${index}`)) { + const feature = document.getElementById(`whyChooseUsFeature_${index}`)?.value; + if (feature) whyChooseUsFeatures.push(feature); + index++; + } + + const whyChooseUsData = { + heading: document.getElementById('whyChooseUsHeading')?.value || '', + subheading: document.getElementById('whyChooseUsSubheading')?.value || '', + description: document.getElementById('whyChooseUsDescription')?.value || '', + items: whyChooseUsItems, + features: whyChooseUsFeatures, + ctaButton: { + label: document.getElementById('whyChooseUsCtaLabel')?.value || '', + href: document.getElementById('whyChooseUsCtaHref')?.value || '', + }, + }; + document.getElementById('whyChooseUsJson').value = JSON.stringify(whyChooseUsData); + + // Visa Solutions + const visaSolutionsItems = []; + index = 0; + while (document.getElementById(`visaSolutionsNumber_${index}`)) { + visaSolutionsItems.push({ + number: document.getElementById(`visaSolutionsNumber_${index}`)?.value || '', + title: document.getElementById(`visaSolutionsTitle_${index}`)?.value || '', + description: document.getElementById(`visaSolutionsDescription_${index}`)?.value || '', + link: document.getElementById(`visaSolutionsLink_${index}`)?.value || '', + }); + index++; + } + + const visaSolutionsData = { + heading: document.getElementById('visaSolutionsHeading')?.value || '', + subheading: document.getElementById('visaSolutionsSubheading')?.value || '', + items: visaSolutionsItems, + }; + document.getElementById('visaSolutionsJson').value = JSON.stringify(visaSolutionsData); + + // Visa Countries + const visaCountriesItems = []; + index = 0; + while (document.getElementById(`visaCountriesName_${index}`)) { + const visaTypesStr = document.getElementById(`visaCountriesVisaTypes_${index}`)?.value || ''; + const visaTypes = visaTypesStr.split(',').map(v => v.trim()).filter(v => v); + + visaCountriesItems.push({ + name: document.getElementById(`visaCountriesName_${index}`)?.value || '', + code: document.getElementById(`visaCountriesCode_${index}`)?.value || '', + flag: document.getElementById(`visaCountriesFlag_${index}`)?.value || '', + link: document.getElementById(`visaCountriesLink_${index}`)?.value || '', + visaTypes: visaTypes, + }); + index++; + } + + const visaCountriesData = { + heading: document.getElementById('visaCountriesHeading')?.value || '', + subheading: document.getElementById('visaCountriesSubheading')?.value || '', + description: document.getElementById('visaCountriesDescription')?.value || '', + countries: visaCountriesItems, + ctaButton: { + label: document.getElementById('visaCountriesCtaLabel')?.value || '', + href: document.getElementById('visaCountriesCtaHref')?.value || '', + }, + }; + document.getElementById('visaCountriesJson').value = JSON.stringify(visaCountriesData); + + // Testimonials + const testimonialsItems = []; + index = 0; + while (document.getElementById(`testimonialsName_${index}`)) { + testimonialsItems.push({ + name: document.getElementById(`testimonialsName_${index}`)?.value || '', + role: document.getElementById(`testimonialsRole_${index}`)?.value || '', + country: document.getElementById(`testimonialsCountry_${index}`)?.value || '', + rating: parseInt(document.getElementById(`testimonialsRating_${index}`)?.value || 5), + comment: document.getElementById(`testimonialsComment_${index}`)?.value || '', + avatar: document.getElementById(`testimonialsAvatar_${index}`)?.value || '', + }); + index++; + } + + const testimonialsData = { + heading: document.getElementById('testimonialsHeading')?.value || '', + subheading: document.getElementById('testimonialsSubheading')?.value || '', + videoUrl: document.getElementById('testimonialsVideoUrl')?.value || '', + videoThumbnail: document.getElementById('testimonialsVideoThumbnail')?.value || '', + items: testimonialsItems, + }; + document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData); + + // Video Gallery + const videoGalleryData = { + heading: document.getElementById('videoGalleryHeading')?.value || '', + videoUrl: document.getElementById('videoGalleryVideoUrl')?.value || '', + thumbnail: document.getElementById('videoGalleryThumbnail')?.value || '', + }; + document.getElementById('videoGalleryJson').value = JSON.stringify(videoGalleryData); + + // FAQ + const faqItems = []; + index = 0; + while (document.getElementById(`faqQuestion_${index}`)) { + faqItems.push({ + question: document.getElementById(`faqQuestion_${index}`)?.value || '', + answer: document.getElementById(`faqAnswer_${index}`)?.value || '', + }); + index++; + } + + const faqData = { + heading: document.getElementById('faqHeading')?.value || '', + subheading: document.getElementById('faqSubheading')?.value || '', + description: document.getElementById('faqDescription')?.value || '', + ctaButton: { + label: document.getElementById('faqCtaLabel')?.value || '', + href: document.getElementById('faqCtaHref')?.value || '', + }, + items: faqItems, + }; + document.getElementById('faqJson').value = JSON.stringify(faqData); + + // Achievements + const achievementsItems = []; + index = 0; + while (document.getElementById(`achievementsValue_${index}`)) { + achievementsItems.push({ + value: document.getElementById(`achievementsValue_${index}`)?.value || '', + suffix: document.getElementById(`achievementsSuffix_${index}`)?.value || '', + label: document.getElementById(`achievementsLabel_${index}`)?.value || '', + description: document.getElementById(`achievementsDescription_${index}`)?.value || '', + }); + index++; + } + + const achievementsData = { + heading: document.getElementById('achievementsHeading')?.value || '', + subheading: document.getElementById('achievementsSubheading')?.value || '', + items: achievementsItems, + }; + document.getElementById('achievementsJson').value = JSON.stringify(achievementsData); + + // Partners + const visaConsultancyItems = []; + document.querySelectorAll('.visa-consultancy-item').forEach((item) => { + const name = item.querySelector('.visa-consultancy-name')?.value || ''; + const icon = item.querySelector('.visa-consultancy-icon')?.value || ''; + const year = item.querySelector('.visa-consultancy-year')?.value || ''; + + if (name || icon || year) { + visaConsultancyItems.push({ name, icon, year }); + } + }); + + const brandItems = []; + document.querySelectorAll('.brand-item').forEach((item) => { + const logo = item.querySelector('.brand-logo')?.value || ''; + if (logo) { + brandItems.push({ logo }); + } + }); + + const partnersData = { + visaConsultancy: { + heading: document.getElementById('partnersVisaConsultancyHeading')?.value || '', + items: visaConsultancyItems, + }, + brands: { + items: brandItems, + }, + }; + + document.getElementById('partnersJson').value = JSON.stringify(partnersData); + + // Blog Preview + const blogPreviewItems = []; + index = 0; + while (document.getElementById(`blogPreviewTitle_${index}`)) { + blogPreviewItems.push({ + title: document.getElementById(`blogPreviewTitle_${index}`)?.value || '', + excerpt: document.getElementById(`blogPreviewExcerpt_${index}`)?.value || '', + category: document.getElementById(`blogPreviewCategory_${index}`)?.value || '', + date: document.getElementById(`blogPreviewDate_${index}`)?.value || '', + author: { + name: document.getElementById(`blogPreviewAuthorName_${index}`)?.value || '', + avatar: document.getElementById(`blogPreviewAuthorAvatar_${index}`)?.value || '', + }, + comments: parseInt(document.getElementById(`blogPreviewComments_${index}`)?.value || 0), + link: document.getElementById(`blogPreviewLink_${index}`)?.value || '', + thumbnail: document.getElementById(`blogPreviewThumbnail_${index}`)?.value || '', + }); + index++; + } + + const blogPreviewData = { + heading: document.getElementById('blogPreviewHeading')?.value || '', + subheading: document.getElementById('blogPreviewSubheading')?.value || '', + ctaButton: { + label: document.getElementById('blogPreviewCtaLabel')?.value || '', + href: document.getElementById('blogPreviewCtaHref')?.value || '', + }, + items: blogPreviewItems, + }; + document.getElementById('blogPreviewJson').value = JSON.stringify(blogPreviewData); + + console.log('All JSON data prepared, submitting form...'); + // Submit form + form.submit(); + } catch (error) { + console.error('Error processing form:', error); + alert('Error processing form. Please check console for details.'); + } + }); +}); diff --git a/routes/auth.js b/routes/auth.js index ab1d871..ade48f2 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -1,8 +1,8 @@ const express = require('express'); const router = express.Router(); - -const ADMIN_USERNAME = 'admin'; -const ADMIN_PASSWORD = 'admin1234'; +const User = require('../models/User'); +const crypto = require('crypto'); +const bcrypt = require('bcryptjs'); // Login page router.get('/login', (req, res) => { @@ -16,24 +16,216 @@ router.get('/login', (req, res) => { }); }); +// Login handle router.post('/login', async (req, res) => { const { username, password } = req.body; - if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) { + + try { + // Check database user + const user = await User.findOne({ username }); + + if (!user) { + req.flash('error_msg', 'Invalid username or password'); + return res.redirect('/auth/login'); + } + + const isMatch = await user.comparePassword(password); + + if (!isMatch) { + req.flash('error_msg', 'Invalid username or password'); + return res.redirect('/auth/login'); + } + + // Login success req.session.user = { - username: ADMIN_USERNAME, - email: 'admin@ggcamp.org', - name: 'Administrator', - role: 'admin' + id: user._id, + username: user.username, + email: user.email, + name: user.name, + role: user.role }; req.session.isAuthenticated = true; req.flash('success_msg', 'Login successful'); res.redirect('/admin/dashboard'); - } else { - req.flash('error_msg', 'Invalid username or password'); + + } catch (err) { + console.error(err); + req.flash('error_msg', 'An error occurred during login'); res.redirect('/auth/login'); } }); +// Register page +router.get('/register', (req, res) => { + if (req.session.isAuthenticated) { + return res.redirect('/admin/dashboard'); + } + res.render('auth/register', { + title: 'Create Account', + layout: false + }); +}); + +// Register handle +router.post('/register', async (req, res) => { + const { username, email, password, confirm_password, name } = req.body; + let errors = []; + + if (!username || !email || !password || !confirm_password || !name) { + errors.push({ msg: 'Please enter all fields' }); + } + + if (password !== confirm_password) { + errors.push({ msg: 'Passwords do not match' }); + } + + if (password.length < 6) { + errors.push({ msg: 'Password must be at least 6 characters' }); + } + + if (errors.length > 0) { + res.render('auth/register', { + errors, + username, + email, + name, + title: 'Create Account', + layout: false + }); + } else { + try { + // Check if user exists + const existingUser = await User.findOne({ $or: [{ email }, { username }] }); + + if (existingUser) { + errors.push({ msg: 'Email or Username already exists' }); + return res.render('auth/register', { + errors, + username, + email, + name, + title: 'Create Account', + layout: false + }); + } + + const newUser = new User({ + username, + email, + name, + password + }); + + await newUser.save(); + req.flash('success_msg', 'You are now registered and can log in'); + res.redirect('/auth/login'); + } catch (err) { + console.error(err); + errors.push({ msg: 'An error occurred during registration' }); + res.render('auth/register', { + errors, + username, + email, + name, + title: 'Create Account', + layout: false + }); + } + } +}); + +// Forgot Password Page +router.get('/forgot-password', (req, res) => { + res.render('auth/forgot-password', { + title: 'Forgot Password', + layout: false + }); +}); + +// Forgot Password Handle +router.post('/forgot-password', async (req, res) => { + const { email } = req.body; + try { + const user = await User.findOne({ email }); + if (!user) { + req.flash('error_msg', 'No account with that email address exists.'); + return res.redirect('/auth/forgot-password'); + } + + const token = crypto.randomBytes(20).toString('hex'); + + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + await user.save(); + + // Direct flow as requested: Enter email -> Submit -> Enter new password + // Redirect directly to the reset password page with the generated token + res.redirect(`/auth/reset-password/${token}`); + + } catch (err) { + console.error(err); + req.flash('error_msg', 'Error processing request'); + res.redirect('/auth/forgot-password'); + } +}); + +// Reset Password Page +router.get('/reset-password/:token', async (req, res) => { + try { + const user = await User.findOne({ + resetPasswordToken: req.params.token, + resetPasswordExpires: { $gt: Date.now() } + }); + + if (!user) { + req.flash('error_msg', 'Password reset token is invalid or has expired.'); + return res.redirect('/auth/forgot-password'); + } + + res.render('auth/reset-password', { + title: 'Reset Password', + layout: false, + token: req.params.token + }); + } catch (err) { + console.error(err); + res.redirect('/auth/forgot-password'); + } +}); + +// Reset Password Handle +router.post('/reset-password/:token', async (req, res) => { + try { + const user = await User.findOne({ + resetPasswordToken: req.params.token, + resetPasswordExpires: { $gt: Date.now() } + }); + + if (!user) { + req.flash('error_msg', 'Password reset token is invalid or has expired.'); + return res.redirect('/auth/forgot-password'); + } + + if (req.body.password !== req.body.confirm_password) { + req.flash('error_msg', 'Passwords do not match.'); + return res.redirect('back'); + } + + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + await user.save(); + + req.flash('success_msg', 'Success! Your password has been changed.'); + res.redirect('/auth/login'); + } catch (err) { + console.error(err); + res.redirect('back'); + } +}); + router.get('/logout', (req, res) => { req.session.destroy((err) => { if (err) { @@ -43,4 +235,4 @@ router.get('/logout', (req, res) => { }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/views/auth/forgot-password.ejs b/views/auth/forgot-password.ejs new file mode 100644 index 0000000..5fb2fd9 --- /dev/null +++ b/views/auth/forgot-password.ejs @@ -0,0 +1,235 @@ + + + + + + + + <%= title %> | CMS-SIMS + + + + + + + + + + + +
+ + <% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %> + + <% } %> + + + +
+

© 2024 Swiss Institute of Management and Sciences. All rights + reserved.

+
+
+ + + + + + + + \ No newline at end of file diff --git a/views/auth/login.ejs b/views/auth/login.ejs index 70bf828..cc0bb98 100644 --- a/views/auth/login.ejs +++ b/views/auth/login.ejs @@ -11,35 +11,18 @@ + + + + + +
+ + <% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' || + (typeof errors !=='undefined' && errors.length> 0)) { %> + + <% } %> + + + +
+

© 2024 Swiss Institute of Management and Sciences. All + rights reserved.

+
+
+ + + + + + + + \ No newline at end of file diff --git a/views/auth/reset-password.ejs b/views/auth/reset-password.ejs new file mode 100644 index 0000000..aff4f58 --- /dev/null +++ b/views/auth/reset-password.ejs @@ -0,0 +1,239 @@ + + + + + + + + <%= title %> | CMS-SIMS + + + + + + + + + + + +
+ + <% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %> + + <% } %> + + + +
+

© 2024 Swiss Institute of Management and Sciences. All rights + reserved.

+
+
+ + + + + + + + \ No newline at end of file