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>
202 lines
No EOL
5.2 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
} |