mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-22 22:02:00 +00:00
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:
13
Dockerfile
13
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
|
||||
|
122
apps/server/reset-password.sh
Executable file
122
apps/server/reset-password.sh
Executable 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
|
245
apps/server/src/scripts/reset-password.ts
Normal file
245
apps/server/src/scripts/reset-password.ts
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user