feat: update Dockerfile and add password reset functionality

- Upgraded Node.js version in Dockerfile from 18-alpine to 20-alpine for improved performance and security.
- Added API_BASE_URL environment variable to support local API calls.
- Introduced a new reset-password.sh script for interactive password resets within the Docker container.
- Created reset-password.ts script to handle password reset logic, including user validation and password hashing.
- Enhanced Dockerfile to copy the new scripts and ensure proper permissions for execution.
This commit is contained in:
Daniel Luiz Alves
2025-06-03 17:49:48 -03:00
parent 998b690659
commit 1fb06067cd
3 changed files with 377 additions and 3 deletions

View File

@@ -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

122
apps/server/reset-password.sh Executable file
View File

@@ -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

View File

@@ -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<string> {
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);
}