Collaborative-pixel-art/backend/src/services/UserService.ts
martin 98f290a662 Rewrite with modern stack
Major refactor from simple HTML/JS app to modern full-stack TypeScript application:

## Architecture Changes
- Migrated to monorepo structure with workspaces (backend, frontend, shared)
- Backend: Node.js + Express + TypeScript + Socket.IO
- Frontend: Next.js 15.5 + React 19 + TypeScript + Tailwind CSS
- Shared: Common types and utilities across packages

## Key Features Implemented
- Real-time WebSocket collaboration via Socket.IO
- Virtual canvas with chunked loading for performance
- Modern UI with dark mode and responsive design
- Mock database system for easy development (Redis/PostgreSQL compatible)
- Comprehensive error handling and rate limiting
- User presence and cursor tracking
- Infinite canvas support with zoom/pan controls

## Performance Optimizations
- Canvas virtualization - only renders visible viewport
- Chunked pixel data loading (64x64 pixel chunks)
- Optimized WebSocket protocol
- Memory-efficient state management with Zustand

## Development Experience
- Full TypeScript support across all packages
- Hot reload for both frontend and backend
- Docker support for production deployment
- Comprehensive linting and formatting
- Automated development server startup

## Fixed Issues
- Corrected start script paths
- Updated environment configuration
- Fixed ESLint configuration issues
- Ensured all dependencies are properly installed
- Verified build process works correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 19:28:05 +02:00

202 lines
No EOL
5.2 KiB
TypeScript

import { pgPool } from '../config/database-factory';
import { User } from '@gaplace/shared';
import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { config } from '../config/env';
export class UserService {
async createGuestUser(): Promise<User> {
const client = await pgPool.connect();
try {
const guestId = uuidv4();
const username = `Guest_${guestId.slice(0, 8)}`;
const result = await client.query(`
INSERT INTO users (id, username, is_guest)
VALUES ($1, $2, true)
RETURNING *
`, [guestId, username]);
const row = result.rows[0];
return {
id: row.id,
username: row.username,
email: row.email,
avatar: row.avatar_url,
isGuest: true,
createdAt: new Date(row.created_at).getTime(),
lastSeen: new Date(row.last_seen).getTime(),
};
} finally {
client.release();
}
}
async createUser(username: string, email: string, password: string): Promise<User> {
const client = await pgPool.connect();
try {
// Check if username or email already exists
const existingUser = await client.query(`
SELECT id FROM users WHERE username = $1 OR email = $2
`, [username, email]);
if (existingUser.rows.length > 0) {
throw new Error('Username or email already exists');
}
// Hash password
const passwordHash = await bcrypt.hash(password, 12);
const result = await client.query(`
INSERT INTO users (username, email, password_hash, is_guest)
VALUES ($1, $2, $3, false)
RETURNING *
`, [username, email, passwordHash]);
const row = result.rows[0];
return {
id: row.id,
username: row.username,
email: row.email,
avatar: row.avatar_url,
isGuest: false,
createdAt: new Date(row.created_at).getTime(),
lastSeen: new Date(row.last_seen).getTime(),
};
} finally {
client.release();
}
}
async authenticateUser(usernameOrEmail: string, password: string): Promise<User | null> {
const client = await pgPool.connect();
try {
const result = await client.query(`
SELECT * FROM users
WHERE (username = $1 OR email = $1) AND is_guest = false
`, [usernameOrEmail]);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
const isValid = await bcrypt.compare(password, row.password_hash);
if (!isValid) {
return null;
}
// Update last seen
await client.query(`
UPDATE users SET last_seen = NOW() WHERE id = $1
`, [row.id]);
return {
id: row.id,
username: row.username,
email: row.email,
avatar: row.avatar_url,
isGuest: false,
createdAt: new Date(row.created_at).getTime(),
lastSeen: Date.now(),
};
} finally {
client.release();
}
}
async getUser(userId: string): Promise<User | null> {
const client = await pgPool.connect();
try {
const result = await client.query(`
SELECT * FROM users WHERE id = $1
`, [userId]);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
return {
id: row.id,
username: row.username,
email: row.email,
avatar: row.avatar_url,
isGuest: row.is_guest,
createdAt: new Date(row.created_at).getTime(),
lastSeen: new Date(row.last_seen).getTime(),
};
} finally {
client.release();
}
}
async updateUserLastSeen(userId: string): Promise<void> {
const client = await pgPool.connect();
try {
await client.query(`
UPDATE users SET last_seen = NOW() WHERE id = $1
`, [userId]);
} finally {
client.release();
}
}
generateJWT(user: User): string {
return jwt.sign(
{
userId: user.id,
username: user.username,
isGuest: user.isGuest
},
config.jwtSecret,
{ expiresIn: '7d' }
);
}
verifyJWT(token: string): { userId: string; username: string; isGuest: boolean } | null {
try {
return jwt.verify(token, config.jwtSecret) as any;
} catch (error) {
return null;
}
}
async getUserStats(userId: string): Promise<{
totalPixels: number;
joinedCanvases: number;
accountAge: number;
}> {
const client = await pgPool.connect();
try {
const [userResult, canvasResult] = await Promise.all([
client.query(`
SELECT created_at FROM users WHERE id = $1
`, [userId]),
client.query(`
SELECT COUNT(DISTINCT canvas_id) as canvas_count
FROM user_sessions WHERE user_id = $1
`, [userId])
]);
const user = userResult.rows[0];
const canvasCount = canvasResult.rows[0]?.canvas_count || 0;
return {
totalPixels: 0, // Will be fetched from Redis via RateLimitService
joinedCanvases: parseInt(canvasCount),
accountAge: user ? Date.now() - new Date(user.created_at).getTime() : 0,
};
} finally {
client.release();
}
}
}