diff --git a/Dockerfile b/Dockerfile index bb1663b..47c018b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS base +FROM node:20-alpine AS base # Install system dependencies (removed netcat-openbsd since we no longer need to wait for PostgreSQL) RUN apk add --no-cache \ @@ -70,6 +70,7 @@ FROM base AS runner # Set production environment ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +ENV API_BASE_URL=http://127.0.0.1:3333 # Create application user RUN addgroup --system --gid 1001 nodejs @@ -91,6 +92,12 @@ COPY --from=server-builder --chown=palmr:nodejs /app/server/node_modules ./node_ COPY --from=server-builder --chown=palmr:nodejs /app/server/prisma ./prisma COPY --from=server-builder --chown=palmr:nodejs /app/server/package.json ./ +# Copy password reset script and make it executable +COPY --from=server-builder --chown=palmr:nodejs /app/server/reset-password.sh ./ +COPY --from=server-builder --chown=palmr:nodejs /app/server/src/scripts/ ./src/scripts/ +COPY --from=server-builder --chown=palmr:nodejs /app/server/PASSWORD_RESET_GUIDE.md ./ +RUN chmod +x ./reset-password.sh + # Ensure storage directories have correct permissions RUN chown -R palmr:nodejs /app/server/uploads /app/server/temp-chunks /app/server/prisma @@ -133,14 +140,14 @@ environment=PORT=3333,HOME="/home/palmr",ENABLE_S3="false",ENCRYPTION_KEY="defau priority=100 [program:web] -command=/bin/sh -c 'echo "Waiting for API to be ready..."; while ! netstat -tln | grep ":3333 "; do echo "API not ready, waiting..."; sleep 2; done; echo "API is ready! Starting frontend..."; exec node server.js' +command=/bin/sh -c 'echo "Waiting for API to be ready..."; while ! curl -f http://127.0.0.1:3333/health >/dev/null 2>&1; do echo "API not ready, waiting..."; sleep 2; done; echo "API is ready! Starting frontend..."; exec node server.js' directory=/app/web user=palmr autostart=true autorestart=true stderr_logfile=/var/log/supervisor/web.err.log stdout_logfile=/var/log/supervisor/web.out.log -environment=PORT=5487,HOSTNAME="0.0.0.0",HOME="/home/palmr" +environment=PORT=5487,HOSTNAME="0.0.0.0",HOME="/home/palmr",API_BASE_URL="http://127.0.0.1:3333" priority=200 startsecs=10 EOF diff --git a/apps/server/reset-password.sh b/apps/server/reset-password.sh new file mode 100755 index 0000000..767140d --- /dev/null +++ b/apps/server/reset-password.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# Palmr Password Reset Script +# This script allows resetting user passwords from within the Docker container + +echo "🔐 Palmr Password Reset Tool" +echo "=============================" + +# Check if we're in the right directory +if [ ! -f "package.json" ]; then + echo "❌ Error: This script must be run from the server directory (/app/server)" + echo " Current directory: $(pwd)" + echo " Expected: /app/server" + exit 1 +fi + +# Function to check if tsx is available +check_tsx() { + # Check if tsx binary exists in node_modules + if [ -f "node_modules/.bin/tsx" ]; then + return 0 + fi + + # Fallback: try npx + if npx tsx --version >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + +# Function to install only tsx if missing +install_tsx_only() { + echo "📦 Installing tsx (quick install)..." + if command -v pnpm >/dev/null 2>&1; then + pnpm add tsx --save-dev --silent 2>/dev/null + elif command -v npm >/dev/null 2>&1; then + npm install tsx --save-dev --silent 2>/dev/null + else + return 1 + fi + + return $? +} + +# Function to install all dependencies as fallback +install_all_deps() { + echo "📦 Installing all dependencies (this may take a moment)..." + if command -v pnpm >/dev/null 2>&1; then + pnpm install --silent 2>/dev/null + elif command -v npm >/dev/null 2>&1; then + npm install --silent 2>/dev/null + else + echo "❌ Error: No package manager found (pnpm/npm)" + exit 1 + fi +} + +# Function to ensure Prisma client is available +ensure_prisma() { + # Check if Prisma client exists and is valid + if [ -d "node_modules/@prisma/client" ] && [ -f "node_modules/@prisma/client/index.js" ]; then + return 0 + fi + + echo "📦 Generating Prisma client..." + if npx prisma generate --silent >/dev/null 2>&1; then + echo "✅ Prisma client ready" + return 0 + else + echo "❌ Error: Failed to generate Prisma client" + exit 1 + fi +} + +# Quick checks first +echo "🔍 Checking dependencies..." + +# Check tsx availability +if check_tsx; then + echo "✅ tsx is ready" +else + echo "📦 tsx not found, installing..." + + # Try quick tsx-only install first + if install_tsx_only && check_tsx; then + echo "✅ tsx installed successfully" + else + echo "⚠️ Quick install failed, installing all dependencies..." + install_all_deps + + # Final check + if ! check_tsx; then + echo "❌ Error: tsx is still not available after full installation" + echo " Please check your package.json and node_modules" + exit 1 + fi + echo "✅ tsx is now ready" + fi +fi + +# Ensure Prisma client +ensure_prisma + +# Check if the TypeScript script exists +if [ ! -f "src/scripts/reset-password.ts" ]; then + echo "❌ Error: Reset password script not found at src/scripts/reset-password.ts" + echo " Available files in src/scripts/:" + ls -la src/scripts/ 2>/dev/null || echo " Directory src/scripts/ does not exist" + exit 1 +fi + +# All checks passed, run the script +echo "🚀 Starting password reset tool..." +echo "" + +# Execute the script using the most reliable method +if [ -f "node_modules/.bin/tsx" ]; then + node_modules/.bin/tsx src/scripts/reset-password.ts "$@" +else + npx tsx src/scripts/reset-password.ts "$@" +fi \ No newline at end of file diff --git a/apps/server/src/scripts/reset-password.ts b/apps/server/src/scripts/reset-password.ts new file mode 100644 index 0000000..6fa37cb --- /dev/null +++ b/apps/server/src/scripts/reset-password.ts @@ -0,0 +1,245 @@ +#!/usr/bin/env node +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; +import * as readline from "readline"; + +const prisma = new PrismaClient(); + +// Função para ler entrada do usuário de forma assíncrona +function createReadlineInterface() { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); +} + +function question(rl: readline.Interface, query: string): Promise { + return new Promise((resolve) => rl.question(query, resolve)); +} + +// Função para validar formato de email básico +function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Função para validar senha com base nas regras do sistema +function isValidPassword(password: string): boolean { + // Minimum length baseado na configuração padrão do sistema (8 caracteres) + return password.length >= 8; +} + +async function resetUserPassword() { + const rl = createReadlineInterface(); + + try { + console.log("\n🔐 Palmr Password Reset Tool"); + console.log("==============================="); + console.log("This script allows you to reset a user's password directly from the Docker terminal."); + console.log("⚠️ WARNING: This bypasses normal security checks. Use only when necessary!\n"); + + // Solicitar email do usuário + let email: string; + let user: any; + + while (true) { + email = await question(rl, "Enter user email: "); + + if (!email.trim()) { + console.log("❌ Email cannot be empty. Please try again.\n"); + continue; + } + + if (!isValidEmail(email)) { + console.log("❌ Please enter a valid email address.\n"); + continue; + } + + // Buscar usuário no banco de dados + user = await prisma.user.findUnique({ + where: { email: email.toLowerCase() }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + username: true, + isActive: true, + isAdmin: true, + }, + }); + + if (!user) { + console.log(`❌ No user found with email: ${email}\n`); + const retry = await question(rl, "Try another email? (y/n): "); + if (retry.toLowerCase() !== "y") { + console.log("\n👋 Exiting..."); + return; + } + console.log(); + continue; + } + + break; + } + + // Mostrar informações do usuário encontrado + console.log("\n✅ User found:"); + console.log(` Name: ${user.firstName} ${user.lastName}`); + console.log(` Username: ${user.username}`); + console.log(` Email: ${user.email}`); + console.log(` Status: ${user.isActive ? "Active" : "Inactive"}`); + console.log(` Admin: ${user.isAdmin ? "Yes" : "No"}\n`); + + // Confirmar se deseja prosseguir + const confirm = await question(rl, "Do you want to reset the password for this user? (y/n): "); + if (confirm.toLowerCase() !== "y") { + console.log("\n👋 Operation cancelled."); + return; + } + + // Solicitar nova senha + let newPassword: string; + while (true) { + console.log("\n🔑 Enter new password requirements:"); + console.log(" - Minimum 8 characters"); + + newPassword = await question(rl, "\nEnter new password: "); + + if (!newPassword.trim()) { + console.log("❌ Password cannot be empty. Please try again."); + continue; + } + + if (!isValidPassword(newPassword)) { + console.log("❌ Password must be at least 8 characters long. Please try again."); + continue; + } + + const confirmPassword = await question(rl, "Confirm new password: "); + + if (newPassword !== confirmPassword) { + console.log("❌ Passwords do not match. Please try again."); + continue; + } + + break; + } + + // Hash da senha usando bcrypt (mesmo método usado pelo sistema) + console.log("\n🔄 Hashing password..."); + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Atualizar senha no banco de dados + console.log("💾 Updating password in database..."); + await prisma.user.update({ + where: { id: user.id }, + data: { password: hashedPassword }, + }); + + // Limpar tokens de reset de senha existentes para este usuário + console.log("🧹 Cleaning up existing password reset tokens..."); + await prisma.passwordReset.deleteMany({ + where: { + userId: user.id, + used: false, + }, + }); + + console.log("\n✅ Password reset successful!"); + console.log(` User: ${user.firstName} ${user.lastName} (${user.email})`); + console.log(" The user can now login with the new password."); + console.log("\n🔐 Security Note: The password has been encrypted using bcrypt with salt rounds of 10."); + } catch (error) { + console.error("\n❌ Error resetting password:", error); + process.exit(1); + } finally { + rl.close(); + await prisma.$disconnect(); + } +} + +// Função para listar usuários (funcionalidade auxiliar) +async function listUsers() { + try { + console.log("\n👥 Registered Users:"); + console.log("==================="); + + const users = await prisma.user.findMany({ + select: { + firstName: true, + lastName: true, + email: true, + username: true, + isActive: true, + isAdmin: true, + createdAt: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (users.length === 0) { + console.log("No users found in the system."); + return; + } + + users.forEach((user, index) => { + console.log(`\n${index + 1}. ${user.firstName} ${user.lastName}`); + console.log(` Email: ${user.email}`); + console.log(` Username: ${user.username}`); + console.log(` Status: ${user.isActive ? "Active" : "Inactive"}`); + console.log(` Admin: ${user.isAdmin ? "Yes" : "No"}`); + console.log(` Created: ${user.createdAt.toLocaleDateString()}`); + }); + } catch (error) { + console.error("❌ Error listing users:", error); + } +} + +// Main function +async function main() { + const args = process.argv.slice(2); + + if (args.includes("--help") || args.includes("-h")) { + console.log("\n🔐 Palmr Password Reset Tool"); + console.log("============================="); + console.log("Interactive password reset tool for Docker terminal access"); + console.log("\nUsage:"); + console.log(" ./reset-password.sh - Reset a user's password interactively"); + console.log(" ./reset-password.sh --list - List all users in the system"); + console.log(" ./reset-password.sh --help - Show this help message"); + console.log("\nExamples:"); + console.log(" ./reset-password.sh"); + console.log(" ./reset-password.sh --list"); + console.log("\nNote: This script must be run inside the Docker container with database access."); + console.log("⚠️ For security, all password resets require interactive confirmation."); + return; + } + + if (args.includes("--list") || args.includes("-l")) { + await listUsers(); + await prisma.$disconnect(); + return; + } + + await resetUserPassword(); +} + +// Handle process termination +process.on("SIGINT", async () => { + console.log("\n\n👋 Goodbye!"); + await prisma.$disconnect(); + process.exit(0); +}); + +process.on("SIGTERM", async () => { + await prisma.$disconnect(); + process.exit(0); +}); + +// Run the script +if (require.main === module) { + main().catch(console.error); +}