diff --git a/Docker/Dev/docker-compose.yml b/Docker/Dev/docker-compose.yml index d66c19d..c8b2f58 100644 --- a/Docker/Dev/docker-compose.yml +++ b/Docker/Dev/docker-compose.yml @@ -14,7 +14,7 @@ services: - DB_DATABASE=${DB_DATABASE} - DB_PORT=${DB_PORT} volumes: - - storage_data:/var/www/repuve-backend-v1/storage + - ../../storage:/var/www/repuve-backend-v1/storage networks: - repuve-network mem_limit: 512M @@ -67,8 +67,6 @@ services: volumes: mysql_data: driver: local - storage_data: - driver: local networks: repuve-network: diff --git a/Docker/Dev/dockerfile b/Docker/Dev/dockerfile index 182e0ac..ad39309 100644 --- a/Docker/Dev/dockerfile +++ b/Docker/Dev/dockerfile @@ -17,7 +17,9 @@ RUN apk add --no-cache \ mysql-client \ libreoffice \ ttf-dejavu \ - && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip + && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \ + && echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \ + && echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini COPY --from=composer:latest /usr/bin/composer /usr/bin/composer diff --git a/Docker/Prod/docker-compose.yml b/Docker/Prod/docker-compose.yml index 549aff5..3b25f6c 100644 --- a/Docker/Prod/docker-compose.yml +++ b/Docker/Prod/docker-compose.yml @@ -14,7 +14,7 @@ services: - DB_DATABASE=${DB_DATABASE} - DB_PORT=${DB_PORT} volumes: - - storage_data:/var/www/repuve-backend-v1/storage + - ../../storage:/var/www/repuve-backend-v1/storage networks: - repuve-network mem_limit: 512M @@ -29,7 +29,7 @@ services: - "127.0.0.1:${NGINX_PORT}:80" volumes: - ../../public:/var/www/repuve-backend-v1/public - - storage_data:/var/www/repuve-backend-v1/storage + - ../../storage:/var/www/repuve-backend-v1/storage - ../../Docker/nginx/nginx.conf:/etc/nginx/nginx.conf - /var/log/nginx:/var/log/nginx logging: @@ -65,8 +65,6 @@ services: volumes: mysql_data: driver: local - storage_data: - driver: local networks: repuve-network: diff --git a/Docker/Prod/dockerfile b/Docker/Prod/dockerfile index b1e7786..f3600e2 100644 --- a/Docker/Prod/dockerfile +++ b/Docker/Prod/dockerfile @@ -14,7 +14,9 @@ RUN apk add --no-cache \ bash \ libreoffice \ ttf-dejavu \ - && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip + && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \ + && echo "upload_max_filesize=150M" > /usr/local/etc/php/conf.d/uploads.ini \ + && echo "post_max_size=150M" >> /usr/local/etc/php/conf.d/uploads.ini COPY --from=composer:latest /usr/bin/composer /usr/bin/composer diff --git a/Docker/nginx/nginx.conf b/Docker/nginx/nginx.conf index e26743f..8b98cbc 100644 --- a/Docker/nginx/nginx.conf +++ b/Docker/nginx/nginx.conf @@ -119,12 +119,11 @@ http { fastcgi_param HTTP_X_REQUEST_ID $request_id; } - client_max_body_size 20M; + client_max_body_size 150M; # Handle storage files (Laravel storage link) - location /storage { - alias /var/www/repuve-backend-v1/storage/app/public; - try_files $uri =404; + location /storage/ { + alias /var/www/repuve-backend-v1/storage/app/public/; } location /profile { diff --git a/app/Http/Controllers/Repuve/AppController.php b/app/Http/Controllers/Repuve/AppController.php index 650a404..8c4b496 100644 --- a/app/Http/Controllers/Repuve/AppController.php +++ b/app/Http/Controllers/Repuve/AppController.php @@ -4,61 +4,140 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Repuve\ApkStorageRequest; +use App\Http\Requests\Repuve\ApkUpdateRequest; +use App\Models\ApkLog; +use App\Models\ApkVersion; +use Illuminate\Routing\Controllers\HasMiddleware; use Illuminate\Support\Facades\Storage; use Notsoweb\ApiResponse\Enums\ApiResponse; -use Symfony\Component\HttpFoundation\BinaryFileResponse; -class AppController extends Controller +class AppController extends Controller implements HasMiddleware { - /** - * Middleware - */ + * Middleware + */ public static function middleware(): array { return [ - self::can('apk.upload', ['upload']), + self::can('apk.index', ['index']), + self::can('apk.edit', ['update']), + self::can('apk.destroy', ['destroy']), self::can('apk.download', ['download']), ]; - } + } /** - * Subir APK de la aplicación móvil + * Listar versiones: actual, 2 anteriores y las no disponibles */ - public function upload(ApkStorageRequest $request) + public function index() { - $file = $request->file('apk'); + $latestId = ApkVersion::latest()->value('id'); - // Eliminar APK anterior si existe - $existingFiles = Storage::disk('public')->files('apk'); - foreach ($existingFiles as $existingFile) { - Storage::disk('public')->delete($existingFile); - } - - // Guardar el nuevo APK - $fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk'; - $file->storeAs('apk', $fileName, 'public'); + $versions = ApkVersion::with('uploader:id,name') + ->withCount('logs') + ->latest() + ->get() + ->map(function ($version) use ($latestId) { + $version->is_current = $version->id === $latestId; + $version->available = $version->id === $latestId; + return $version; + }); return ApiResponse::OK->response([ - 'message' => 'APK subido correctamente', - 'file' => $fileName, + 'current' => $versions->where('is_current', true)->first(), + 'recent' => $versions->where('is_current', false)->take(2)->values(), + 'unavailable' => $versions->where('is_current', false)->skip(2)->values(), ]); } /** - * Descargar APK de la aplicación móvil + * Subir nueva versión del APK */ - public function download(): BinaryFileResponse + public function store(ApkStorageRequest $request) { - $files = Storage::disk('public')->files('apk'); + $file = $request->file('apk'); - if (empty($files)) { + $fileName = 'repuve-app-' . now()->format('Y-m-d') . '.apk'; + + // Eliminar APK anterior del storage + foreach (Storage::disk('public')->files('apk') as $existing) { + Storage::disk('public')->delete($existing); + } + + $path = $file->storeAs('apk', $fileName, 'public'); + + ApkVersion::create([ + 'file_name' => $fileName, + 'path' => $path, + 'changelog' => $request->input('changelog'), + 'uploaded_by' => auth()->id(), + ]); + + return ApiResponse::OK->response([ + 'message' => 'APK subido correctamente', + 'file' => $fileName, + ]); + } + + /** + * Actualizar nombre y/o changelog de una versión + */ + public function update(ApkUpdateRequest $request, ApkVersion $app) + { + $data = $request->only(['changelog', 'file_name']); + + if ($request->filled('file_name') && $request->input('file_name') !== $app->file_name) { + $newFileName = $request->input('file_name'); + $newPath = 'apk/' . $newFileName; + + if (Storage::disk('public')->exists($newPath)) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'Ya existe un archivo con ese nombre.', + ]); + } + + Storage::disk('public')->move($app->path, $newPath); + + $data['file_name'] = $newFileName; + $data['path'] = $newPath; + } + + $app->update($data); + + return ApiResponse::OK->response([ + 'message' => 'Versión actualizada correctamente', + ]); + } + + /** + * Eliminar una versión + */ + public function destroy(ApkVersion $app) + { + Storage::disk('public')->delete($app->path); + $app->delete(); + + return ApiResponse::OK->response([ + 'message' => 'Versión eliminada correctamente', + ]); + } + + /** + * Descargar APK más reciente y registrar log + */ + public function download() + { + $version = ApkVersion::latest()->first(); + + if (!$version) { abort(404, 'No hay APK disponible para descargar.'); } - $latestFile = end($files); - $path = Storage::disk('public')->path($latestFile); + ApkLog::create([ + 'apk_version_id' => $version->id, + 'downloaded_by' => auth()->id(), + ]); - return response()->download($path); + return redirect(asset('storage/' . $version->path)); } } diff --git a/app/Http/Requests/Repuve/ApkStorageRequest.php b/app/Http/Requests/Repuve/ApkStorageRequest.php index 1edc7f3..c7fa63d 100644 --- a/app/Http/Requests/Repuve/ApkStorageRequest.php +++ b/app/Http/Requests/Repuve/ApkStorageRequest.php @@ -14,7 +14,7 @@ public function authorize() public function rules() { return [ - 'apk' => 'required|file|max:102400', + 'apk' => 'required|file|max:153600', ]; } @@ -23,7 +23,7 @@ public function messages() return [ 'apk.required' => 'El archivo APK es obligatorio', 'apk.file' => 'El archivo debe ser un archivo válido', - 'apk.max' => 'El archivo no debe superar los 100MB', + 'apk.max' => 'El archivo no debe superar los 150MB', ]; } } diff --git a/app/Http/Requests/Repuve/ApkUpdateRequest.php b/app/Http/Requests/Repuve/ApkUpdateRequest.php new file mode 100644 index 0000000..aba1b1b --- /dev/null +++ b/app/Http/Requests/Repuve/ApkUpdateRequest.php @@ -0,0 +1,31 @@ +user()->can('apk.edit'); + } + + public function rules() + { + return [ + 'file_name' => 'nullable|string|max:255', + 'changelog' => 'nullable|string|max:255', + ]; + } + + public function messages() + { + return [ + 'file_name.string' => 'El nombre del archivo debe ser una cadena de texto', + 'file_name.max' => 'El nombre del archivo no debe superar los 255 caracteres', + 'changelog.string' => 'El changelog debe ser una cadena de texto', + 'changelog.max' => 'El changelog no debe superar los 255 caracteres', + ]; + } +} diff --git a/app/Models/ApkLog.php b/app/Models/ApkLog.php new file mode 100644 index 0000000..9b9c967 --- /dev/null +++ b/app/Models/ApkLog.php @@ -0,0 +1,32 @@ + + * + * @version 1.0.0 + */ +class ApkLog extends Model +{ + protected $fillable = [ + 'apk_version_id', + 'downloaded_by', + ]; + + public function apkVersion() + { + return $this->belongsTo(ApkVersion::class, 'apk_version_id'); + } + + public function downloader() + { + return $this->belongsTo(User::class, 'downloaded_by'); + } +} diff --git a/app/Models/ApkVersion.php b/app/Models/ApkVersion.php new file mode 100644 index 0000000..b5cd478 --- /dev/null +++ b/app/Models/ApkVersion.php @@ -0,0 +1,25 @@ +belongsTo(User::class, 'uploaded_by'); + } + + public function logs() + { + return $this->hasMany(ApkLog::class, 'apk_version_id'); + } +} diff --git a/database/migrations/2026_03_11_124942_create_apk_versions_table.php b/database/migrations/2026_03_11_124942_create_apk_versions_table.php new file mode 100644 index 0000000..d38f60a --- /dev/null +++ b/database/migrations/2026_03_11_124942_create_apk_versions_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('file_name'); + $table->string('path')->nullable(); + $table->text('changelog')->nullable(); + $table->foreignId('uploaded_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('apk_versions'); + } +}; diff --git a/database/migrations/2026_03_11_133332_create_apk_logs_table.php b/database/migrations/2026_03_11_133332_create_apk_logs_table.php new file mode 100644 index 0000000..c47b392 --- /dev/null +++ b/database/migrations/2026_03_11_133332_create_apk_logs_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('apk_version_id')->constrained('apk_versions')->onDelete('cascade'); + $table->foreignId('downloaded_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('apk_logs'); + } +}; diff --git a/database/migrations/data/2026_02_13_095440_reseed_roles_and_permissions.php b/database/migrations/data/2026_03_11_143730_reseed_permissions_table.php similarity index 92% rename from database/migrations/data/2026_02_13_095440_reseed_roles_and_permissions.php rename to database/migrations/data/2026_03_11_143730_reseed_permissions_table.php index 841a545..b3e5f16 100644 --- a/database/migrations/data/2026_02_13_095440_reseed_roles_and_permissions.php +++ b/database/migrations/data/2026_03_11_143730_reseed_permissions_table.php @@ -55,7 +55,6 @@ public function up(): void */ public function down(): void { - // No se puede revertir la eliminación de datos sin un backup - // Si necesitas revertir, restaura desde un backup de la base de datos + // } }; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index b0248f3..3a0609f 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -11,6 +11,7 @@ use Illuminate\Database\Seeder; use Notsoweb\LaravelCore\Traits\MySql\RolePermission; use Spatie\Permission\Models\Permission; +use Spatie\Permission\PermissionRegistrar; /** * Roles y permisos @@ -53,7 +54,9 @@ public function run(): void // === APK === $apk = PermissionType::updateOrCreate(['name' => 'App Móvil']); - $apkUpload = $this->onPermission('apk.upload', 'Subir APK de la aplicación móvil', $apk, 'api'); + $apkIndex = $this->onPermission('apk.index', 'Historial de registros apk', $apk, 'api'); + $apkEdit = $this->onPermission('apk.edit', 'Actualizar registro de apk', $apk, 'api'); + $apkDestroy = $this->onPermission('apk.destroy', 'Eliminar registro de apk', $apk, 'api'); $apkDownload = $this->onPermission('apk.download', 'Descargar APK de la aplicación móvil', $apk, 'api'); // === MÓDULOS === @@ -190,7 +193,7 @@ public function run(): void // Constancias $tagIndex, $tagCreate, $tagEdit, $tagDestroy, //app - $apkUpload, $apkDownload, + $apkIndex, $apkEdit, $apkDestroy, $apkDownload, ); // Encargado diff --git a/entrypoint-dev.sh b/entrypoint-dev.sh index c37f9ef..dc85001 100644 --- a/entrypoint-dev.sh +++ b/entrypoint-dev.sh @@ -61,8 +61,8 @@ php artisan storage:link --force || true echo "Ejecutando configuración de desarrollo..." composer run env:dev -echo "Creando directorio de claves Passport..." -mkdir -p storage/app/keys +echo "Creando directorios necesarios..." +mkdir -p storage/app/keys storage/app/public/apk echo "Generando claves de Passport..." php artisan passport:keys --force || true diff --git a/entrypoint-prod.sh b/entrypoint-prod.sh index 3ca6a8e..65dcccf 100644 --- a/entrypoint-prod.sh +++ b/entrypoint-prod.sh @@ -71,8 +71,8 @@ php artisan view:cache echo "Generando caché de eventos..." php artisan event:cache -echo "Creando directorio de claves Passport..." -mkdir -p storage/app/keys +echo "Creando directorios necesarios..." +mkdir -p storage/app/keys storage/app/public/apk if [ ! -f "storage/app/keys/oauth-private.key" ] || [ ! -f "storage/app/keys/oauth-public.key" ]; then echo "Claves de Passport no encontradas, generando..." diff --git a/routes/api.php b/routes/api.php index fd1b883..893aadc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -94,8 +94,11 @@ Route::post('repuve-credentials/decrypt', [SettingsController::class, 'decrypt']); // Rutas App móvil + Route::get('app', [AppController::class, 'index']); + Route::post('app', [AppController::class, 'store']); + Route::put('app/{app}', [AppController::class, 'update']); + Route::delete('app/{app}', [AppController::class, 'destroy']); Route::get('app/download', [AppController::class, 'download']); - Route::post('app/upload', [AppController::class, 'upload']); });