You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
21 KiB
561 lines
21 KiB
import bcrypt from "bcrypt";
|
|
import { getClient } from "../utils/database.js";
|
|
import jwt from "jsonwebtoken";
|
|
import path, { dirname } from "path";
|
|
import fs from "fs";
|
|
import { fileURLToPath } from "url";
|
|
import crypto from "crypto";
|
|
import { sendEmail } from "../utils/mail.js";
|
|
|
|
export async function register(req, res) {
|
|
try {
|
|
const user = {
|
|
email: req.body.email,
|
|
username: req.body.username,
|
|
password: req.body.password,
|
|
picture: req.body.picture,
|
|
}
|
|
|
|
const logger = req.body.logger;
|
|
logger.action("try to register a user with username: " + req.body.username + " and email: " + req.body.email, user);
|
|
|
|
user.password = await bcrypt.hash(req.body.password, 10);
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
let file = req.file.buffer;
|
|
if (file) {
|
|
const finalName = user.username + "." + req.file.originalname.split(".")[1];
|
|
const destinationPath = path.join(__dirname, "../uploads/profiles/" + finalName)
|
|
console.log(destinationPath)
|
|
|
|
fs.writeFileSync(destinationPath, file);
|
|
user.picture = "/api/media/profile/" + finalName;
|
|
} else {
|
|
user.picture = "/api/media/profile/default.png";
|
|
}
|
|
|
|
const client = await getClient();
|
|
|
|
const query = `INSERT INTO users (email, username, password, picture) VALUES ($1, $2, $3, $4) RETURNING id`;
|
|
const result = await client.query(query, [user.email, user.username, user.password, user.picture]);
|
|
|
|
const id = result.rows[0].id;
|
|
|
|
const playlistQuery = `INSERT INTO playlists (name, owner) VALUES ('A regarder plus tard', $1)`;
|
|
await client.query(playlistQuery, [id]);
|
|
|
|
// GENERATE EMAIL HEXA VERIFICATION TOKEN
|
|
const token = crypto.randomBytes(32).toString("hex").slice(0, 5);
|
|
|
|
const textMessage = "Merci de vous être inscrit. Veuillez vérifier votre e-mail. Code: " + token;
|
|
|
|
const htmlMessage = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bienvenue sur Freetube</title>
|
|
</head>
|
|
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
|
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
|
<!-- Header -->
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center;">
|
|
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: bold;">
|
|
🎬 Bienvenue sur Freetube!
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div style="padding: 40px 30px;">
|
|
<h2 style="color: #333333; margin-top: 0; font-size: 24px;">
|
|
Bonjour ${user.username}! 👋
|
|
</h2>
|
|
|
|
<p style="color: #666666; font-size: 16px; line-height: 1.6; margin: 20px 0;">
|
|
Merci de vous être inscrit sur <strong>Freetube</strong>! Nous sommes ravis de vous accueillir dans notre communauté.
|
|
</p>
|
|
|
|
<p style="color: #666666; font-size: 16px; line-height: 1.6; margin: 20px 0;">
|
|
Pour finaliser votre inscription, veuillez utiliser le code de vérification ci-dessous :
|
|
</p>
|
|
|
|
<!-- Verification Code Box -->
|
|
<div style="background-color: #f8f9fa; border: 2px dashed #667eea; border-radius: 8px; padding: 25px; text-align: center; margin: 30px 0;">
|
|
<p style="color: #333333; font-size: 14px; margin: 0 0 10px 0; text-transform: uppercase; letter-spacing: 1px;">
|
|
Code de vérification
|
|
</p>
|
|
<div style="background-color: #667eea; color: #ffffff; font-size: 32px; font-weight: bold; padding: 15px 25px; border-radius: 6px; letter-spacing: 3px; display: inline-block;">
|
|
${token}
|
|
</div>
|
|
</div>
|
|
|
|
<p style="color: #999999; font-size: 14px; line-height: 1.5; margin: 30px 0 10px 0;">
|
|
⚠️ Ce code expirera dans <strong>1 heure</strong>.
|
|
</p>
|
|
|
|
<p style="color: #999999; font-size: 14px; line-height: 1.5; margin: 10px 0;">
|
|
Si vous n'avez pas créé de compte, vous pouvez ignorer cet e-mail.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div style="background-color: #f8f9fa; padding: 20px 30px; border-top: 1px solid #eee;">
|
|
<p style="color: #999999; font-size: 12px; margin: 0; text-align: center;">
|
|
© 2025 Freetube. Tous droits réservés.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
console.log("Sending email to:", user.email);
|
|
|
|
await sendEmail(user.email, "🎬 Bienvenue sur Freetube - Vérifiez votre email", textMessage, htmlMessage);
|
|
|
|
// Store the token in the database
|
|
const expirationDate = new Date();
|
|
expirationDate.setHours(expirationDate.getHours() + 1); // Token expires in 1 hour
|
|
|
|
const insertQuery = `INSERT INTO email_verification (email, token, expires_at) VALUES ($1, $2, $3)`;
|
|
await client.query(insertQuery, [user.email, token, expirationDate]);
|
|
|
|
console.log("Successfully registered");
|
|
logger.write("successfully registered", 200);
|
|
res.status(200).send({ user: user });
|
|
} catch (err) {
|
|
console.log(err);
|
|
logger?.write("failed to register user", 500);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
export async function verifyEmail(req, res) {
|
|
const { email, token } = req.body;
|
|
|
|
const logger = req.body.logger;
|
|
logger.action("try to verify email for " + email + " with token " + token);
|
|
|
|
const client = await getClient();
|
|
|
|
try {
|
|
const query = `SELECT * FROM email_verification WHERE email = $1 AND token = $2`;
|
|
const result = await client.query(query, [email, token]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.write("failed to verify email for " + email, 404);
|
|
return res.status(404).json({ error: "Invalid token or email" });
|
|
}
|
|
|
|
// If we reach this point, the email is verified
|
|
|
|
const queryDelete = `DELETE FROM email_verification WHERE email = $1`;
|
|
await client.query(queryDelete, [email]);
|
|
|
|
const updateQuery = `UPDATE users SET is_verified = TRUE WHERE email = $1`;
|
|
await client.query(updateQuery, [email]);
|
|
|
|
logger.write("successfully verified email for " + email, 200);
|
|
res.status(200).json({ message: "Email verified successfully" });
|
|
} catch (error) {
|
|
console.error("Error verifying email:", error);
|
|
logger.write("failed to verify email for " + email, 500);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
export async function login(req, res) {
|
|
const user = {
|
|
username: req.body.username,
|
|
password: req.body.password,
|
|
}
|
|
|
|
const logger = req.body.logger;
|
|
logger.action("try to login with username '" + user.username + "'");
|
|
|
|
const client = await getClient();
|
|
|
|
try {
|
|
let query = `SELECT id, username, email, picture, password FROM users WHERE username = $1`;
|
|
const result = await client.query(query, [user.username]);
|
|
|
|
const userInBase = result.rows[0];
|
|
|
|
if (!userInBase) {
|
|
logger.write("failed to login", 401)
|
|
res.status(401).json({ error: "Invalid credentials" });
|
|
return
|
|
}
|
|
|
|
const isPasswordValid = await bcrypt.compare(req.body.password, userInBase.password);
|
|
|
|
if (!isPasswordValid) {
|
|
logger.write("failed to login", 401)
|
|
res.status(401).json({ error: "Invalid credentials" });
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
id: userInBase.id,
|
|
username: userInBase.username,
|
|
}
|
|
|
|
const token = jwt.sign(payload, process.env.JWT_SECRET);
|
|
|
|
const userData = {
|
|
id: userInBase.id,
|
|
username: userInBase.username,
|
|
email: userInBase.email,
|
|
picture: userInBase.picture
|
|
}
|
|
|
|
logger.write("Successfully logged in", 200);
|
|
res.status(200).json({ token: token, user: userData });
|
|
} catch (err) {
|
|
console.log(err);
|
|
logger?.write("failed to login", 500);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
export async function getById(req, res) {
|
|
const id = req.params.id;
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve user " + id);
|
|
const client = await getClient();
|
|
const query = `SELECT id, email, username, picture
|
|
FROM users
|
|
WHERE id = $1`;
|
|
const result = await client.query(query, [id]);
|
|
if (!result.rows[0]) {
|
|
logger.write("failed to retrieve user " + id + " because it doesn't exist", 404);
|
|
client.release()
|
|
res.status(404).json({ error: "Not Found" });
|
|
return
|
|
}
|
|
logger.write("successfully retrieved user " + id, 200);
|
|
if (result.rows[0].picture) {
|
|
return res.status(200).json({ user: result.rows[0] });
|
|
}
|
|
}
|
|
|
|
export async function getByUsername(req, res) {
|
|
const username = req.params.username;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve user " + username);
|
|
const query = `SELECT id, email, username, picture FROM users WHERE username = $1`;
|
|
const result = await client.query(query, [username]);
|
|
if (!result.rows[0]) {
|
|
logger.write("failed to retrieve user " + username + " because it doesn't exist", 404);
|
|
client.release()
|
|
res.status(404).json({ error: "Not Found" });
|
|
return
|
|
}
|
|
logger.write("successfully retrieved user " + username, 200);
|
|
client.release();
|
|
return res.status(200).json({ user: result.rows[0] });
|
|
}
|
|
|
|
export async function update(req, res) {
|
|
|
|
try {
|
|
const id = req.params.id;
|
|
let user = {
|
|
email: req.body.email,
|
|
username: req.body.username,
|
|
password: req.body.password,
|
|
picture: req.body.picture,
|
|
}
|
|
|
|
const client = await getClient();
|
|
const userQuery = `SELECT * FROM users WHERE id = $1`;
|
|
const userResult = await client.query(userQuery, [id]);
|
|
const userInBase = userResult.rows[0];
|
|
|
|
const logger = req.body.logger;
|
|
logger.action("try to update user " + id);
|
|
|
|
if (user.email !== userInBase.email) {
|
|
const emailQuery = `SELECT email FROM users WHERE email = $1`;
|
|
const emailResult = await client.query(emailQuery, [user.email]);
|
|
if (emailResult.rows[0]) {
|
|
logger.write("failed to update because email is already used", 400)
|
|
client.release();
|
|
res.status(400).json({ error: "Email already exists" });
|
|
}
|
|
}
|
|
|
|
if (user.username !== userInBase.username) {
|
|
const usernameQuery = `SELECT username FROM users WHERE username = $1`;
|
|
const usernameResult = await client.query(usernameQuery, [user.username]);
|
|
if (usernameResult.rows[0]) {
|
|
logger.write("failed to update because username is already used", 400)
|
|
client.release();
|
|
res.status(400).json({ error: "Username already exists" });
|
|
}
|
|
}
|
|
|
|
if (user.password) {
|
|
const isPasswordValid = await bcrypt.compare(req.body.password, userInBase.password);
|
|
|
|
if (!isPasswordValid) {
|
|
user.password = await bcrypt.hash(req.body.password, 10);
|
|
} else {
|
|
user.password = userInBase.password;
|
|
}
|
|
} else {
|
|
user.password = userInBase.password;
|
|
}
|
|
|
|
let __filename = fileURLToPath(import.meta.url);
|
|
let __dirname = dirname(__filename);
|
|
console.log(__dirname);
|
|
|
|
let profilePicture = userInBase.picture.split("/").pop();
|
|
fs.rename(
|
|
path.join(__dirname, "..", "uploads", "profiles", profilePicture),
|
|
path.join(__dirname, "..", "uploads", "profiles", user.username + "." + profilePicture.split(".").pop()),
|
|
(err) => {
|
|
if (err) {
|
|
logger.write("failed to update profile picture", 500);
|
|
console.error("Error renaming file:", err);
|
|
throw err;
|
|
}
|
|
});
|
|
profilePicture = "/api/media/profile/" + user.username + "." + profilePicture.split(".").pop();
|
|
|
|
const updateQuery = `UPDATE users SET email = $1, username = $2, password = $3, picture = $4 WHERE id = $5 RETURNING id, email, username, picture`;
|
|
const result = await client.query(updateQuery, [user.email, user.username, user.password, profilePicture, id]);
|
|
logger.write("successfully updated user " + id, 200);
|
|
client.release();
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.log(err);
|
|
client.release()
|
|
res.status(500).json({ error: err });
|
|
}
|
|
|
|
}
|
|
|
|
export async function deleteUser(req, res) {
|
|
const id = req.params.id;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to delete user " + id);
|
|
const query = `DELETE FROM users WHERE id = $1`;
|
|
await client.query(query, [id]);
|
|
logger.write("successfully deleted user " + id);
|
|
client.release();
|
|
res.status(200).json({ message: 'User deleted' });
|
|
}
|
|
|
|
export async function getChannel(req, res) {
|
|
const id = req.params.id;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve channel of user " + id);
|
|
|
|
const query = `SELECT * FROM channels WHERE owner = $1`;
|
|
const result = await client.query(query, [id]);
|
|
|
|
if (!result.rows[0]) {
|
|
logger.write("failed to retrieve channel of user " + id + " because it doesn't exist", 404);
|
|
client.release();
|
|
res.status(404).json({ error: "Channel Not Found" });
|
|
return;
|
|
}
|
|
|
|
logger.write("successfully retrieved channel of user " + id, 200);
|
|
client.release();
|
|
res.status(200).json({ channel: result.rows[0] });
|
|
}
|
|
|
|
export async function getHistory(req, res) {
|
|
const id = req.params.id;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve history of user " + id);
|
|
|
|
const query = `SELECT
|
|
v.title, v.thumbnail, v.release_date, v.channel, c.name, u.picture, v.id
|
|
FROM history
|
|
JOIN public.videos v on history.video = v.id
|
|
JOIN public.channels c on v.channel = c.id
|
|
JOIN public.users u on c.owner = u.id
|
|
WHERE user_id = $1
|
|
LIMIT 10
|
|
`;
|
|
|
|
const result = await client.query(query, [id]);
|
|
|
|
const videos = [];
|
|
|
|
for (const video of result.rows) {
|
|
// GET VIDEO VIEW COUNT
|
|
const videoQuery = `SELECT COUNT(*) as view_count FROM history WHERE video = $1`;
|
|
const videoResult = await client.query(videoQuery, [video.id]);
|
|
const videoToAdd = {
|
|
title: video.title,
|
|
thumbnail: video.thumbnail,
|
|
release_date: video.release_date,
|
|
views: videoResult.rows[0].view_count,
|
|
id: video.id,
|
|
creator: {
|
|
id: video.channel,
|
|
name: video.name,
|
|
profilePicture: video.picture
|
|
},
|
|
user_picture: video.picture
|
|
};
|
|
videos.push(videoToAdd);
|
|
}
|
|
|
|
if (!result.rows[0]) {
|
|
logger.write("failed to retrieve history of user " + id + " because it doesn't exist", 404);
|
|
res.status(404).json({ error: "History Not Found" });
|
|
client.release();
|
|
return;
|
|
}
|
|
|
|
logger.write("successfully retrieved history of user " + id, 200);
|
|
client.release();
|
|
res.status(200).json(videos);
|
|
}
|
|
|
|
export async function isSubscribed(req, res) {
|
|
const token = req.headers.authorization.split(" ")[1];
|
|
const tokenPayload = jwt.decode(token);
|
|
const userId = tokenPayload.id;
|
|
const channelId = req.params.id;
|
|
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action(`check if user ${userId} is subscribed to channel ${channelId}`);
|
|
|
|
const query = `SELECT * FROM subscriptions WHERE owner = $1 AND channel = $2`;
|
|
const result = await client.query(query, [userId, channelId]);
|
|
|
|
if (result.rows[0]) {
|
|
logger.write(`user ${userId} is subscribed to channel ${channelId}`, 200);
|
|
client.release();
|
|
return res.status(200).json({ subscribed: true });
|
|
} else {
|
|
logger.write(`user ${userId} is not subscribed to channel ${channelId}`, 200);
|
|
client.release();
|
|
return res.status(200).json({ subscribed: false });
|
|
}
|
|
}
|
|
|
|
export async function searchByUsername(req, res) {
|
|
const username = req.query.username;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to search user by username " + username);
|
|
|
|
const query = `SELECT id, username, picture, email, is_verified FROM users WHERE username ILIKE $1`;
|
|
const result = await client.query(query, [`%${username}%`]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.write("no user found with username " + username, 404);
|
|
client.release();
|
|
return res.status(404).json({ error: "User Not Found" });
|
|
}
|
|
|
|
logger.write("successfully found user with username " + username, 200);
|
|
client.release();
|
|
res.status(200).json(result.rows);
|
|
}
|
|
|
|
export async function getAllSubscriptions(req,res) {
|
|
const userId = req.params.id;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve all subscriptions of user " + userId);
|
|
|
|
const query = `
|
|
SELECT
|
|
subscriptions.id,
|
|
channels.id AS channel_id,
|
|
channels.name AS channel_name,
|
|
users.picture
|
|
|
|
FROM
|
|
subscriptions
|
|
LEFT JOIN channels ON subscriptions.channel = channels.id
|
|
LEFT JOIN users ON channels.owner = users.id
|
|
WHERE
|
|
subscriptions.owner = $1
|
|
`;
|
|
const result = await client.query(query, [userId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.write("no subscriptions found for user " + userId, 404);
|
|
client.release();
|
|
return res.status(404).json({ error: "No Subscriptions Found" });
|
|
}
|
|
|
|
logger.write("successfully retrieved all subscriptions of user " + userId, 200);
|
|
client.release();
|
|
res.status(200).json(result.rows);
|
|
}
|
|
|
|
export async function getAllSubscriptionVideos(req, res) {
|
|
const userId = req.params.id;
|
|
const client = await getClient();
|
|
const logger = req.body.logger;
|
|
logger.action("try to retrieve all subscriptions of user " + userId);
|
|
|
|
const query = `
|
|
SELECT
|
|
videos.id,
|
|
videos.title,
|
|
videos.thumbnail,
|
|
channels.id AS channel,
|
|
videos.visibility,
|
|
videos.file,
|
|
videos.format,
|
|
videos.release_date,
|
|
channels.id AS channel_id,
|
|
channels.owner,
|
|
COUNT(history.id) AS views,
|
|
JSON_BUILD_OBJECT(
|
|
'name', channels.name,
|
|
'profilePicture', users.picture,
|
|
'description', channels.description
|
|
) AS creator
|
|
|
|
FROM
|
|
subscriptions
|
|
LEFT JOIN channels ON subscriptions.channel = channels.id
|
|
LEFT JOIN users ON channels.owner = users.id
|
|
LEFT JOIN videos ON channels.id = videos.channel
|
|
LEFT JOIN history ON videos.id = history.video
|
|
|
|
WHERE
|
|
subscriptions.owner = $1
|
|
GROUP BY
|
|
videos.id,
|
|
channels.id,
|
|
users.id;
|
|
`;
|
|
const result = await client.query(query, [userId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
logger.write("no subscriptions found for user " + userId, 404);
|
|
client.release();
|
|
return res.status(404).json({ error: "No Subscriptions Found" });
|
|
}
|
|
|
|
logger.write("successfully retrieved all subscriptions of user " + userId, 200);
|
|
client.release();
|
|
res.status(200).json(result.rows);
|
|
}
|