first lines

This commit is contained in:
florian 2025-06-13 23:25:11 +02:00
commit 91999b0699
18 changed files with 877 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
package-lock.json
db/database.db
.env

0
db/.gitkeep Normal file
View File

236
index.js Normal file
View File

@ -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}`);
}
);

25
mailFile/mail.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Email Address Verification</title>
<style>
body { font-family: Arial, sans-serif; background: #f6f6f6; margin: 0; padding: 0; }
.container { background: #fff; max-width: 600px; margin: 40px auto; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);}
.btn { display: inline-block; padding: 12px 24px; background: #007bff; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 20px;}
.footer { margin-top: 30px; color: #888; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h2>Welcome to PengLogin</h2>
<p>Hello,</p>
<p>Thank you for signing up. To complete your registration, please verify your email address by clicking the button below:</p>
<a href="{{verification_link}}" class="btn">Verify my email address</a>
<p>If you did not request this, please ignore this message.</p>
<div class="footer">
&copy; 2025 PengLogin. All rights reserved.
</div>
</div>
</body>
</html>

22
package.json Normal file
View File

@ -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"
}
}

View File

@ -0,0 +1,2 @@
<p>&copy; 2025 The land of the penguin</p>
<p>Contact us at: <a href="mailto:florian@thepenguninontheweb.tech">florian@thepenguinontheweb.tech</a></p>

View File

@ -0,0 +1,13 @@
<a href="https://thepenguinontheweb.tech/index.html" class="header-link">
The land of the penguin
</a>
<div class="back-button"">
<svg width=" 24" height="8" viewBox="0 0 16 8" fill="none" class="arrow-icon">
<path d="M15 4H4V1" stroke="#C99CCF" />
<path d="M14.5 4H3.5H0" stroke="#C99CCF" />
<path
d="M15.8536 4.35355C16.0488 4.15829 16.0488 3.84171 15.8536 3.64645L12.6716 0.464466C12.4763 0.269204 12.1597 0.269204 11.9645 0.464466C11.7692 0.659728 11.7692 0.976311 11.9645 1.17157L14.7929 4L11.9645 6.82843C11.7692 7.02369 11.7692 7.34027 11.9645 7.53553C12.1597 7.7308 12.4763 7.7308 12.6716 7.53553L15.8536 4.35355ZM15 4.5L15.5 4.5L15.5 3.5L15 3.5L15 4.5Z"
fill="#C99CCF" />
</svg>
</div>

BIN
public/imgs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

40
public/index.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PengLogin</title>
<link rel="stylesheet" href="styles/common.css">
<link rel="stylesheet" href="styles/index.css">
<link rel="icon" href="imgs/favicon.ico" type="image/x-icon">
<script src="scripts/common.js" defer></script>
<script src="scripts/index.js" defer></script>
</head>
<body id="body" class="body">
<header id="header" class="blur">
</header>
<main>
<div class="card blur">
<h2>LOGIN</h2>
<form id="login-form" class="form">
<label for="username or email">Username or email:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Login</button>
</form>
<a href="/register">Create account.</a>
</div>
</main>
<footer id="footer" class="blur">
</footer>
</body>
</html>

46
public/register.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PengRegister</title>
<link rel="stylesheet" href="styles/common.css">
<link rel="stylesheet" href="styles/register.css">
<link rel="icon" href="imgs/favicon.ico" type="image/x-icon">
<script src="scripts/common.js" defer></script>
<script src="scripts/register.js" defer></script>
</head>
<body id="body" class="body">
<header id="header" class="blur">
</header>
<main>
<div class="card blur">
<h2>Register</h2>
<form id="register-form" class="form">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<label for="confirm-password">Confirm Password:</label>
<input type="password" id="confirm-password" name="confirmPassword" required>
<button type="submit">Register</button>
</form>
<a href="/login">Login Page.</a>
</div>
</main>
<footer id="footer" class="blur">
</footer>
</body>
</html>

20
public/scripts/common.js Normal file
View File

@ -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();

29
public/scripts/index.js Normal file
View File

@ -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.');
}
}
);

View File

@ -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.');
}
}
);

36
public/scripts/verify.js Normal file
View File

@ -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.');
}
});

331
public/styles/common.css Normal file
View File

@ -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 */
}

0
public/styles/index.css Normal file
View File

View File

38
public/verify.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PengRegister</title>
<link rel="stylesheet" href="styles/common.css">
<link rel="icon" href="imgs/favicon.ico" type="image/x-icon">
<script src="scripts/common.js" defer></script>
<script src="scripts/verify.js" defer></script>
</head>
<body id="body" class="body">
<header id="header" class="blur">
</header>
<main>
<div class="card blur" id="verify-card">
<h2>Verify account</h2>
<p>To verify your account, please enter the verification code sent to your email.</p>
<button id="verify-button">Verify</button>
<a href="/register">Register Page.</a>
</div>
<div class="card blur hidden" id="success-message">
<h2>Verification Successful!</h2>
<p>Your email has been successfully verified.</p>
<a href="/login">Go to Login Page</a>
</div>
</main>
<footer id="footer" class="blur">
</footer>
</body>
</html>