commit 91999b06998586cf0d030f4992a60a15094ec86a Author: florian Date: Fri Jun 13 23:25:11 2025 +0200 first lines diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c0d6a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +db/database.db +.env \ No newline at end of file diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js new file mode 100644 index 0000000..3a42371 --- /dev/null +++ b/index.js @@ -0,0 +1,236 @@ +import express from 'express'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; +import bcrypt from 'bcrypt'; +import nodemailer from 'nodemailer'; +import dotenv from 'dotenv'; +import fs, { stat } from 'fs'; + +dotenv.config(); + +const transporter = nodemailer.createTransport({ + host: 'email.thepenguinontheweb.tech', + port: 587, + secure: false, // false pour STARTTLS + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } +}); + +function sendMail(to, subject, html) { + const mailOptions = { + from: process.env.EMAIL_USER, + to: to, + subject: subject, + html: html + }; + + return transporter.sendMail(mailOptions) + .then(() => console.log('Email sent successfully')) + .catch(error => console.error('Error sending email:', error)); +} + +const db = await open({ + filename: './db/database.db', + driver: sqlite3.Database +}); + +function initializeDatabase() { + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + historyToDefault INTEGER DEFAULT 0 + ); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS verify ( + id INTEGER PRIMARY KEY, + token TEXT NOT NULL UNIQUE, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); +} + +initializeDatabase(); + +const app = express(); +const port = 20909; + +app.use(express.json()); +app.use(express.static('public')); + +app.get('/login', (req, res) => { + res.sendFile('index.html', { root: 'public' }); +} +); +app.get('/register', (req, res) => { + res.sendFile('register.html', { root: 'public' }); +} +); +app.get('/', (req, res) => { + // redirect to login page + res.redirect('/login'); +} +); + +app.post('/api/login', (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ message: 'Username and password are required' }); + } + + db.get('SELECT * FROM users WHERE username = ? OR email = ?', [username, username]) + .then(user => { + if (!user) { + return res.status(401).json({ message: 'Invalid username or password' }); + } + + // Check password (replace with real password check logic) + const isPasswordValid = bcrypt.compareSync(password, user.password); + if (!isPasswordValid) { + return res.status(401).json({ message: 'Invalid username or password' }); + } + + res.status(200).json({ message: 'Login successful' }); + }) + .catch(err => { + console.error('Database error:', err); + res.status(500).json({ message: 'Internal server error' }); + }); +} +); + +app.post('/api/register', (req, res) => { + const { username, email, password } = req.body; + + if (!username || !email || !password) { + return res.status(400).json({ message: 'Username, email, and password are required' }); + } + + const hashedPassword = bcrypt.hashSync(password, 10); + + let isAlreadyRegistered = false; + db.get('SELECT * FROM users WHERE username = ? OR email = ?', [username, email]) + .then(user => { + if (user) { + isAlreadyRegistered = true; + return res.status(409).json({ message: 'Username or email already exists' }); + } + }) + .catch(err => { + console.error('Database error:', err); + return res.status(500).json({ message: 'Internal server error' }); + }); + + if (isAlreadyRegistered) { + return; + } + + db.get('SELECT * FROM verify WHERE username = ? OR email = ?', [username, email]) + .then(verify => { + if (verify) { + // Verify if the last verification token is still valid + if (Date.now() - new Date(verify.createdAt).getTime() < 24 * 60 * 60 * 1000) { + isAlreadyRegistered = true; + return res.status(409).json({ message: 'Verification already sent, please check your email' }); + } + else { + // If the token is expired, delete it + db.run('DELETE FROM verify WHERE id = ?', [verify.id]) + .catch(err => { + console.error('Database error:', err); + return res.status(500).json({ message: 'Internal server error' }); + }); + } + } + }) + .catch(err => { + console.error('Database error:', err); + return res.status(500).json({ message: 'Internal server error' }); + }); + + if (isAlreadyRegistered) { + return; + } + + const verificationToken = Math.random().toString(36).substring(2, 15); + db.run('INSERT INTO verify (token, username, email, password) VALUES (?, ?, ?, ?)', [verificationToken, username, email, hashedPassword]) + .then(() => { + // read the email template + const emailTemplate = fs.readFileSync('mailFile/mail.html', 'utf8'); + + // replace placeholders in the email template + const emailContent = emailTemplate.replace('{{verification_link}}', `${process.env.URL}/verify?token=${verificationToken}`); + + sendMail(email, 'Welcome to Our Service', emailContent); + res.status(201).json({ message: 'email send' }); + }) + .catch(err => { + console.error('Database error:', err); + return res.status(500).json({ message: 'Internal server error' }); + }); +} +); + +app.get('/verify', (req, res) => { + const { token } = req.query; + + if (!token) { + return res.status(400).json({ message: 'Token is required' }); + } + + res.sendFile('verify.html', { root: 'public' }); +} +); + +app.post('/api/verify', (req, res) => { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ message: 'Token is required' }); + } + + db.get('SELECT * FROM verify WHERE token = ?', [token]) + .then(verify => { + if (!verify) { + return res.status(404).json({ message: 'Invalid or expired token' }); + } + + // Insert user into users table + db.run('INSERT INTO users (username, email, password) VALUES (?, ?, ?)', [verify.username, verify.email, verify.password]) + .then(() => { + // Delete the verification record + db.run('DELETE FROM verify WHERE id = ?', [verify.id]) + .then(() => { + res.status(200).json({ message: 'Email verified successfully' }); + }) + .catch(err => { + console.error('Database error:', err); + res.status(500).json({ message: 'Internal server error' }); + }); + }) + .catch(err => { + console.error('Database error:', err); + res.status(500).json({ message: 'Internal server error' }); + }); + }) + .catch(err => { + console.error('Database error:', err); + res.status(500).json({ message: 'Internal server error' }); + }); +} +); + +app.listen(port, "127.0.0.1", () => { + console.log(`Server is running on localhost:${port}`); +} +); \ No newline at end of file diff --git a/mailFile/mail.html b/mailFile/mail.html new file mode 100644 index 0000000..6057a2b --- /dev/null +++ b/mailFile/mail.html @@ -0,0 +1,25 @@ + + + + + Email Address Verification + + + +
+

Welcome to PengLogin

+

Hello,

+

Thank you for signing up. To complete your registration, please verify your email address by clicking the button below:

+ Verify my email address +

If you did not request this, please ignore this message.

+ +
+ + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d477aae --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "penglogin", + "version": "1.0.0", + "description": "", + "repository": { + "type": "git", + "url": "ssh://git@git.thepenguinontheweb.tech:5900/adminflsu/PengLogin.git" + }, + "license": "ISC", + "author": "", + "type": "module", + "main": "index.js", + "scripts": { + "test": "nodemon index.js", + "run": "node index.js" + }, + "dependencies": { + "dotenv": "^16.5.0", + "nodemailer": "^7.0.3", + "nodemon": "^3.1.10" + } +} diff --git a/public/footer-snippet.html b/public/footer-snippet.html new file mode 100644 index 0000000..68781ee --- /dev/null +++ b/public/footer-snippet.html @@ -0,0 +1,2 @@ +

© 2025 The land of the penguin

+

Contact us at: florian@thepenguinontheweb.tech

\ No newline at end of file diff --git a/public/header-snippet.html b/public/header-snippet.html new file mode 100644 index 0000000..289e0b4 --- /dev/null +++ b/public/header-snippet.html @@ -0,0 +1,13 @@ + + The land of the penguin + + +
+ + + + + +
\ No newline at end of file diff --git a/public/imgs/favicon.ico b/public/imgs/favicon.ico new file mode 100644 index 0000000..8a188c5 Binary files /dev/null and b/public/imgs/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c0b7078 --- /dev/null +++ b/public/index.html @@ -0,0 +1,40 @@ + + + + + + + PengLogin + + + + + + + + + +
+
+

LOGIN

+
+ + + + + + + +
+ + Create account. +
+
+ + + + \ No newline at end of file diff --git a/public/register.html b/public/register.html new file mode 100644 index 0000000..3391869 --- /dev/null +++ b/public/register.html @@ -0,0 +1,46 @@ + + + + + + + PengRegister + + + + + + + + + +
+
+

Register

+
+ + + + + + + + + + + + + +
+ + Login Page. +
+
+ + + + \ No newline at end of file diff --git a/public/scripts/common.js b/public/scripts/common.js new file mode 100644 index 0000000..a98817c --- /dev/null +++ b/public/scripts/common.js @@ -0,0 +1,20 @@ +const headerElement = document.querySelector('header'); +const footerElement = document.querySelector('footer'); + +async function loadHeaderFooter() { + try { + const headerResponse = await fetch('/header-snippet.html'); + if (!headerResponse.ok) throw new Error('Failed to load header'); + const headerHTML = await headerResponse.text(); + headerElement.innerHTML = headerHTML; + + const footerResponse = await fetch('/footer-snippet.html'); + if (!footerResponse.ok) throw new Error('Failed to load footer'); + const footerHTML = await footerResponse.text(); + footerElement.innerHTML = footerHTML; + } catch (error) { + console.error('Error loading header/footer:', error); + } +} + +loadHeaderFooter(); \ No newline at end of file diff --git a/public/scripts/index.js b/public/scripts/index.js new file mode 100644 index 0000000..2215ea3 --- /dev/null +++ b/public/scripts/index.js @@ -0,0 +1,29 @@ +const loginForm = document.getElementById('login-form'); + +loginForm.addEventListener('submit', async (event) => { + event.preventDefault(); + + const formData = new FormData(loginForm); + const data = Object.fromEntries(formData.entries()); + + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + if (response.ok) { + alert('Login successful!'); + } else { + const errorData = await response.json(); + alert(`Login failed: ${errorData.message}`); + } + } catch (error) { + console.error('Error during login:', error); + alert('An error occurred while trying to log in.'); + } +} +); \ No newline at end of file diff --git a/public/scripts/register.js b/public/scripts/register.js new file mode 100644 index 0000000..32c0ea9 --- /dev/null +++ b/public/scripts/register.js @@ -0,0 +1,35 @@ +const registerForm = document.getElementById('register-form'); + +registerForm.addEventListener('submit', async (event) => { + event.preventDefault(); + + const formData = new FormData(registerForm); + const data = Object.fromEntries(formData.entries()); + + if (data.password !== data.confirmPassword) { + alert('Passwords do not match. Please try again.'); + return; + } + + try { + const response = await fetch('/api/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + if (response.ok) { + alert('Mail send, verify your mail.'); + window.location.href = '/login'; // Redirect to login page + } else { + const errorData = await response.json(); + alert(`Registration failed: ${errorData.message}`); + } + } catch (error) { + console.error('Error during registration:', error); + alert('An error occurred while trying to register.'); + } + } +); \ No newline at end of file diff --git a/public/scripts/verify.js b/public/scripts/verify.js new file mode 100644 index 0000000..fc3f37d --- /dev/null +++ b/public/scripts/verify.js @@ -0,0 +1,36 @@ +const verifyCard = document.getElementById('verify-card'); +const successMessage = document.getElementById('success-message'); + +const verifyButton = document.getElementById('verify-button'); + +verifyButton.addEventListener('click', async () => { + + + //token in query string + const token = new URLSearchParams(window.location.search).get('token'); + if (!token) { + alert('Verification token is missing. Please check your email for the verification link.'); + return; + } + + try { + const response = await fetch('/api/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: token }) + }); + + if (response.ok) { + successMessage.classList.remove('hidden'); + verifyCard.classList.add('hidden'); + } else { + const errorData = await response.json(); + alert(`Verification failed: ${errorData.message}`); + } + } catch (error) { + console.error('Error during verification:', error); + alert('An error occurred while trying to verify your account.'); + } +}); \ No newline at end of file diff --git a/public/styles/common.css b/public/styles/common.css new file mode 100644 index 0000000..cb44175 --- /dev/null +++ b/public/styles/common.css @@ -0,0 +1,331 @@ +html, +body { + height: 100%; + width: 100%; + /* Fait en sorte que html et body prennent toute la hauteur */ + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + /* Affichage en colonne */ + align-items: center; + justify-content: space-between; + /* Centre le contenu verticalement et horizontalement */ + + min-height: 100vh; + + /* S'assure que le body prend au minimum toute la hauteur de la vue */ + a { + text-decoration: none; + /* Enlève le soulignement des liens */ + color: inherit; + /* Hérite de la couleur du parent */ + } +} + +.body { + background: repeating-conic-gradient(from 45deg, + #a37fb1 0% 25%, + #604566 0% 50%); + background-size: max(10vw, 10svh) max(10vw, 10svh); + + display: flex; + /* Transforme le body en conteneur flex */ + flex-direction: column; + /* Affichage en colonne */ + min-height: 100vh; + /* S'assure que le body prend au minimum toute la hauteur de la vue */ + + animation: background 0.5s ease-in-out; + /* Animation de transition pour le fond */ +} + + +header { + font-size: 20px; + height: 90px; + min-height: 90px; + width: calc(100% - 80px); + color: #C99CCF; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + flex-direction: row; + background: #433147d8; + margin: 20px; + border-radius: 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + -o-border-radius: 10px; +} + +.blur::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + /* Pour Safari */ + z-index: -1; + /* Assure que le fond est derrière le contenu */ +} + +.blur { + position: relative; + /* Nécessaire pour le positionnement du pseudo-élément */ +} + +footer { + font-size: 20px; + height: 90px; + min-height: 90px; + width: calc(100% - 80px); + + + color: rgb(201, 156, 207); + + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + flex-direction: row; + + background: #433147d8; + + margin: 20px; + border-radius: 10px; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + -o-border-radius: 10px; + + font-size: 0.875rem; + + p { + width: 100%; + text-align: center; + } +} + +main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + height: auto; +} + +.card-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + height: auto; + + gap: 20px; + /* Espace entre les cartes */ +} + +@media (max-width: 768px) { + .card-container { + flex-direction: column; + } +} + +.card { + border-radius: 10px; + padding: 20px; + min-width: 300px; + width: auto; + min-height: 400px; + height: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + border-radius: 10px; + + text-decoration: none; + color: rgb(201, 156, 207); + + background: #433147d8; + + transition: background 0.3s ease-in-out, transform 0.3s ease-in-out, margin 0.3s ease-in-out; + + cursor: pointer; + + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} + +.card:hover { + transform: scale(1.05); + transition: background 0.3s ease-in-out, transform 0.3s ease-in-out, margin 0.3s ease-in-out; + + margin: 7px; +} + +.card::before { + border-radius: 10px; +} + + + +.card-image { + height: 200px; + width: 200px; + + border-radius: 10px; +} + +.presentation { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + height: auto; + width: 1000px; + cursor: default; + + gap: 30px; + /* Espace entre les éléments de la carte */ + + h1 { + color: white; + } +} + +.info.card { + min-width: 500px; + width: auto; + height: auto; + + cursor: default; + + h2 { + font-size: 20px; + color: rgb(255, 255, 255); + text-align: center; + } + + p { + font-size: 16px; + text-align: center; + } + + .card-container { + display: grid; + grid-template-columns: repeat(5, 1fr); + /* 2 colonnes de largeur égale */ + gap: 20px; + width: 1400px; + + .card { + cursor: default; + + h3 { + font-size: 18px; + color: rgb(255, 255, 255); + text-align: center; + } + + background-color: #FFFFFF00; + width: calc(100% - 40px); + /* Les cartes prennent toute la largeur de la colonne */ + height: 180px; + /* Hauteur automatique pour s'adapter au contenu */ + } + } +} + +.form { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 100%; + height: auto; + + gap: 20px; + /* Espace entre les éléments du formulaire */ + + label { + width: 90%; + } + + input, + textarea { + width: 90%; + max-width: 500px; + /* Largeur maximale pour les champs de saisie */ + padding: 10px; + border-radius: 5px; + border: none; + background-color: #C99CCF30; + color: white; + font-size: 16px; + margin-bottom: 10px; + /* Espace entre les champs */ + } + + button { + padding: 10px 20px; + border-radius: 5px; + border: none; + background-color: #C99CCF; + color: white; + font-size: 16px; + cursor: pointer; + + transition: background-color 0.3s ease-in-out; + + &:hover { + background-color: #A37FB1; + /* Couleur au survol */ + } + } +} + +.arrow-icon { + width: 50px; + height: auto; + + cursor: pointer; + + path:nth-child(2) { + d: path('M14.5 4H3.5H4'); + } + + path { + transition: 0.25s ease; + } + + &:hover { + path:nth-child(1) { + d: path('M15 4H4V4'); + } + + path:nth-child(2) { + d: path('M14.5 4H3.5H0'); + transform: translateX(4px); + } + + path:nth-child(3) { + transform: translateX(4px); + } + } +} + +.hidden { + display: none; + /* Cache l'élément */ +} \ No newline at end of file diff --git a/public/styles/index.css b/public/styles/index.css new file mode 100644 index 0000000..e69de29 diff --git a/public/styles/register.css b/public/styles/register.css new file mode 100644 index 0000000..e69de29 diff --git a/public/verify.html b/public/verify.html new file mode 100644 index 0000000..bdb0547 --- /dev/null +++ b/public/verify.html @@ -0,0 +1,38 @@ + + + + + + + PengRegister + + + + + + + + +
+
+

Verify account

+

To verify your account, please enter the verification code sent to your email.

+ + + Register Page. +
+ + +
+ + + + \ No newline at end of file