mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +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)
|
# Install system dependencies (removed netcat-openbsd since we no longer need to wait for PostgreSQL)
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
@@ -70,6 +70,7 @@ FROM base AS runner
|
|||||||
# Set production environment
|
# Set production environment
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV API_BASE_URL=http://127.0.0.1:3333
|
||||||
|
|
||||||
# Create application user
|
# Create application user
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
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/prisma ./prisma
|
||||||
COPY --from=server-builder --chown=palmr:nodejs /app/server/package.json ./
|
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
|
# Ensure storage directories have correct permissions
|
||||||
RUN chown -R palmr:nodejs /app/server/uploads /app/server/temp-chunks /app/server/prisma
|
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
|
priority=100
|
||||||
|
|
||||||
[program:web]
|
[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
|
directory=/app/web
|
||||||
user=palmr
|
user=palmr
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
stderr_logfile=/var/log/supervisor/web.err.log
|
stderr_logfile=/var/log/supervisor/web.err.log
|
||||||
stdout_logfile=/var/log/supervisor/web.out.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
|
priority=200
|
||||||
startsecs=10
|
startsecs=10
|
||||||
EOF
|
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