diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cbae169 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.git +*.log +apps/web/dist +apps/server/dist +apps/server/uploads +.vscode +.env diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 0000000..b213382 --- /dev/null +++ b/Dockerfile.server @@ -0,0 +1,28 @@ +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies at the root +COPY package*.json ./ +COPY apps/server/package*.json ./apps/server/ +COPY apps/web/package*.json ./apps/web/ + +RUN npm install + +# Copy source and prisma +COPY apps/server ./apps/server + +# Generate Prisma client +WORKDIR /app/apps/server +RUN npx prisma generate + +# Build server +RUN npm run build + +# Make start script executable +RUN chmod +x ./start.sh + +EXPOSE 3001 + +# Run the server using the start script +CMD ["./start.sh"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..3f1402b --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,31 @@ +# Stage 1: Build React app +FROM node:20-alpine AS build + +WORKDIR /app + +# ROOT package.json +COPY package*.json ./ +COPY apps/server/package*.json ./apps/server/ +COPY apps/web/package*.json ./apps/web/ + +RUN npm install + +# Copy source +COPY . . + +# Build web frontend +WORKDIR /app/apps/web +RUN npm run build + +# Stage 2: Serve with Nginx +FROM nginx:alpine + +# Copy built assets +COPY --from=build /app/apps/web/dist /usr/share/nginx/html + +# Custom Nginx config to proxy /api and /socket.io to server:3001 +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/server/src/routes/auth.ts b/apps/server/src/routes/auth.ts index e34a921..eaa222e 100644 --- a/apps/server/src/routes/auth.ts +++ b/apps/server/src/routes/auth.ts @@ -46,22 +46,26 @@ router.post('/register', registerLimiter, async (req, res) => { } if (!username || !password) { + console.log('Registration failed: missing username or password'); res.status(400).json({ error: 'Username и пароль обязательны' }); return; } if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { + console.log('Registration failed: invalid username format', username); res.status(400).json({ error: 'Username: 3-20 символов, только латиница, цифры, _' }); return; } if (password.length < config.minPasswordLength) { + console.log('Registration failed: password too short'); res.status(400).json({ error: `Пароль должен быть не менее ${config.minPasswordLength} символов` }); return; } // Password must contain at least one letter and one digit if (!/[a-zA-Zа-яА-Я]/.test(password) || !/\d/.test(password)) { + console.log('Registration failed: password missing letters or digits'); res.status(400).json({ error: 'Пароль должен содержать буквы и цифры' }); return; } diff --git a/apps/server/start.sh b/apps/server/start.sh new file mode 100644 index 0000000..0fcd89b --- /dev/null +++ b/apps/server/start.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Wait for DB +echo "Waiting for database..." +# Simple sleep for now, or use docker-compose healthcheck +sleep 5 + +# Create DB schema (push for dev, or migrate deploy for prod) +echo "Running Prisma DB push/seed..." +npx prisma db push --accept-data-loss +npx prisma db seed + +# Start server +echo "Starting server..." +npm start diff --git a/apps/server/uploads/3d03f534-e466-460b-b48d-ff9e94049e9d.jpg b/apps/server/uploads/3d03f534-e466-460b-b48d-ff9e94049e9d.jpg new file mode 100644 index 0000000..7ae04c5 Binary files /dev/null and b/apps/server/uploads/3d03f534-e466-460b-b48d-ff9e94049e9d.jpg differ diff --git a/apps/server/uploads/777a5af2-4064-449d-a659-6b21e6bc48d0.webp b/apps/server/uploads/777a5af2-4064-449d-a659-6b21e6bc48d0.webp new file mode 100644 index 0000000..d07464c Binary files /dev/null and b/apps/server/uploads/777a5af2-4064-449d-a659-6b21e6bc48d0.webp differ diff --git a/apps/server/uploads/7ee26807-9bc0-44c9-b664-b17078a3efd3.avif b/apps/server/uploads/7ee26807-9bc0-44c9-b664-b17078a3efd3.avif new file mode 100644 index 0000000..af597d5 Binary files /dev/null and b/apps/server/uploads/7ee26807-9bc0-44c9-b664-b17078a3efd3.avif differ diff --git a/apps/web/src/lib/i18n.ts b/apps/web/src/lib/i18n.ts index 4888707..e6095f1 100644 --- a/apps/web/src/lib/i18n.ts +++ b/apps/web/src/lib/i18n.ts @@ -198,6 +198,11 @@ const translations = { createAccount: 'Создать аккаунт', testAccounts: 'Тестовые аккаунты: evgeniy, anastasia, artem, polina', passwordForAll: 'Пароль для всех:', + dontHaveAccount: 'Нет аккаунта?', + alreadyHaveAccount: 'Уже есть аккаунт?', + passwordRequirements: 'Минимум 8 символов, буквы и цифры', + registerNow: 'Зарегистрироваться', + loginNow: 'Войти', // File/upload fileTooLarge: 'Файл слишком большой (макс. 50МБ)', dropFileHere: 'Отпустите файл здесь', @@ -439,6 +444,11 @@ const translations = { createAccount: 'Create account', testAccounts: 'Test accounts: evgeniy, anastasia, artem, polina', passwordForAll: 'Password for all:', + dontHaveAccount: "Don't have an account?", + alreadyHaveAccount: 'Already have an account?', + passwordRequirements: 'At least 8 characters, letters and numbers', + registerNow: 'Register', + loginNow: 'Login', fileTooLarge: 'File too large (max 50MB)', dropFileHere: 'Drop file here', uploading: 'Uploading...', diff --git a/apps/web/src/pages/AuthPage.tsx b/apps/web/src/pages/AuthPage.tsx index c96547b..04279e4 100644 --- a/apps/web/src/pages/AuthPage.tsx +++ b/apps/web/src/pages/AuthPage.tsx @@ -58,7 +58,8 @@ export default function AuthPage() { className="relative z-10 w-full max-w-md mx-4" >
- {/* Логотип */} + + {/* Заголовок */}

