This commit is contained in:
sionow
2026-01-27 15:04:04 +01:00
parent 9e4c84f01b
commit ee73bcc35a
37 changed files with 9 additions and 0 deletions
@@ -0,0 +1,63 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import {query} from '../db.js';
async function login(username, password)
{
try
{
const result = await query
(
`SELECT id, password_hash FROM users WHERE username = $1`,
[username]
);
if (result.rows.length === 0)
return ({status: 401, data: {error: 'Invalid credentials'}});
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match)
return ({status: 401, data: {error: 'Invalid credentials'}});
const token = jwt.sign
(
{
userId: user.id,
username: username
},
process.env.JWT_SECRET,
{expiresIn: '1h'}
);
return ({status: 200, data: {token}});
}
catch (err)
{
console.error(err);
return ({status: 500, data: {error: 'Server error'}});
}
};
async function register(username, password)
{
try
{
const password_hash = await bcrypt.hash(password, 10);
await query
(
`INSERT INTO users (username, password_hash) VALUES ($1, $2)`,
[username, password_hash]
);
return ({status: 201, data: {message: 'User created'}});
}
catch (err)
{
if (err.code === '23505')
return ({status: 409, data: {error: 'Username already exists'}});
console.error(err);
return ({status: 500, data: {error: 'Server error'}});
}
};
export default {register, login};
@@ -0,0 +1,148 @@
import {query} from '../db.js';
import path from 'path';
import fs from 'fs';
import {fileURLToPath} from 'url';
import {fileTypeFromBuffer} from 'file-type';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const AVATAR_DIR = path.join(__dirname, '../avatar');
const DEFAULT_AVATAR = '/avatar/default.png';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5mb
const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
// Verify that the avatar folder already exists
if (!fs.existsSync(AVATAR_DIR))
{
fs.mkdirSync(AVATAR_DIR, { recursive: true });
}
async function uploadAvatar(userId, file)
{
try
{
// File check (type and size)
if (!file)
return ({status: 400, data: {error: 'No file provided'}});
if (!ALLOWED_TYPES.includes(file.mimetype))
return ({status: 400, data: { error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP allowed'}});
const fileType = await fileTypeFromBuffer(file.buffer);
if (!fileType || !['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(fileType.mime))
return ({status: 400, data: {error: 'Invalid file content. File does not match allowed image types'}});
if (file.size > MAX_FILE_SIZE)
return ({status: 400, data: {error: 'File too large. Maximum size is 5MB'}});
const currentAvatar = await getCurrentAvatar(userId);
if (currentAvatar === null)
return ({status: 404, data: {error: 'User not found'}});
// Create a unique name for the new avatar to avoid duplicates
const fileExt = path.extname(file.originalname);
const fileName = `user_${userId}_${Date.now()}${fileExt}`;
const avatarPath = `/avatar/${fileName}`;
// Save the new avatar in the folder
const filePath = path.join(AVATAR_DIR, fileName);
fs.writeFileSync(filePath, file.buffer);
await setAvatar(avatarPath, userId);
deleteNonDefault(currentAvatar);
return ({status: 200, data: {avatar_url: avatarPath}});
}
catch (err)
{
console.error('Avatar upload error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
async function deleteAvatar(userId) {
try
{
const currentAvatar = await getCurrentAvatar(userId);
if (currentAvatar === null)
return ({status: 404, data: {error: 'User not found'}});
// Reset the avatar to the default one
await setAvatar(DEFAULT_AVATAR, userId);
deleteNonDefault(currentAvatar);
return ({status: 200, data: {avatar_url: DEFAULT_AVATAR}});
}
catch (err)
{
console.error('Avatar delete error:', err);
return ({status: 500, data: {error: 'Server error'}});
}
}
async function setAvatar(newAvatar, userId)
{
await query
(
'UPDATE users SET avatar_url = $1 WHERE id = $2',
[newAvatar, userId]
);
}
async function getCurrentAvatar(userId)
{
const res = await query
(
'SELECT avatar_url FROM users WHERE id = $1',
[userId]
);
if (res.rows.length === 0)
return (null);
return (res.rows[0].avatar_url);
}
function deleteNonDefault(curAvatar)
{
if (curAvatar && curAvatar !== DEFAULT_AVATAR)
{
const fileName = path.basename(curAvatar);
const filePath = path.join(AVATAR_DIR, fileName);
if (fs.existsSync(filePath))
fs.unlinkSync(filePath);
}
}
async function getAvatarUrl(userId)
{
try
{
const result = await query
(
'SELECT avatar_url FROM users WHERE id = $1',
[userId]
);
if (result.rows.length === 0)
return ({status: 404, data: {error: 'User not found'}});
const avatarUrl = result.rows[0].avatar_url || DEFAULT_AVATAR;
return ({status: 200, data: {avatar_url: avatarUrl}});
}
catch (err)
{
console.error('Get avatar error:', err);
return ({status: 500, data: {error: 'Server error'}});
}
}
export default
{
uploadAvatar,
deleteAvatar,
getAvatarUrl,
AVATAR_DIR,
DEFAULT_AVATAR
};
@@ -0,0 +1,240 @@
import { query } from '../db.js';
/**
* Get list of friends for a user
*/
async function getFriends(userId) {
try {
const result = await query(
`SELECT u.id, u.username, u.avatar_url
FROM friendship f
JOIN users u ON (
CASE
WHEN f.id_user1 = $1 THEN f.id_user2 = u.id
ELSE f.id_user1 = u.id
END
)
WHERE (f.id_user1 = $1 OR f.id_user2 = $1)
AND f.status = 'accepted'`,
[userId]
);
return { status: 200, data: { friends: result.rows } };
} catch (err) {
console.error('Get friends error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Get pending friend requests received by user
*/
async function getPendingRequests(userId) {
try {
const result = await query(
`SELECT u.id, u.username, u.avatar_url, f.created_at
FROM friendship f
JOIN users u ON (
CASE
WHEN f.id_user1 = $1 THEN f.id_user2 = u.id
ELSE f.id_user1 = u.id
END
)
WHERE (f.id_user1 = $1 OR f.id_user2 = $1)
AND f.status = 'pending_' || $1::text`,
[userId]
);
return { status: 200, data: { requests: result.rows } };
} catch (err) {
console.error('Get pending requests error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Search users by username
*/
async function searchUsers(userId, searchTerm) {
try {
const result = await query(
`SELECT id, username, avatar_url
FROM users
WHERE username ILIKE $1
AND id != $2
LIMIT 20`,
[`%${searchTerm}%`, userId]
);
return { status: 200, data: { users: result.rows } };
} catch (err) {
console.error('Search users error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Send a friend request
*/
async function sendFriendRequest(fromUserId, toUserId) {
try {
if (fromUserId === toUserId) {
return { status: 400, data: { error: 'Cannot add yourself as friend' } };
}
// Check if user exists
const userCheck = await query('SELECT id FROM users WHERE id = $1', [toUserId]);
if (userCheck.rows.length === 0) {
return { status: 404, data: { error: 'User not found' } };
}
// Ensure id_user1 < id_user2 for the constraint
const id1 = Math.min(fromUserId, toUserId);
const id2 = Math.max(fromUserId, toUserId);
// Check existing friendship
const existing = await query(
'SELECT status FROM friendship WHERE id_user1 = $1 AND id_user2 = $2',
[id1, id2]
);
if (existing.rows.length > 0) {
const status = existing.rows[0].status;
if (status === 'accepted') {
return { status: 400, data: { error: 'Already friends' } };
}
if (status.startsWith('pending_')) {
return { status: 400, data: { error: 'Friend request already exists' } };
}
}
// Status indicates who needs to accept: pending_<receiver_id>
const status = `pending_${toUserId}`;
await query(
`INSERT INTO friendship (id_user1, id_user2, status)
VALUES ($1, $2, $3)
ON CONFLICT (id_user1, id_user2)
DO UPDATE SET status = $3`,
[id1, id2, status]
);
return { status: 200, data: { message: 'Friend request sent' } };
} catch (err) {
console.error('Send friend request error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Accept a friend request
*/
async function acceptFriendRequest(userId, fromUserId) {
try {
const id1 = Math.min(userId, fromUserId);
const id2 = Math.max(userId, fromUserId);
const result = await query(
`UPDATE friendship
SET status = 'accepted'
WHERE id_user1 = $1 AND id_user2 = $2
AND status = $3
RETURNING *`,
[id1, id2, `pending_${userId}`]
);
if (result.rows.length === 0) {
return { status: 404, data: { error: 'Friend request not found' } };
}
return { status: 200, data: { message: 'Friend request accepted' } };
} catch (err) {
console.error('Accept friend request error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Decline a friend request
*/
async function declineFriendRequest(userId, fromUserId) {
try {
const id1 = Math.min(userId, fromUserId);
const id2 = Math.max(userId, fromUserId);
const result = await query(
`DELETE FROM friendship
WHERE id_user1 = $1 AND id_user2 = $2
AND status = $3
RETURNING *`,
[id1, id2, `pending_${userId}`]
);
if (result.rows.length === 0) {
return { status: 404, data: { error: 'Friend request not found' } };
}
return { status: 200, data: { message: 'Friend request declined' } };
} catch (err) {
console.error('Decline friend request error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
/**
* Get list of friend IDs for a user (for quick lookup)
*/
async function getFriendIds(userId) {
try {
const result = await query(
`SELECT
CASE
WHEN f.id_user1 = $1 THEN f.id_user2
ELSE f.id_user1
END as friend_id
FROM friendship f
WHERE (f.id_user1 = $1 OR f.id_user2 = $1)
AND f.status = 'accepted'`,
[userId]
);
return result.rows.map(row => row.friend_id);
} catch (err) {
console.error('Get friend IDs error:', err);
return [];
}
}
/**
* Remove a friend
*/
async function removeFriend(userId, friendId) {
try {
const id1 = Math.min(userId, friendId);
const id2 = Math.max(userId, friendId);
const result = await query(
`DELETE FROM friendship
WHERE id_user1 = $1 AND id_user2 = $2
AND status = 'accepted'
RETURNING *`,
[id1, id2]
);
if (result.rows.length === 0) {
return { status: 404, data: { error: 'Friendship not found' } };
}
return { status: 200, data: { message: 'Friend removed' } };
} catch (err) {
console.error('Remove friend error:', err);
return { status: 500, data: { error: 'Server error' } };
}
}
export default {
getFriends,
getFriendIds,
getPendingRequests,
searchUsers,
sendFriendRequest,
acceptFriendRequest,
declineFriendRequest,
removeFriend
};
@@ -0,0 +1,119 @@
import {query} from '../db.js';
// Create the room with name as the only parameter
// max_players, status and the other variables have their default values defined in db.js
async function createRoom(name, userId)
{
const result = await query
(
`INSERT INTO game_rooms (name) VALUES ($1) RETURNING *`,
[name]
);
const room = result.rows[0];
await query
(
'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2)',
[room.id, userId]
);
return (room);
}
async function getRoomById(roomId)
{
const result = await query
(
'SELECT * FROM game_rooms WHERE id = $1',
[roomId]
);
return (result.rows[0]);
}
// List all the waiting rooms and the player amount in each of them
async function listActiveRooms()
{
const result = await query
(
`SELECT r.*, COUNT(p.id) as player_count
FROM game_rooms r
LEFT JOIN game_players p ON r.id = p.room_id
WHERE r.status = 'waiting'
GROUP BY r.id
ORDER BY player_count DESC, r.created_at DESC`
);
return (result.rows);
}
async function joinRoom(roomId, userId)
{
const room = await getRoomById(roomId);
if (!room)
throw new Error('Room not found');
const playerCount = await query
(
'SELECT COUNT(*) FROM game_players WHERE room_id = $1',
[roomId]
);
if (parseInt(playerCount.rows[0].count) >= 8)
throw new Error('Room is full');
if (room.status !== 'waiting')
throw new Error('Game already started or ended');
const result = await query
(
'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2) RETURNING *',
[roomId, userId]
);
return (result.rows[0]);
}
async function leaveRoom(roomId, userId)
{
await query
(
'DELETE FROM game_players WHERE room_id = $1 AND user_id = $2',
[roomId, userId]
);
const playerCount = await query
(
'SELECT COUNT(*) FROM game_players WHERE room_id = $1',
[roomId]
);
if (parseInt(playerCount.rows[0].count) === 0)
{
await query
(
'DELETE FROM game_rooms WHERE id = $1',
[roomId]
);
}
}
// List the players in the room and their scores
// Useful for the scoreboard and also tell which player is currently drawing
async function getRoomPlayers(roomId)
{
const result = await query
(
`SELECT gp.*, u.username
FROM game_players gp
JOIN users u ON gp.user_id = u.id
WHERE gp.room_id = $1
ORDER BY gp.score DESC`,
[roomId]
);
return (result.rows);
}
export default
{
createRoom,
getRoomById,
listActiveRooms,
joinRoom,
leaveRoom,
getRoomPlayers
};
@@ -0,0 +1,27 @@
import {query} from '../db.js';
async function saveMessage(userId, content)
{
const result = await query
(
'INSERT INTO messages (sender_id, content) VALUES ($1 ,$2) RETURNING *',
[userId, content]
);
return (result.rows[0]);
}
async function getRecentMessages(limit = 50)
{
const result = await query
(
`SELECT m.sender_id, m.content, m.created_at, u.username
FROM messages m
JOIN users u ON m.sender_id = u.id
ORDER BY m.created_at DESC
LIMIT $1`,
[limit]
);
return (result.rows.reverse());
}
export default {saveMessage, getRecentMessages};
@@ -0,0 +1,73 @@
import jwt from 'jsonwebtoken';
import chatService from './global_chat.js';
import friendsService from './friends.js';
function setupSocketIO(io)
{
io.use((socket, next) =>
{
const token = socket.handshake.auth.token;
if (!token)
return (next(new Error('Authentication error')));
try
{
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.user = decoded;
next();
}
catch(err)
{
next(new Error('Authentication error'));
}
});
io.on('connection', async (socket) =>
{
console.log(`User connected: ${socket.user.username}`);
socket.join('general-chat');
// Send recent messages and friend IDs on connection
try {
const [recentMessages, friendIds] = await Promise.all([
chatService.getRecentMessages(50),
friendsService.getFriendIds(socket.user.userId)
]);
socket.emit('chat-init', {
messages: recentMessages,
friendIds: friendIds
});
} catch (err) {
console.error('Error fetching initial data:', err);
}
socket.on('chat-message', async(data) =>
{
try
{
const message = await chatService.saveMessage(socket.user.userId, data.content);
socket.broadcast.to('general-chat').emit('chat-message',
{
id: message.id,
sender_id: socket.user.userId,
username: socket.user.username,
content: message.content,
created_at: message.created_at
});
}
catch (err)
{
console.error('Error saving message:', err);
socket.emit('error', {message: 'Failed to send message'});
}
});
socket.on('disconnect', () =>
{
console.log(`User disconnected: ${socket.user.username}`);
});
});
}
export default setupSocketIO;