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>
This commit is contained in:
parent
5eb7a1482e
commit
98f290a662
69 changed files with 17771 additions and 1589 deletions
202
backend/src/services/UserService.ts
Normal file
202
backend/src/services/UserService.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue