Notifs + Logout + delete Avatar
This commit is contained in:
@@ -26,6 +26,17 @@ router.post('/login', async(req, res) =>
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.post('/logout', async(req, res) =>
|
||||
{
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
if (!token)
|
||||
return (res.status(401).json({error: 'Missing token'}));
|
||||
|
||||
const result = await authService.logout(token);
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.get('/github', (req, res) => {
|
||||
const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
|
||||
`client_id=${process.env.GITHUB_CLIENT_ID}&` +
|
||||
|
||||
@@ -25,7 +25,7 @@ router.post('/upload', authenticateToken, upload.single('avatar'), async(req, re
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.delete('/', authenticateToken, async(req, res) =>
|
||||
router.delete('/delete', authenticateToken, async(req, res) =>
|
||||
{
|
||||
const result = await avatarService.deleteAvatar(req.user.userId);
|
||||
res.status(result.status).json(result.data);
|
||||
|
||||
@@ -2,6 +2,30 @@ import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import {query} from '../db.js';
|
||||
|
||||
async function logout(token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!token)
|
||||
return ({status: 400, data: {error: 'Missing token'}});
|
||||
try
|
||||
{
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ({status: 401, data: {error: 'Invalid token'}});
|
||||
}
|
||||
|
||||
return ({status: 200, data: {message: 'Logged out'}});
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(err);
|
||||
return ({status: 500, data: {error: 'Server error'}});
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username, password)
|
||||
{
|
||||
try
|
||||
@@ -60,4 +84,4 @@ async function register(username, password)
|
||||
}
|
||||
};
|
||||
|
||||
export default {register, login};
|
||||
export default {register, login, logout};
|
||||
|
||||
@@ -69,6 +69,9 @@ async function deleteAvatar(userId) {
|
||||
if (currentAvatar === null)
|
||||
return ({status: 404, data: {error: 'User not found'}});
|
||||
|
||||
if (currentAvatar === DEFAULT_AVATAR)
|
||||
return ({status: 400, data: {error: 'Cannot delete default avatar'}});
|
||||
|
||||
// Reset the avatar to the default one
|
||||
await setAvatar(DEFAULT_AVATAR, userId);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { windowRegistry } from './core/windows.js';
|
||||
import { LoginWindow } from './windows/login.js';
|
||||
import { LogoutWindow } from './windows/logout.js';
|
||||
import { GlobalChat } from './windows/global_chat.js';
|
||||
import { AvatarWindow } from './windows/avatar.js';
|
||||
import { FriendsWindow } from './windows/friends.js';
|
||||
@@ -32,6 +33,7 @@ class App {
|
||||
new FriendsWindow();
|
||||
new GameRoomWindow();
|
||||
new StatsWindow();
|
||||
new LogoutWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +51,8 @@ class App {
|
||||
'login': 'login',
|
||||
'chat': 'chat',
|
||||
'avatar': 'avatar',
|
||||
'friends': 'friends'
|
||||
'friends': 'friends',
|
||||
'logout': 'logout'
|
||||
};
|
||||
|
||||
// Event delegation on the menu
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
export const API = {
|
||||
AUTH: {
|
||||
LOGIN: '/api/auth/login',
|
||||
LOGOUT: '/api/auth/logout',
|
||||
REGISTER: '/api/auth/register',
|
||||
GITHUB: '/api/auth/github'
|
||||
},
|
||||
AVATAR: {
|
||||
GET: '/api/avatar/me',
|
||||
UPLOAD: '/api/avatar/upload'
|
||||
UPLOAD: '/api/avatar/upload',
|
||||
DELETE: '/api/avatar/delete'
|
||||
},
|
||||
FRIENDS: {
|
||||
LIST: '/api/friends',
|
||||
|
||||
@@ -82,6 +82,7 @@ export const Events = {
|
||||
|
||||
// Avatar
|
||||
AVATAR_UPDATED: 'avatar:updated',
|
||||
AVATAR_DELETED: 'avatar:deleted',
|
||||
|
||||
// Chat
|
||||
CHAT_CONNECTED: 'chat:connected',
|
||||
|
||||
@@ -228,6 +228,56 @@ export class Window {
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
NotficationContainer()
|
||||
{
|
||||
if (document.getElementById('notification-container')) return;
|
||||
|
||||
const container = this.createElement('div');
|
||||
container.id = 'notification-container';
|
||||
Object.assign(container.style, {
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
showNotification(message, color) {
|
||||
this.NotficationContainer();
|
||||
const container = document.getElementById('notification-container');
|
||||
if (!container) return;
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
Object.assign(notification.style, {
|
||||
backgroundColor: color,
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '5px',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
||||
opacity: '0',
|
||||
transform: 'translateY(-8px)',
|
||||
transition: 'opacity 0.5s ease, transform 0.5s ease'
|
||||
});
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => notification.remove(), 500);
|
||||
}, 2200);
|
||||
}
|
||||
}
|
||||
|
||||
// Export old class name for compatibility (alias)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<button class="menu__item" data-action="chat" aria-label="Global chat">Global chat</button>
|
||||
<button class="menu__item" data-action="avatar" aria-label="Avatar">Avatar</button>
|
||||
<button class="menu__item" data-action="friends" aria-label="Amis">Amis</button>
|
||||
<button class="menu__item" data-action="logout" aria-label="Logout">Logout</button>
|
||||
</nav>
|
||||
|
||||
<nav class="game" aria-label="Game">
|
||||
|
||||
@@ -67,8 +67,12 @@ export class AvatarWindow extends Window {
|
||||
this.refreshBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], {
|
||||
text: 'Refresh'
|
||||
});
|
||||
|
||||
this.deleteBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], {
|
||||
text: 'Delete avatar'
|
||||
});
|
||||
|
||||
this.controls.append(this.statsBtn, this.chooseBtn, this.saveBtn, this.refreshBtn);
|
||||
this.controls.append(this.statsBtn, this.chooseBtn, this.saveBtn, this.refreshBtn, this.deleteBtn);
|
||||
|
||||
// Feedback message
|
||||
this.message = this.createElement('div', CSS.MESSAGE);
|
||||
@@ -93,6 +97,7 @@ export class AvatarWindow extends Window {
|
||||
this.chooseBtn.addEventListener('click', () => this.fileInput.click());
|
||||
this.saveBtn.addEventListener('click', () => this.uploadAvatar());
|
||||
this.refreshBtn.addEventListener('click', () => this.loadAvatar());
|
||||
this.deleteBtn.addEventListener('click', () => this.deleteAvatar());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,12 +217,14 @@ export class AvatarWindow extends Window {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.showMessage('You must be logged in', 'error');
|
||||
this.showNotification('You must be logged in to change your avatar', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = this.fileInput.files?.[0];
|
||||
if (!file) {
|
||||
this.showMessage('Select an image first', 'error');
|
||||
this.showNotification('Please select an image to upload', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,6 +247,7 @@ export class AvatarWindow extends Window {
|
||||
if (!response.ok) {
|
||||
const errorMsg = data?.error || data?.message || 'Upload failed';
|
||||
this.showMessage(errorMsg, 'error');
|
||||
this.showNotification('Failed to upload avatar.', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -248,11 +256,47 @@ export class AvatarWindow extends Window {
|
||||
}
|
||||
|
||||
this.showMessage('Avatar saved!', 'success');
|
||||
this.showNotification('Avatar updated successfully!', 'green');
|
||||
eventBus.emit(Events.AVATAR_UPDATED, { url: data?.avatar_url });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Avatar upload error:', error);
|
||||
this.showMessage('Upload error', 'error');
|
||||
this.showNotification('Failed to upload avatar.', 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.showMessage('You must be logged in', 'error');
|
||||
this.showNotification('You must be logged in to delete your avatar', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API.AVATAR.DELETE, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.showMessage('Failed to delete avatar', 'error');
|
||||
this.showNotification('Failed to delete avatar.', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
this.preview.src = '';
|
||||
this.showMessage('Avatar deleted!', 'success');
|
||||
this.showNotification('Avatar deleted successfully!', 'green');
|
||||
eventBus.emit(Events.AVATAR_DELETED);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Avatar delete error:', error);
|
||||
this.showMessage('Delete error', 'error');
|
||||
this.showNotification('Failed to delete avatar.', 'red');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -840,17 +840,20 @@ export class GameRoomWindow extends Window {
|
||||
const name = this.roomNameInput.value.trim();
|
||||
if (!name) {
|
||||
this.showMessage('Entrez un nom pour le salon', 'error');
|
||||
this.showNotification('Entrez un nom pour le salon', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.showMessage('Connectez-vous pour creer un salon', 'info');
|
||||
this.showNotification('Connectez-vous pour créer un salon', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentRoom) {
|
||||
this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
|
||||
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,6 +867,7 @@ export class GameRoomWindow extends Window {
|
||||
this.currentRoom = currentData;
|
||||
this.enterLobby(currentData);
|
||||
this.showMessage('Vous etes deja dans un salon', 'error');
|
||||
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -884,6 +888,7 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
if (this.roomNameExists(name)) {
|
||||
this.showMessage('Un salon avec ce nom existe deja', 'error');
|
||||
this.showNotification('Un salon avec ce nom existe deja', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -905,6 +910,7 @@ export class GameRoomWindow extends Window {
|
||||
this.showMessage('Salon cree', 'success');
|
||||
eventBus.emit(Events.ROOM_CREATED, data);
|
||||
this.enterLobby(data);
|
||||
this.showNotification(`Vous avez créé le salon "${data.name}"`, 'green');
|
||||
} catch (error) {
|
||||
console.error('Create room error:', error);
|
||||
this.showMessage('Erreur de connexion', 'error');
|
||||
@@ -1037,6 +1043,7 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
if (this.currentRoom) {
|
||||
this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
|
||||
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1050,6 +1057,7 @@ export class GameRoomWindow extends Window {
|
||||
this.currentRoom = currentData;
|
||||
this.enterLobby(currentData);
|
||||
this.showMessage('Vous etes deja dans un salon', 'error');
|
||||
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export class LoginWindow extends Window {
|
||||
this.buildUI();
|
||||
this.bindEvents();
|
||||
this.checkIfAlreadyLoggedIn();
|
||||
this.NotficationContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,55 +220,6 @@ export class LoginWindow extends Window {
|
||||
window.addEventListener('message', handleMessage, { once: true });
|
||||
}
|
||||
|
||||
NotficationContainer()
|
||||
{
|
||||
if (document.getElementById('notification-container')) return;
|
||||
|
||||
const container = this.createElement('div');
|
||||
container.id = 'notification-container';
|
||||
Object.assign(container.style, {
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
showNotification(message, color) {
|
||||
const container = document.getElementById('notification-container');
|
||||
if (!container) return;
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
Object.assign(notification.style, {
|
||||
backgroundColor: color,
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '5px',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
||||
opacity: '0',
|
||||
transform: 'translateY(-8px)',
|
||||
transition: 'opacity 0.5s ease, transform 0.5s ease'
|
||||
});
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => notification.remove(), 500);
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a feedback message
|
||||
* @param {string} text - Message text
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Window } from '../core/windows.js';
|
||||
import { API, STORAGE_KEYS, CSS } from '../core/config.js';
|
||||
import { eventBus, Events } from '../core/events.js';
|
||||
|
||||
export class LogoutWindow extends Window {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'logout',
|
||||
title: 'Logout',
|
||||
cssClasses: ['logout-window']
|
||||
});
|
||||
|
||||
this.buildUI();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
buildUI() {
|
||||
this.text = this.createElement('div', 'logout__text', {
|
||||
text: 'Are you sure you want to log out?'
|
||||
});
|
||||
this.actions = this.createElement('div', 'logout__actions');
|
||||
|
||||
this.yesBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], {
|
||||
text: 'Yes'
|
||||
});
|
||||
this.noBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], {
|
||||
text: 'No'
|
||||
});
|
||||
|
||||
this.actions.append(this.yesBtn, this.noBtn);
|
||||
this.body.append(this.text, this.actions);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.yesBtn.addEventListener('click', () => this.confirmLogout());
|
||||
this.noBtn.addEventListener('click', () => this.hide());
|
||||
}
|
||||
|
||||
show () {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.text.textContent = 'You need to login first';
|
||||
this.yesBtn.style.display = 'none';
|
||||
this.noBtn.textContent = 'OK';
|
||||
} else {
|
||||
this.text.textContent = 'Are you sure you want to log out?';
|
||||
this.yesBtn.style.display = 'inline-flex';
|
||||
this.noBtn.textContent = 'No';
|
||||
}
|
||||
super.show();
|
||||
}
|
||||
|
||||
async confirmLogout() {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await fetch(API.AUTH.LOGOUT, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.warn('Logout failed:', err);
|
||||
this.showNotification('Logout failed. Please try again.', 'red');
|
||||
return;
|
||||
}
|
||||
}
|
||||
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
eventBus.emit(Events.USER_LOGGED_OUT);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
this.showNotification('You have been logged out successfully.', 'green');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user