Vortex

-

{t('modernMessengerShort')}

-
- - {/* Переключатель Вход/Регистрация */} -
- - +

+ {isLogin ? t('login') : t('register')} +

{/* Ошибка */} @@ -122,14 +97,14 @@ export default function AuthPage() {
setUsername(e.target.value.replace(/[^a-zA-Z0-9_]/g, ''))} placeholder="username" - className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all" + className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden" required autoFocus autoComplete="off" @@ -143,17 +118,20 @@ export default function AuthPage() { animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.2 }} + className="space-y-4" > - - setDisplayName(e.target.value)} - placeholder={t('displayNamePlaceholder')} - className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all" - /> +
+ + setDisplayName(e.target.value)} + placeholder={t('displayNamePlaceholder')} + className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden" + /> +
)} @@ -166,10 +144,10 @@ export default function AuthPage() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder={t('passwordPlaceholder')} - className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all pr-12" + className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all pr-12 outline-hidden" required autoComplete={isLogin ? 'current-password' : 'new-password'} - minLength={6} + minLength={8} />
+ {!isLogin && ( +

+

+ {t('passwordRequirements')} +

+ )}
@@ -195,7 +179,7 @@ export default function AuthPage() { value={bio} onChange={(e) => setBio(e.target.value)} placeholder={t('bioPlaceholder')} - className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all" + className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden" /> )} @@ -206,7 +190,7 @@ export default function AuthPage() { whileTap={{ scale: 0.99 }} disabled={isSubmitting} type="submit" - className="w-full py-3 px-4 rounded-xl bg-gradient-to-r from-vortex-500 to-purple-600 text-white font-medium shadow-lg shadow-vortex-500/25 hover:shadow-vortex-500/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" + className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-vortex-500 to-purple-600 text-white font-medium shadow-lg shadow-vortex-500/25 hover:shadow-vortex-500/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2" > {isSubmitting ? (
@@ -219,6 +203,25 @@ export default function AuthPage() { + {/* Футер с переключателем */} +
+

+ {isLogin ? t('dontHaveAccount') : t('alreadyHaveAccount')}{' '} + +

+
+
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..07a8ea2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +services: + db: + image: postgres:15-alpine + container_name: vortex-db + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER:-vortex} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vortex_pass} + POSTGRES_DB: ${POSTGRES_DB:-vortex_db} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + server: + build: + context: . + dockerfile: Dockerfile.server + container_name: vortex-server + restart: always + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-vortex}:${POSTGRES_PASSWORD:-vortex_pass}@db:5432/${POSTGRES_DB:-vortex_db} + PORT: 3001 + JWT_SECRET: ${JWT_SECRET:-your-secure-random-secret-key-here} + CORS_ORIGINS: http://localhost,http://localhost:80,http://localhost:5173 + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef} + depends_on: + - db + ports: + - "3001:3001" + volumes: + - ./apps/server/uploads:/app/apps/server/uploads + + web: + build: + context: . + dockerfile: Dockerfile.web + container_name: vortex-web + restart: always + ports: + - "80:80" + depends_on: + - server + +volumes: + postgres_data: diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..0c1eab3 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,40 @@ +server { + listen 80; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests + location /api/ { + proxy_pass http://server:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Proxy Socket.io + location /socket.io/ { + proxy_pass http://server:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Static uploads + location /uploads/ { + proxy_pass http://server:3001; + proxy_set_header Host $host; + } +}