Fix: Solucionados problemas de permisos en Docker y agregada gestion de Webhook de Telegram
This commit is contained in:
45
.env.docker
Normal file
45
.env.docker
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Variables de entorno para Docker
|
||||||
|
# Copiar a .env y completar los valores faltantes
|
||||||
|
|
||||||
|
APP_NAME="Nomina Ventas"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_KEY=
|
||||||
|
APP_URL=http://localhost:8004
|
||||||
|
|
||||||
|
APP_LOCALE=es
|
||||||
|
APP_FALLBACK_LOCALE=es
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Base de datos REMOTA
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=10.10.4.17
|
||||||
|
DB_PORT=3391
|
||||||
|
DB_DATABASE=nomina_pegaso
|
||||||
|
DB_USERNAME=nickpons666
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=true
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
|
||||||
|
# Telegram Bot (opcional)
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Docker Runtime
|
||||||
|
DATA_DIR=/media/DATOS/AppData/nomina_pegaso
|
||||||
|
APP_PORT=8004
|
||||||
|
RUN_MIGRATIONS=false
|
||||||
57
Dockerfile
57
Dockerfile
@@ -1,26 +1,15 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# Laravel PHP-FPM 8.3 - Production Optimized
|
# Laravel - Single Image (Nginx + PHP-FPM)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# Stage 1: Builder with Composer
|
|
||||||
FROM composer:2 AS composer-builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy composer files first for better layer caching
|
|
||||||
COPY composer.json composer.lock* ./
|
|
||||||
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
|
|
||||||
|
|
||||||
# Stage 2: PHP-FPM Production Image
|
|
||||||
FROM php:8.3-fpm-bookworm
|
FROM php:8.3-fpm-bookworm
|
||||||
|
|
||||||
# Build arguments for dynamic user UID/GID
|
# Build arguments for dynamic user UID/GID
|
||||||
ARG PUID=1000
|
ARG PUID=1000
|
||||||
ARG PGID=1000
|
ARG PGID=1000
|
||||||
|
|
||||||
# Labels
|
|
||||||
LABEL maintainer="dev@local.dev" \
|
LABEL maintainer="dev@local.dev" \
|
||||||
description="Laravel PHP-FPM 8.3 production container"
|
description="Laravel single container with Nginx"
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
@@ -37,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libsqlite3-dev \
|
libsqlite3-dev \
|
||||||
unzip \
|
unzip \
|
||||||
git \
|
git \
|
||||||
|
nginx \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
@@ -59,48 +49,43 @@ RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.
|
|||||||
&& echo "opcache.revalidate_freq=2" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
&& echo "opcache.revalidate_freq=2" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||||
&& echo "opcache.fast_shutdown=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
&& echo "opcache.fast_shutdown=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||||
&& echo "upload_max_filesize=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
&& echo "upload_max_filesize=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||||
&& echo "post_max_size=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
&& echo "post_max_size=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||||
|
&& echo "listen = 0.0.0.0:9000" >> /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& sed -i 's/user = www-data/user = laravel/g' /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& sed -i 's/group = www-data/group = laravel/g' /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& echo "sys_temp_dir = /tmp" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||||
|
&& echo "upload_tmp_dir = /tmp" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||||
|
|
||||||
# Copy Composer from builder stage
|
# Create Laravel storage directories
|
||||||
COPY --from=composer-builder /usr/bin/composer /usr/bin/composer
|
|
||||||
COPY --from=composer-builder /root/.composer /root/.composer
|
|
||||||
|
|
||||||
# Create Laravel storage directories with proper permissions
|
|
||||||
RUN mkdir -p /var/www/html/storage/framework/{cache,sessions,views} \
|
RUN mkdir -p /var/www/html/storage/framework/{cache,sessions,views} \
|
||||||
&& mkdir -p /var/www/html/storage/logs \
|
&& mkdir -p /var/www/html/storage/logs \
|
||||||
&& mkdir -p /var/www/html/bootstrap/cache \
|
&& mkdir -p /var/www/html/bootstrap/cache \
|
||||||
&& chmod -R 775 /var/www/html/storage \
|
&& chmod -R 775 /var/www/html/storage \
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache
|
&& chmod -R 775 /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
# Create system user for Laravel (dynamic UID/GID)
|
# Create system user for Laravel
|
||||||
RUN groupadd -g ${PGID} laravel && \
|
RUN groupadd -g ${PGID} laravel && \
|
||||||
useradd -u ${PUID} -g laravel -m -s /bin/bash laravel
|
useradd -u ${PUID} -g laravel -m -s /bin/bash laravel
|
||||||
|
|
||||||
# Set working directory
|
# Copy nginx config
|
||||||
|
COPY docker/nginx/nginx.conf /etc/nginx/sites-available/default
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
# Copy application files
|
# Copy application files
|
||||||
COPY --chown=laravel:laravel . /var/www/html
|
COPY --chown=laravel:laravel . /var/www/html
|
||||||
|
|
||||||
# Fix storage permissions
|
# Fix permissions
|
||||||
RUN chown -R laravel:laravel /var/www/html/storage \
|
RUN chown -R laravel:laravel /var/www/html/storage \
|
||||||
&& chown -R laravel:laravel /var/www/html/bootstrap/cache \
|
&& chown -R laravel:laravel /var/www/html/bootstrap/cache \
|
||||||
&& chmod -R 775 /var/www/html/storage \
|
&& chmod -R 775 /var/www/html/storage \
|
||||||
&& chmod -R 775 /var/www/html/bootstrap/cache
|
&& chmod -R 775 /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
# Switch to non-root user
|
# Copy entrypoint
|
||||||
USER laravel
|
COPY --chown=root:root docker/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Expose port 9000 (PHP-FPM)
|
# Expose port 80 (interno)
|
||||||
EXPOSE 9000
|
EXPOSE 80
|
||||||
|
|
||||||
# Health check
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD php-fpm-healthcheck || exit 1
|
|
||||||
|
|
||||||
# Create healthcheck script
|
|
||||||
RUN echo '#!/bin/sh' > /usr/local/bin/php-fpm-healthcheck \
|
|
||||||
&& echo 'SCRIPT_NAME=/health.php SCRIPT_FILENAME=/health.php REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 > /dev/null 2>&1' >> /usr/local/bin/php-fpm-healthcheck \
|
|
||||||
&& chmod +x /usr/local/bin/php-fpm-healthcheck
|
|
||||||
|
|
||||||
CMD ["php-fpm"]
|
|
||||||
@@ -37,7 +37,10 @@ class TelegramController extends Controller
|
|||||||
$telegramAccount->update(['verification_code' => $verificationCode]);
|
$telegramAccount->update(['verification_code' => $verificationCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('telegram.verify', compact('telegramAccount'));
|
return view('telegram.verify', [
|
||||||
|
'telegramAccount' => $telegramAccount,
|
||||||
|
'webhookInfo' => (new TelegramBotService())->getWebhookInfo()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,8 +101,7 @@ class TelegramController extends Controller
|
|||||||
Log::info('Telegram webhook received', $update);
|
Log::info('Telegram webhook received', $update);
|
||||||
|
|
||||||
if (empty($update)) {
|
if (empty($update)) {
|
||||||
return response()->json(['ok' => true, 'message' => 'No update
|
return response()->json(['ok' => true, 'message' => 'No update received']);
|
||||||
received']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$botService = new TelegramBotService();
|
$botService = new TelegramBotService();
|
||||||
@@ -117,24 +119,32 @@ received']);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configurar webhook (ruta temporal para configurar desde el navegador)
|
* Configurar webhook
|
||||||
*/
|
*/
|
||||||
public function setupWebhook(Request $request)
|
public function setupWebhook(Request $request)
|
||||||
{
|
{
|
||||||
$token = $request->get('token');
|
|
||||||
|
|
||||||
// Verificar token de seguridad simple
|
|
||||||
if ($token !== config('app.key')) {
|
|
||||||
abort(403, 'Token inválido');
|
|
||||||
}
|
|
||||||
|
|
||||||
$botService = new TelegramBotService();
|
$botService = new TelegramBotService();
|
||||||
$result = $botService->setWebhook();
|
$result = $botService->setWebhook();
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
return response()->json(['success' => true, 'message' => 'Webhook configurado correctamente']);
|
return back()->with('success', 'Webhook configurado correctamente.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(['success' => false, 'error' => 'Error al configurar webhook'], 500);
|
return back()->with('error', 'Error al configurar webhook.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Borrar webhook
|
||||||
|
*/
|
||||||
|
public function deleteWebhook(Request $request)
|
||||||
|
{
|
||||||
|
$botService = new TelegramBotService();
|
||||||
|
$result = $botService->deleteWebhook();
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return back()->with('success', 'Webhook borrado correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('error', 'Error al borrar el webhook.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,4 +297,40 @@ class TelegramBotService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener información del webhook
|
||||||
|
*/
|
||||||
|
public function getWebhookInfo(): array
|
||||||
|
{
|
||||||
|
if (!$this->botToken) {
|
||||||
|
return ['ok' => false, 'error' => 'Bot token not configured'];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::get("https://api.telegram.org/bot{$this->botToken}/getWebhookInfo");
|
||||||
|
return $response->json();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Telegram get webhook info error', ['error' => $e->getMessage()]);
|
||||||
|
return ['ok' => false, 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Borrar webhook
|
||||||
|
*/
|
||||||
|
public function deleteWebhook(): bool
|
||||||
|
{
|
||||||
|
if (!$this->botToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/deleteWebhook");
|
||||||
|
return $response->json('ok', false);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Telegram delete webhook error', ['error' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
93
build.sh
Executable file
93
build.sh
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REGISTRY="registry-pons.duckdns.org"
|
||||||
|
IMAGE_NAME="nomina-ventas"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Build Docker Image - Nomina Ventas"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Ask for tag
|
||||||
|
read -p "Ingresa el tag de la imagen (ej: latest, v1.0.0): " TAG
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
TAG="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||||
|
echo "Imagen: $FULL_IMAGE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Ask for cache
|
||||||
|
echo "¿Quieres construir con cache (más rápido) o sin cache (más limpio)?"
|
||||||
|
select CACHE_CHOICE in "Con cache" "Sin cache (--no-cache)"; do
|
||||||
|
case $CACHE_CHOICE in
|
||||||
|
"Con cache") BUILD_CACHE="" ;;
|
||||||
|
"Sin cache (--no-cache)") BUILD_CACHE="--no-cache" ;;
|
||||||
|
esac
|
||||||
|
break
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Ask for registry push
|
||||||
|
echo "¿Quieres subir la imagen al registry?"
|
||||||
|
select PUSH_CHOICE in "Sí" "No (solo construir local)"; do
|
||||||
|
case $PUSH_CHOICE in
|
||||||
|
"Sí") DO_PUSH=true ;;
|
||||||
|
"No (solo construir local)") DO_PUSH=false ;;
|
||||||
|
esac
|
||||||
|
break
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Build the image
|
||||||
|
echo "========================================"
|
||||||
|
echo " Construyendo imagen..."
|
||||||
|
echo "========================================"
|
||||||
|
docker build \
|
||||||
|
--tag "${FULL_IMAGE}" \
|
||||||
|
${BUILD_CACHE} \
|
||||||
|
--build-arg PUID=$(id -u) \
|
||||||
|
--build-arg PGID=$(id -g) \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Imagen construida: ${FULL_IMAGE}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Push if requested
|
||||||
|
if [ "$DO_PUSH" = true ]; then
|
||||||
|
echo "========================================"
|
||||||
|
echo " Subiendo al registry..."
|
||||||
|
echo "========================================"
|
||||||
|
docker push "${FULL_IMAGE}"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Imagen subida al registry"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Update docker-compose.prod.yml with the tag
|
||||||
|
echo "========================================"
|
||||||
|
echo " Actualizando docker-compose.prod.yml"
|
||||||
|
echo "========================================"
|
||||||
|
sed -i "s|image: \${REGISTRY:-registry-pons.duckdns.org}/\${IMAGE:-nomina-ventas}:\${TAG:-latest}|image: ${FULL_IMAGE}|g" docker-compose.prod.yml
|
||||||
|
|
||||||
|
echo "✅ docker-compose.prod.yml actualizado"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Resumen"
|
||||||
|
echo "========================================"
|
||||||
|
echo " Imagen local: ${FULL_IMAGE}"
|
||||||
|
[ "$DO_PUSH" = true ] && echo " Registry: ${FULL_IMAGE}"
|
||||||
|
echo ""
|
||||||
|
echo " Para producción:"
|
||||||
|
echo " 1. Edita .env.docker con tus valores"
|
||||||
|
echo " 2. cp .env.docker .env"
|
||||||
|
echo " 3. docker compose -f docker-compose.prod.yml up -d"
|
||||||
|
echo ""
|
||||||
|
echo " El puerto expuesto será: 8004"
|
||||||
|
echo " Los datos se almacenarán en: /media/DATOS/AppData/nomina_pegaso"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Completado!"
|
||||||
44
docker-compose.prod.yml
Normal file
44
docker-compose.prod.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: registry-pons.duckdns.org/nomina-ventas:latest
|
||||||
|
container_name: nomina_app
|
||||||
|
ports:
|
||||||
|
- "8004:80"
|
||||||
|
volumes:
|
||||||
|
- /media/DATOS/AppData/nomina_pegaso/storage:/var/www/html/storage
|
||||||
|
- /media/DATOS/AppData/nomina_pegaso/bootstrap/cache:/var/www/html/bootstrap/cache
|
||||||
|
environment:
|
||||||
|
- APP_NAME=Nomina Ventas
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=true
|
||||||
|
- APP_KEY=
|
||||||
|
- APP_URL=https://nomina-pegaso.duckdns.org
|
||||||
|
- APP_LOCALE=es
|
||||||
|
- APP_FALLBACK_LOCALE=es
|
||||||
|
- BCRYPT_ROUNDS=12
|
||||||
|
- LOG_CHANNEL=stack
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
- DB_CONNECTION=mysql
|
||||||
|
- DB_HOST=10.10.4.17
|
||||||
|
- DB_PORT=3390
|
||||||
|
- DB_DATABASE=nomina_pegaso
|
||||||
|
- DB_USERNAME=nickpons666
|
||||||
|
- DB_PASSWORD=MiPo6425@@
|
||||||
|
- SESSION_DRIVER=database
|
||||||
|
- SESSION_LIFETIME=120
|
||||||
|
- SESSION_ENCRYPT=true
|
||||||
|
- SESSION_PATH=/
|
||||||
|
- SESSION_DOMAIN=
|
||||||
|
- BROADCAST_CONNECTION=log
|
||||||
|
- FILESYSTEM_DISK=local
|
||||||
|
- QUEUE_CONNECTION=database
|
||||||
|
- CACHE_STORE=database
|
||||||
|
- TELEGRAM_BOT_TOKEN=8324407449:AAF2awMeZ9pgSIp0MvV1r5owu8lO7SEK70E
|
||||||
|
- TELEGRAM_WEBHOOK_URL=
|
||||||
|
- RUN_MIGRATIONS=false
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 512M
|
||||||
@@ -1,28 +1,4 @@
|
|||||||
services:
|
services:
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: nomina_nginx
|
|
||||||
ports:
|
|
||||||
- "${APP_PORT:-80}:80"
|
|
||||||
volumes:
|
|
||||||
- ./public:/var/www/html/public:ro
|
|
||||||
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
networks:
|
|
||||||
- app_network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -31,29 +7,26 @@ services:
|
|||||||
PUID: ${PUID:-1000}
|
PUID: ${PUID:-1000}
|
||||||
PGID: ${PGID:-1000}
|
PGID: ${PGID:-1000}
|
||||||
container_name: nomina_app
|
container_name: nomina_app
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8004}:80"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html:delegated
|
||||||
environment:
|
environment:
|
||||||
- APP_ENV=${APP_ENV:-production}
|
- APP_ENV=${APP_ENV:-local}
|
||||||
- APP_DEBUG=${APP_DEBUG:-false}
|
- APP_DEBUG=${APP_DEBUG:-true}
|
||||||
- APP_KEY=${APP_KEY}
|
- APP_KEY=${APP_KEY}
|
||||||
- APP_URL=${APP_URL:-http://localhost}
|
- APP_URL=${APP_URL:-http://localhost:8004}
|
||||||
- DB_CONNECTION=${DB_CONNECTION:-mysql}
|
- DB_CONNECTION=${DB_CONNECTION:-mysql}
|
||||||
- DB_HOST=${DB_HOST:-db}
|
- DB_HOST=${DB_HOST:-10.10.4.17}
|
||||||
- DB_PORT=${DB_PORT:-3306}
|
- DB_PORT=${DB_PORT:-3391}
|
||||||
- DB_DATABASE=${DB_DATABASE:-nomina_ventas}
|
- DB_DATABASE=${DB_DATABASE:-nomina_pegaso}
|
||||||
- DB_USERNAME=${DB_USERNAME}
|
- DB_USERNAME=${DB_USERNAME:-nickpons666}
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- SESSION_DRIVER=${SESSION_DRIVER:-database}
|
- SESSION_DRIVER=${SESSION_DRIVER:-database}
|
||||||
- CACHE_STORE=${CACHE_STORE:-database}
|
- CACHE_STORE=${CACHE_STORE:-database}
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
- TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL}
|
- TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL}
|
||||||
volumes:
|
- RUN_MIGRATIONS=${RUN_MIGRATIONS:-false}
|
||||||
- ./:/var/www/html:delegated
|
|
||||||
- storage_data:/var/www/html/storage
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- app_network
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -66,13 +39,13 @@ services:
|
|||||||
container_name: nomina_mysql
|
container_name: nomina_mysql
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||||
- MYSQL_DATABASE=${DB_DATABASE:-nomina_ventas}
|
- MYSQL_DATABASE=${DB_DATABASE:-nomina_pegaso}
|
||||||
- MYSQL_USER=${DB_USERNAME}
|
- MYSQL_USER=${DB_USERNAME:-nickpons666}
|
||||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
networks:
|
ports:
|
||||||
- app_network
|
- "${DB_PORT:-3391}:3306"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
@@ -87,9 +60,8 @@ services:
|
|||||||
memory: 1G
|
memory: 1G
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app_network:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
storage_data:
|
|
||||||
100
docker/entrypoint.sh
Normal file
100
docker/entrypoint.sh
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Nomina Ventas - Entrypoint"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Verificar que el código existe en /var/www/html
|
||||||
|
if [ ! -f /var/www/html/artisan ]; then
|
||||||
|
echo "ERROR: No se encontró el código en /var/www/html"
|
||||||
|
echo "Montando volumen con el código..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Asegurar que existan los directorios necesarios (importante para volúmenes montados)
|
||||||
|
mkdir -p /var/www/html/storage/framework/{cache,sessions,views}
|
||||||
|
mkdir -p /var/www/html/storage/logs
|
||||||
|
mkdir -p /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
|
# Asegurar permisos
|
||||||
|
chown -R laravel:laravel /var/www/html/storage
|
||||||
|
chown -R laravel:laravel /var/www/html/bootstrap/cache
|
||||||
|
chmod -R 775 /var/www/html/storage
|
||||||
|
chmod -R 775 /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
|
# ===== GENERAR O ACTUALIZAR .env =====
|
||||||
|
echo "Intentando sincronizar .env desde variables de Docker..."
|
||||||
|
|
||||||
|
# Generamos el contenido en un archivo temporal
|
||||||
|
cat > /tmp/.env.tmp << EOF
|
||||||
|
APP_NAME="${APP_NAME:-Laravel}"
|
||||||
|
APP_ENV="${APP_ENV:-production}"
|
||||||
|
APP_KEY="${APP_KEY}"
|
||||||
|
APP_DEBUG=${APP_DEBUG:-false}
|
||||||
|
APP_URL=${APP_URL:-http://localhost}
|
||||||
|
APP_LOCALE=${APP_LOCALE:-es}
|
||||||
|
APP_FALLBACK_LOCALE=${APP_FALLBACK_LOCALE:-es}
|
||||||
|
LOG_CHANNEL=${LOG_CHANNEL:-stack}
|
||||||
|
LOG_LEVEL=${LOG_LEVEL:-debug}
|
||||||
|
DB_CONNECTION=${DB_CONNECTION:-mysql}
|
||||||
|
DB_HOST=${DB_HOST:-127.0.0.1}
|
||||||
|
DB_PORT=${DB_PORT:-3306}
|
||||||
|
DB_DATABASE=${DB_DATABASE:-laravel}
|
||||||
|
DB_USERNAME=${DB_USERNAME:-root}
|
||||||
|
DB_PASSWORD='${DB_PASSWORD}'
|
||||||
|
SESSION_DRIVER=${SESSION_DRIVER:-file}
|
||||||
|
SESSION_LIFETIME=${SESSION_LIFETIME:-120}
|
||||||
|
SESSION_ENCRYPT=${SESSION_ENCRYPT:-false}
|
||||||
|
SESSION_PATH=${SESSION_PATH:-/}
|
||||||
|
SESSION_DOMAIN=${SESSION_DOMAIN:-null}
|
||||||
|
BROADCAST_CONNECTION=${BROADCAST_CONNECTION:-log}
|
||||||
|
FILESYSTEM_DISK=${FILESYSTEM_DISK:-local}
|
||||||
|
QUEUE_CONNECTION=${QUEUE_CONNECTION:-sync}
|
||||||
|
CACHE_STORE=${CACHE_STORE:-database}
|
||||||
|
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
|
TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL}
|
||||||
|
BCRYPT_ROUNDS=${BCRYPT_ROUNDS:-12}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Intentar mover el temporal al destino final
|
||||||
|
if cp /tmp/.env.tmp /var/www/html/.env 2>/dev/null; then
|
||||||
|
# Si pudimos copiarlo, aplicamos permisos
|
||||||
|
chown laravel:laravel /var/www/html/.env
|
||||||
|
chmod 640 /var/www/html/.env
|
||||||
|
|
||||||
|
# Generar APP_KEY si falta (solo si es escribible)
|
||||||
|
if [ -z "$APP_KEY" ] || [ "$APP_KEY" == "" ]; then
|
||||||
|
echo "APP_KEY no detectada, generando una nueva..."
|
||||||
|
NEW_KEY=$(php /var/www/html/artisan key:generate --show --no-ansi)
|
||||||
|
sed -i "s|APP_KEY=|APP_KEY=$NEW_KEY|g" /var/www/html/.env
|
||||||
|
export APP_KEY=$NEW_KEY
|
||||||
|
fi
|
||||||
|
echo "✅ Archivo .env sincronizado correctamente."
|
||||||
|
else
|
||||||
|
echo "⚠️ ADVERTENCIA: No se pudo escribir en /var/www/html/.env"
|
||||||
|
echo "ℹ️ Probablemente esté montado como volumen de solo lectura (:ro)."
|
||||||
|
echo "ℹ️ Se usarán los valores del archivo montado externamente."
|
||||||
|
fi
|
||||||
|
rm -f /tmp/.env.tmp
|
||||||
|
|
||||||
|
# ===== LIMPIAR CACHE =====
|
||||||
|
echo ">> Limpiando cache..."
|
||||||
|
php /var/www/html/artisan view:clear 2>/dev/null || true
|
||||||
|
php /var/www/html/artisan config:clear 2>/dev/null || true
|
||||||
|
php /var/www/html/artisan cache:clear 2>/dev/null || true
|
||||||
|
php /var/www/html/artisan route:clear 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ">> Cacheando configuración..."
|
||||||
|
php /var/www/html/artisan config:cache 2>&1 || true
|
||||||
|
|
||||||
|
# ===== VERIFICAR =====
|
||||||
|
php /var/www/html/artisan about 2>&1 | head -10 || true
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Iniciando servicios..."
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
php-fpm &
|
||||||
|
nginx -g "daemon off;"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name _;
|
||||||
root /var/www/html/public;
|
root /var/www/html/public;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_pass app:9000;
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
|||||||
159
prompt.txt
Normal file
159
prompt.txt
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
Crea una aplicación web para el control de ingresos por comisiones y gastos personales con soporte multiusuario y un bot de Telegram integrado.
|
||||||
|
|
||||||
|
## Descripción general
|
||||||
|
|
||||||
|
La aplicación debe permitir a múltiples usuarios registrar ventas diarias por mes mediante un calendario interactivo, calcular comisiones basadas en un porcentaje configurable, sumar un sueldo mensual fijo, registrar gastos y mostrar el dinero restante tanto mensual como por quincena.
|
||||||
|
|
||||||
|
Cada usuario debe tener acceso únicamente a su propia información.
|
||||||
|
|
||||||
|
## Funcionalidades principales
|
||||||
|
|
||||||
|
### 1. Sistema multiusuario
|
||||||
|
|
||||||
|
* Registro e inicio de sesión
|
||||||
|
* Cada usuario solo puede ver y modificar sus propios datos
|
||||||
|
* Relación de todos los datos con user_id
|
||||||
|
|
||||||
|
### 2. Gestión por mes
|
||||||
|
|
||||||
|
* Crear, ver y editar meses por usuario
|
||||||
|
|
||||||
|
### 3. Calendario de captura (IMPORTANTE)
|
||||||
|
|
||||||
|
* Cada mes debe mostrarse como un calendario visual
|
||||||
|
* El usuario NO captura la fecha manualmente
|
||||||
|
* El usuario selecciona un día haciendo clic en el calendario
|
||||||
|
|
||||||
|
Al seleccionar un día:
|
||||||
|
|
||||||
|
* Se abre un formulario/modal para capturar:
|
||||||
|
|
||||||
|
* Ventas registradas por el usuario
|
||||||
|
* Ventas del sistema de la empresa
|
||||||
|
|
||||||
|
El sistema debe:
|
||||||
|
|
||||||
|
* Guardar automáticamente la fecha según el día seleccionado
|
||||||
|
* Mostrar visualmente en el calendario:
|
||||||
|
|
||||||
|
* Días con información registrada
|
||||||
|
* Días sin registrar
|
||||||
|
* Indicadores rápidos (ej: colores o íconos)
|
||||||
|
|
||||||
|
### 4. Registro diario de ventas
|
||||||
|
|
||||||
|
Por cada día seleccionado:
|
||||||
|
|
||||||
|
* Ventas registradas por el usuario
|
||||||
|
* Ventas del sistema de la empresa
|
||||||
|
|
||||||
|
Cálculos:
|
||||||
|
|
||||||
|
* Diferencia entre valores
|
||||||
|
* Comisión diaria
|
||||||
|
|
||||||
|
### 5. Configuración de variables por usuario
|
||||||
|
|
||||||
|
Cada usuario puede definir:
|
||||||
|
|
||||||
|
* Porcentaje de comisión
|
||||||
|
* Sueldo mensual
|
||||||
|
|
||||||
|
### 6. Cálculos automáticos
|
||||||
|
|
||||||
|
* Comisión diaria = ventas del sistema × porcentaje
|
||||||
|
* Total mensual de comisiones
|
||||||
|
* Total ingresos = sueldo + comisiones
|
||||||
|
* Total gastos
|
||||||
|
* Balance final
|
||||||
|
|
||||||
|
### 7. Gestión de gastos
|
||||||
|
|
||||||
|
* Agregar múltiples gastos:
|
||||||
|
|
||||||
|
* Descripción
|
||||||
|
* Monto
|
||||||
|
* Fecha (opcional o seleccionable desde calendario)
|
||||||
|
|
||||||
|
### 8. Cálculo por quincena
|
||||||
|
|
||||||
|
* Primera quincena (1–15)
|
||||||
|
* Segunda quincena (16–fin)
|
||||||
|
|
||||||
|
Mostrar:
|
||||||
|
|
||||||
|
* Ingresos
|
||||||
|
* Gastos
|
||||||
|
* Balance
|
||||||
|
|
||||||
|
### 9. Dashboard
|
||||||
|
|
||||||
|
* Resumen mensual
|
||||||
|
* Totales
|
||||||
|
* Vista de calendario
|
||||||
|
* Tabla opcional de ventas
|
||||||
|
* Resumen por quincena
|
||||||
|
|
||||||
|
## Integración con Telegram
|
||||||
|
|
||||||
|
### Vinculación segura
|
||||||
|
|
||||||
|
* El usuario inicia sesión en la web
|
||||||
|
* Genera un código único de vinculación
|
||||||
|
* En Telegram, el usuario envía ese código al bot
|
||||||
|
* El bot vincula el chat_id con el usuario
|
||||||
|
|
||||||
|
### Funcionalidades del bot
|
||||||
|
|
||||||
|
El usuario puede:
|
||||||
|
|
||||||
|
* Registrar ventas:
|
||||||
|
comando: /venta 1500 1400
|
||||||
|
(el sistema usa automáticamente la fecha actual)
|
||||||
|
|
||||||
|
* Registrar gastos:
|
||||||
|
comando: /gasto comida 200
|
||||||
|
|
||||||
|
* Consultar resumen:
|
||||||
|
comando: /resumen
|
||||||
|
|
||||||
|
* Consultar quincena:
|
||||||
|
comando: /quincena
|
||||||
|
|
||||||
|
### Seguridad
|
||||||
|
|
||||||
|
* No pedir usuario/contraseña en Telegram
|
||||||
|
* Validar chat_id vinculado
|
||||||
|
* Solo permitir acceso a datos del usuario vinculado
|
||||||
|
|
||||||
|
## Base de datos (sugerida)
|
||||||
|
|
||||||
|
* users (id, name, email, password)
|
||||||
|
* telegram_accounts (id, user_id, chat_id)
|
||||||
|
* months (id, user_id, name, year)
|
||||||
|
* daily_sales (id, user_id, month_id, date, user_sales, system_sales)
|
||||||
|
* settings (id, user_id, commission_percentage, monthly_salary)
|
||||||
|
* expenses (id, user_id, month_id, description, amount, date)
|
||||||
|
|
||||||
|
## Requisitos técnicos
|
||||||
|
|
||||||
|
* Backend: Laravel
|
||||||
|
* Frontend: Blade o Vue
|
||||||
|
* Base de datos: MySQL
|
||||||
|
* Uso de librería de calendario (ej: FullCalendar)
|
||||||
|
|
||||||
|
## Extras opcionales
|
||||||
|
|
||||||
|
* Exportar a Excel/CSV
|
||||||
|
* Gráficas
|
||||||
|
* Notificaciones por Telegram
|
||||||
|
|
||||||
|
## UI/UX
|
||||||
|
|
||||||
|
* Interfaz tipo dashboard
|
||||||
|
* Calendario como elemento principal
|
||||||
|
* Colores para indicar estado de días
|
||||||
|
* Captura rápida mediante modales
|
||||||
|
|
||||||
|
La aplicación debe ser rápida, intuitiva y enfocada en facilitar la captura diaria sin errores y todos los logs se deben de guardar en la carpeta logs de la raiz.
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Información</h5>
|
<h5 class="mb-0">Información</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +73,64 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-primary">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-robot"></i> Gestión Webhook</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if(isset($webhookInfo['ok']) && $webhookInfo['ok'])
|
||||||
|
@php $info = $webhookInfo['result']; @endphp
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="small fw-bold text-muted d-block">Estado:</label>
|
||||||
|
@if(!empty($info['url']))
|
||||||
|
<span class="badge bg-success">Configurado</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-warning text-dark">No configurado</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($info['url']))
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="small fw-bold text-muted d-block">URL Webhook:</label>
|
||||||
|
<code class="small break-all" style="word-break: break-all;">{{ $info['url'] }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 small">
|
||||||
|
<label class="small fw-bold text-muted d-block">Pendientes:</label>
|
||||||
|
{{ $info['pending_update_count'] ?? 0 }} mensajes
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<form action="{{ route('telegram.setup-webhook') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="bi bi-gear-fill"></i> {{ empty($info['url']) ? 'Configurar Webhook' : 'Actualizar Webhook' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if(!empty($info['url']))
|
||||||
|
<form action="{{ route('telegram.delete-webhook') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger w-100" onclick="return confirm('¿Seguro que deseas borrar el webhook?')">
|
||||||
|
<i class="bi bi-trash-fill"></i> Borrar Webhook
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-danger small">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> Error al conectar con Telegram API.
|
||||||
|
@if(isset($webhookInfo['description']))
|
||||||
|
<br><small>{{ $webhookInfo['description'] }}</small>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-light">
|
||||||
|
<small class="text-muted italic">Basado en APP_URL: {{ config('app.url') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
@@ -60,6 +60,8 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::get('/telegram/verify', [TelegramController::class, 'showVerifyPage'])->name('telegram.verify');
|
Route::get('/telegram/verify', [TelegramController::class, 'showVerifyPage'])->name('telegram.verify');
|
||||||
Route::post('/telegram/regenerate', [TelegramController::class, 'regenerateCode'])->name('telegram.regenerate');
|
Route::post('/telegram/regenerate', [TelegramController::class, 'regenerateCode'])->name('telegram.regenerate');
|
||||||
Route::post('/telegram/unlink', [TelegramController::class, 'unlink'])->name('telegram.unlink');
|
Route::post('/telegram/unlink', [TelegramController::class, 'unlink'])->name('telegram.unlink');
|
||||||
|
Route::post('/telegram/setup-webhook', [TelegramController::class, 'setupWebhook'])->name('telegram.setup-webhook');
|
||||||
|
Route::post('/telegram/delete-webhook', [TelegramController::class, 'deleteWebhook'])->name('telegram.delete-webhook');
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
||||||
|
|||||||
13
supervisor.conf
Normal file
13
supervisor.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=/usr/local/sbin/php-fpm
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
Reference in New Issue
Block a user