--- File: D:\projects\digitalvocano\Modules\AppManager\app\Http\Controllers\Admin\ActivationLogController.php --- storageDisk = config('appmanager.storage_disk', 'local'); } public function download(Request $request) { $validator = Validator::make($request->all(), [ 'product_slug' => 'required|string|exists:am_managed_scripts,slug', 'file_identifier' => 'required|string', // This could be the target_path or a unique ID/file_name 'version' => 'required|string', 'activation_token' => 'required|string', // The token received from successful activation 'license_key' => 'sometimes|required|string', // Optional: For an extra layer of validation if token is tied to license 'domain' => 'sometimes|required|string', // Optional: If token is tied to a domain ]); if ($validator->fails()) { return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400); } // --- Robust Token Validation --- $downloadToken = DownloadToken::where('token', $request->input('activation_token'))->first(); if (!$downloadToken) { Log::warning('AppManager: Invalid download token used.', ['token' => $request->input('activation_token'), 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Invalid or expired download token.'], 403); } if ($downloadToken->isExpired()) { Log::info('AppManager: Expired download token used.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Download token has expired.'], 403); } if ($downloadToken->isMaxUsesReached()) { Log::info('AppManager: Download token max uses reached.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Download token usage limit reached.'], 403); } // Optional: Further checks if token is tied to specific license_key or domain // This requires the DownloadToken to have a direct relationship or stored metadata // For example, if DownloadToken belongs to a License: if ($request->filled('license_key') && $downloadToken->license->license_key !== $request->input('license_key')) { Log::warning('AppManager: Download token license mismatch.', [ 'token_id' => $downloadToken->id, 'expected_key' => $downloadToken->license->license_key, 'provided_key' => $request->input('license_key'), 'ip' => $request->ip() ]); return response()->json(['status' => 'error', 'message' => 'Token not valid for this license.'], 403); } // You might also check if the token was generated for the correct product_slug via $downloadToken->license->managed_script_id // --- End Robust Token Validation --- $script = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail(); $downloadableFile = DownloadableFile::where('managed_script_id', $script->id) ->where(function($query) use ($request) { // Allow lookup by target_path primarily, or file_name as a fallback $query->where('target_path', $request->input('file_identifier')) ->orWhere('file_name', $request->input('file_identifier')); }) ->where('version', $request->input('version')) ->first(); if (!$downloadableFile) { Log::info('AppManager: Downloadable file not found or version mismatch.', $request->all()); return response()->json(['status' => 'error', 'message' => 'File not found or version mismatch.'], 404); } $filePathOnDisk = $downloadableFile->file_path; // This path is relative to the disk's root if (!Storage::disk($this->storageDisk)->exists($filePathOnDisk)) { Log::error("AppManager: File source not found on server for download.", ['path' => $filePathOnDisk, 'request' => $request->all()]); return response()->json(['status' => 'error', 'message' => 'File source not found on server.'], 500); } // Optional: Verify hash before sending $currentHash = hash_file('sha256', Storage::disk($this->storageDisk)->path($filePathOnDisk)); if ($downloadableFile->file_hash && $currentHash !== $downloadableFile->file_hash) { Log::critical("AppManager: CRITICAL - File hash mismatch for download.", [ 'file_record_id' => $downloadableFile->id, 'path' => $filePathOnDisk, 'stored_hash' => $downloadableFile->file_hash, 'current_hash' => $currentHash ]); return response()->json(['status' => 'error', 'message' => 'File integrity check failed on server.'], 500); } $downloadToken->incrementUsage(); // Optionally, delete the token if it has reached max uses and you want it to be strictly single-session for a set of downloads // if ($downloadToken->isMaxUsesReached()) { $downloadToken->delete(); } return Storage::disk($this->storageDisk)->download($filePathOnDisk, $downloadableFile->file_name, [ 'Content-Type' => Storage::disk($this->storageDisk)->mimeType($filePathOnDisk) ?? 'application/octet-stream', 'X-File-Hash' => $downloadableFile->file_hash // Send hash for client-side verification ]); } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Http\Controllers\AppManagerController.php --- 'datetime', ]; public function license(): BelongsTo { return $this->belongsTo(License::class); } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\DownloadableFile.php --- belongsTo(ManagedScript::class); } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\DownloadToken.php --- 'datetime', 'supported_until' => 'datetime', 'metadata' => 'array', // 'is_boilerplate_core_license' => 'boolean', // Example if you have such a field, ensure it's in fillable if mass assignable ]; // If you have a factory, uncomment and point to the correct path // protected static function newFactory() // { // return \Modules\AppManager\Database\factories\LicenseFactory::new(); // } public function managedScript(): BelongsTo { return $this->belongsTo(ManagedScript::class); } public function activationLogs(): HasMany { // Assuming ActivationLog model is in the same namespace return $this->hasMany(ActivationLog::class); } protected static function booted() { parent::boot(); // It's good practice to call parent::boot() static::creating(function ($license) { // Check for the temporary flag set by the controller if (empty($license->license_key) && ($license->attributes['auto_generate_key_flag'] ?? false)) { $maxAttempts = 5; // Prevent infinite loop in rare collision cases $attempt = 0; do { // Generate a more robust key // Example: LIC-SCRIPTNAME-RANDOM-RANDOM-RANDOM-RANDOM $scriptPrefix = strtoupper(Str::slug($license->managedScript->name ?? 'APP', '')); $prefix = "LIC-{$scriptPrefix}-"; $key = $prefix . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)); $attempt++; } while (static::where('license_key', $key)->exists() && $attempt < $maxAttempts); if ($attempt >= $maxAttempts && static::where('license_key', $key)->exists()) { // Fallback or throw an exception if a unique key couldn't be generated // For simplicity, appending a timestamp or more random chars $key .= '-' . strtoupper(Str::random(4)); } $license->license_key = $key; } // Remove the temporary flag as it's not a DB column unset($license->attributes['auto_generate_key_flag']); }); static::updating(function ($license) { // Check if the license_key is being cleared/empty and auto_generate_key_flag is set if (empty($license->license_key) && ($license->attributes['auto_generate_key_flag'] ?? false)) { $maxAttempts = 5; $attempt = 0; do { $scriptPrefix = strtoupper(Str::slug($license->managedScript->name ?? 'APP', '')); $prefix = "LIC-{$scriptPrefix}-"; $key = $prefix . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)) . '-' . strtoupper(Str::random(6)); $attempt++; } while (static::where('license_key', $key)->where('id', '!=', $license->id)->exists() && $attempt < $maxAttempts); if ($attempt >= $maxAttempts && static::where('license_key', $key)->where('id', '!=', $license->id)->exists()) { $key .= '-' . strtoupper(Str::random(4)); } $license->license_key = $key; } // Remove the temporary flag as it's not a DB column unset($license->attributes['auto_generate_key_flag']); }); } /** * Get all unique successfully activated domains for this license. * * @return \Illuminate\Support\Collection */ public function getActiveDomains(): \Illuminate\Support\Collection { return $this->activationLogs() ->where('status', 'success') // Assuming 'success' is the status for successful activation ->distinct('activated_domain') ->orderBy('activated_at', 'desc') // Optional: get the most recent first ->pluck('activated_domain'); } /** * Increment the current activations count. * * @return bool */ public function incrementActivations(): bool { $this->current_activations = ($this->current_activations ?? 0) + 1; return $this->save(); } /** * Decrement the current activations count. * * @return bool */ public function decrementActivations(): bool { if ($this->current_activations > 0) { $this->current_activations--; return $this->save(); } return true; // Or false if you want to indicate no change was made } /** * Check if the license has reached its activation limit. * Considers 0 as unlimited. * * @return bool */ public function hasReachedActivationLimit(): bool { if ($this->activation_limit == 0) { // 0 means unlimited return false; } return ($this->current_activations ?? 0) >= $this->activation_limit; } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Models\ManagedScript.php --- 'boolean', ]; protected static function newFactory() { // return \Modules\AppManager\Database\factories\ManagedScriptFactory::new(); } public function downloadableFiles(): HasMany { return $this->hasMany(DownloadableFile::class); } public function licenses(): HasMany { return $this->hasMany(License::class); } protected static function booted() { static::creating(function ($script) { if (empty($script->slug)) { $script->slug = Str::slug($script->name); } }); } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\AppManagerServiceProvider.php --- registerCommands(); $this->registerCommandSchedules(); $this->registerTranslations(); $this->registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->name, 'database/migrations')); } /** * Register the service provider. */ public function register(): void { $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } /** * Register commands in the format of Command::class */ protected function registerCommands(): void { // $this->commands([]); } /** * Register command Schedules. */ protected function registerCommandSchedules(): void { // $this->app->booted(function () { // $schedule = $this->app->make(Schedule::class); // $schedule->command('inspire')->hourly(); // }); } /** * Register translations. */ public function registerTranslations(): void { $langPath = resource_path('lang/modules/'.$this->nameLower); if (is_dir($langPath)) { $this->loadTranslationsFrom($langPath, $this->nameLower); $this->loadJsonTranslationsFrom($langPath); } else { $this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower); $this->loadJsonTranslationsFrom(module_path($this->name, 'lang')); } } /** * Register config. */ protected function registerConfig(): void { $configPath = module_path($this->name, config('modules.paths.generator.config.path')); if (is_dir($configPath)) { $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); foreach ($iterator as $file) { if ($file->isFile() && $file->getExtension() === 'php') { $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); $segments = explode('.', $this->nameLower.'.'.$config_key); // Remove duplicated adjacent segments $normalized = []; foreach ($segments as $segment) { if (end($normalized) !== $segment) { $normalized[] = $segment; } } $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); $this->publishes([$file->getPathname() => config_path($config)], 'config'); $this->merge_config_from($file->getPathname(), $key); } } } } /** * Merge config from the given path recursively. */ protected function merge_config_from(string $path, string $key): void { $existing = config($key, []); $module_config = require $path; config([$key => array_replace_recursive($existing, $module_config)]); } /** * Register views. */ public function registerViews(): void { $viewPath = resource_path('views/modules/'.$this->nameLower); $sourcePath = module_path($this->name, 'resources/views'); $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); } /** * Get the services provided by the provider. */ public function provides(): array { return []; } private function getPublishableViewPaths(): array { $paths = []; foreach (config('view.paths') as $path) { if (is_dir($path.'/modules/'.$this->nameLower)) { $paths[] = $path.'/modules/'.$this->nameLower; } } return $paths; } } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\EventServiceProvider.php --- > */ protected $listen = []; /** * Indicates if events should be discovered. * * @var bool */ protected static $shouldDiscoverEvents = true; /** * Configure the proper event listeners for email verification. */ protected function configureEmailVerification(): void {} } --- File: D:\projects\digitalvocano\Modules\AppManager\app\Providers\RouteServiceProvider.php --- mapApiRoutes(); $this->mapWebRoutes(); } /** * Define the "web" routes for the application. * * These routes all receive session state, CSRF protection, etc. */ protected function mapWebRoutes(): void { Route::middleware('web')->group(module_path($this->name, '/routes/web.php')); } /** * Define the "api" routes for the application. * * These routes are typically stateless. */ protected function mapApiRoutes(): void { Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php')); } } --- File: D:\projects\digitalvocano\Modules\AppManager\config\config.php --- 'AppManager', /* |-------------------------------------------------------------------------- | Storage Disk for Downloadable Files |-------------------------------------------------------------------------- | | Specify the Laravel filesystem disk where downloadable files managed by | AppManager will be stored. 'local' is recommended for security, | as files will not be publicly accessible via a URL. | */ 'storage_disk' => env('APPMANAGER_STORAGE_DISK', 'local'), /* |-------------------------------------------------------------------------- | Envato API Configuration |-------------------------------------------------------------------------- | | Settings for integrating with the Envato API to verify purchase codes. | */ 'envato_personal_token' => env('ENVATO_PERSONAL_TOKEN', null), // You might map product slugs to Envato Item IDs here or in the database // 'envato_item_ids' => [ // 'your-script-slug' => 12345678, // Example Envato Item ID // ], /* |-------------------------------------------------------------------------- | Download Token Configuration |-------------------------------------------------------------------------- */ 'download_token_ttl_minutes' => env('APPMANAGER_DOWNLOAD_TOKEN_TTL', 30), // Time-to-live in minutes 'download_token_max_uses' => env('APPMANAGER_DOWNLOAD_TOKEN_MAX_USES', 5), // Max uses per token (e.g., for a set of files) ]; --- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\ActivationLogFactoryFactory.php --- License::factory(), // Assumes LicenseFactory exists 'token' => Str::random(60), 'ip_address' => $this->faker->ipv4, 'user_agent' => $this->faker->userAgent, 'uses' => 0, 'max_uses' => $this->faker->numberBetween(1, 5), 'expires_at' => now()->addHours($this->faker->numberBetween(1, 24)), ]; } } --- File: D:\projects\digitalvocano\Modules\AppManager\database\factories\DownloadTokenFactoryFactory.php --- id(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('downloadable_files'); } }; --- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_120648_create_licenses_table.php --- id(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('licenses'); } }; --- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_120741_create_activation_logs_table.php --- id(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('activation_logs'); } }; --- File: D:\projects\digitalvocano\Modules\AppManager\database\migrations\2025_05_24_124924_add_envato_item_id_to_am_managed_scripts_table.php --- id(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('download_tokens'); } }; --- File: D:\projects\digitalvocano\Modules\AppManager\database\seeders\AppManagerDatabaseSeeder.php --- call([]); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Entities\ActivationLog.php --- 'datetime', ]; public function license(): BelongsTo { return $this->belongsTo(License::class); } /** * Get the managed script associated with the activation log. */ public function managedScript(): BelongsTo { return $this->belongsTo(ManagedScript::class); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Entities\DownloadableFile.php --- belongsTo(ManagedScript::class); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Entities\DownloadToken.php --- 'datetime', ]; protected static function newFactory() { return \Modules\AppManager\Database\factories\DownloadTokenFactory::new(); } public function license(): BelongsTo { return $this->belongsTo(License::class); } protected static function booted() { static::creating(function ($token) { if (empty($token->token)) { $token->token = Str::random(60); // Ensure token is generated } }); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Entities\License.php --- 'datetime', 'supported_until' => 'datetime', 'metadata' => 'array', ]; protected static function newFactory() { // return \Modules\AppManager\Database\factories\LicenseFactory::new(); } public function managedScript(): BelongsTo { return $this->belongsTo(ManagedScript::class); } public function activationLogs(): HasMany { return $this->hasMany(ActivationLog::class); } protected static function booted() { static::creating(function ($license) { if (empty($license->license_key)) { // Generate a more robust key, e.g., prefix + UUID $prefix = strtoupper(Str::slug($license->managedScript->name ?? 'LIC', '_')); $license->license_key = $prefix . '_' . Str::uuid()->toString(); } }); } public function getActiveDomains(): \Illuminate\Support\Collection { return $this->activationLogs() ->where('status', 'success') ->distinct('activated_domain') // Ensure we only get unique domains ->pluck('activated_domain'); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Entities\ManagedScript.php --- 'boolean', ]; protected static function newFactory() { // return \Modules\AppManager\Database\factories\ManagedScriptFactory::new(); } public function downloadableFiles(): HasMany { return $this->hasMany(DownloadableFile::class); } public function licenses(): HasMany { return $this->hasMany(License::class); } protected static function booted() { static::creating(function ($script) { if (empty($script->slug)) { $script->slug = Str::slug($script->name); } }); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\ActivationLogController.php --- orderBy('activated_at', 'desc'); if ($request->filled('search')) { $searchTerm = $request->input('search'); $query->where(function ($q) use ($searchTerm) { $q->where('activated_domain', 'like', "%{$searchTerm}%") ->orWhere('ip_address', 'like', "%{$searchTerm}%") ->orWhereHas('license', function ($lq) use ($searchTerm) { $lq->where('license_key', 'like', "%{$searchTerm}%") ->orWhere('customer_email', 'like', "%{$searchTerm}%"); }); }); } if ($request->filled('status') && $request->status !== 'all') { $query->where('status', $request->input('status')); } $logs = $query->paginate(20)->withQueryString(); $statuses = ['success' => 'Success', 'failed_invalid_key' => 'Invalid Key', 'failed_limit_reached' => 'Limit Reached', 'failed_expired' => 'Expired', 'failed_domain_mismatch' => 'Domain Mismatch', 'failed_server_error' => 'Server Error']; return view('appmanager::admin.activation_logs.index', compact('logs', 'statuses')); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\DownloadableFileController.php --- downloadableFiles()->orderBy('version', 'desc')->orderBy('file_name')->paginate(15); return view('appmanager::admin.downloadable_files.index', compact('script', 'files')); } /** * Show the form for creating a new resource. * @param ManagedScript $script * @return \Illuminate\Contracts\View\View */ public function create(ManagedScript $script) { return view('appmanager::admin.downloadable_files.create', compact('script')); } /** * Store a newly created resource in storage. * @param Request $request * @param ManagedScript $script * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request, ManagedScript $script) { $request->validate([ 'uploaded_file' => 'required|file|max:102400', // Max 100MB, adjust as needed 'version' => 'required|string|max:50', 'target_path' => 'required|string|max:255', 'description' => 'nullable|string', 'is_critical' => 'boolean', ]); $data = $request->only(['version', 'target_path', 'description']); $data['is_critical'] = $request->has('is_critical'); if ($request->hasFile('uploaded_file')) { $file = $request->file('uploaded_file'); $originalFileName = $file->getClientOriginalName(); // Sanitize original file name for storage path, or use a UUID $safeFileName = Str::slug(pathinfo($originalFileName, PATHINFO_FILENAME)) . '.' . $file->getClientOriginalExtension(); $versionPath = Str::slug($data['version']); $securePath = "{$this->baseStoragePath}/{$script->slug}/{$versionPath}"; // Store the file $storedFilePath = $file->storeAs($securePath, $safeFileName, $this->storageDisk); if (!$storedFilePath) { return redirect()->back()->with('error', 'Failed to upload file. Please check storage permissions.')->withInput(); } $data['file_name'] = $originalFileName; $data['file_path'] = $storedFilePath; // Path relative to the disk's root $data['file_hash'] = hash_file('sha256', Storage::disk($this->storageDisk)->path($storedFilePath)); } else { return redirect()->back()->with('error', 'File is required.')->withInput(); } $script->downloadableFiles()->create($data); return redirect()->route('admin.appmanager.scripts.files.index', $script) ->with('success', 'Downloadable file uploaded and created successfully.'); } /** * Show the form for editing the specified resource. * @param ManagedScript $script * @param DownloadableFile $file * @return \Illuminate\Contracts\View\View */ public function edit(ManagedScript $script, DownloadableFile $file) { if ($file->managed_script_id !== $script->id) { abort(404); // Or redirect with error } return view('appmanager::admin.downloadable_files.edit', compact('script', 'file')); } /** * Update the specified resource in storage. * @param Request $request * @param ManagedScript $script * @param DownloadableFile $file * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, ManagedScript $script, DownloadableFile $file) { if ($file->managed_script_id !== $script->id) { abort(404); } $request->validate([ 'uploaded_file' => 'nullable|file|max:102400', // Optional on update 'version' => 'required|string|max:50', 'target_path' => 'required|string|max:255', 'description' => 'nullable|string', 'is_critical' => 'boolean', ]); $data = $request->only(['version', 'target_path', 'description']); $data['is_critical'] = $request->has('is_critical'); if ($request->hasFile('uploaded_file')) { // Delete old file if ($file->file_path && Storage::disk($this->storageDisk)->exists($file->file_path)) { Storage::disk($this->storageDisk)->delete($file->file_path); } $newUploadedFile = $request->file('uploaded_file'); $originalFileName = $newUploadedFile->getClientOriginalName(); $safeFileName = Str::slug(pathinfo($originalFileName, PATHINFO_FILENAME)) . '.' . $newUploadedFile->getClientOriginalExtension(); $versionPath = Str::slug($data['version']); $securePath = "{$this->baseStoragePath}/{$script->slug}/{$versionPath}"; $storedFilePath = $newUploadedFile->storeAs($securePath, $safeFileName, $this->storageDisk); if (!$storedFilePath) { return redirect()->back()->with('error', 'Failed to upload new file. Please check storage permissions.')->withInput(); } $data['file_name'] = $originalFileName; $data['file_path'] = $storedFilePath; $data['file_hash'] = hash_file('sha256', Storage::disk($this->storageDisk)->path($storedFilePath)); } $file->update($data); return redirect()->route('admin.appmanager.scripts.files.index', $script) ->with('success', 'Downloadable file updated successfully.'); } /** * Remove the specified resource from storage. * @param ManagedScript $script * @param DownloadableFile $file * @return \Illuminate\Http\RedirectResponse */ public function destroy(ManagedScript $script, DownloadableFile $file) { if ($file->managed_script_id !== $script->id) { abort(404); } try { if ($file->file_path && Storage::disk($this->storageDisk)->exists($file->file_path)) { Storage::disk($this->storageDisk)->delete($file->file_path); } $file->delete(); return redirect()->route('admin.appmanager.scripts.files.index', $script) ->with('success', 'Downloadable file deleted successfully.'); } catch (\Exception $e) { return redirect()->route('admin.appmanager.scripts.files.index', $script) ->with('error', 'Failed to delete file: ' . $e->getMessage()); } } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\LicenseController.php --- orderBy('created_at', 'desc'); if ($request->filled('search')) { $searchTerm = $request->input('search'); $query->where(function ($q) use ($searchTerm) { $q->where('license_key', 'like', "%{$searchTerm}%") ->orWhere('customer_email', 'like', "%{$searchTerm}%") ->orWhere('purchase_code', 'like', "%{$searchTerm}%") ->orWhereHas('managedScript', function ($sq) use ($searchTerm) { $sq->where('name', 'like', "%{$searchTerm}%"); }); }); } if ($request->filled('managed_script_id')) { $query->where('managed_script_id', $request->input('managed_script_id')); } if ($request->filled('status') && $request->status !== 'all') { $query->where('status', $request->input('status')); } $licenses = $query->paginate(15)->withQueryString(); $scripts = ManagedScript::orderBy('name')->pluck('name', 'id'); $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked']; return view('appmanager::admin.licenses.index', compact('licenses', 'scripts', 'statuses')); } public function create() { $scripts = ManagedScript::orderBy('name')->pluck('name', 'id'); $types = ['internal' => 'Internal', 'codecanyon' => 'CodeCanyon', 'subscription' => 'Subscription']; $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked']; return view('appmanager::admin.licenses.create', compact('scripts', 'types', 'statuses')); } public function store(Request $request) { $rules = [ 'managed_script_id' => 'required|exists:am_managed_scripts,id', 'auto_generate_key' => 'nullable|boolean', 'type' => 'required|in:internal,codecanyon,subscription', 'customer_email' => 'nullable|email|max:255', 'customer_name' => 'nullable|string|max:255', 'purchase_code' => [ 'nullable', 'string', 'max:255', Rule::unique('am_licenses')->where(function ($query) use ($request) { return $query->where('type', 'codecanyon'); // Ensures uniqueness among CodeCanyon type licenses if purchase_code is provided }), ], 'activation_limit' => 'required|integer|min:0', // 0 for unlimited 'status' => 'required|in:pending,active,suspended,expired,revoked', 'expires_at' => 'nullable|date', 'does_not_expire' => 'nullable|boolean', 'supported_until' => 'nullable|date', 'metadata' => 'nullable|json', 'notes' => 'nullable|string', ]; // Conditionally add rules for license_key if ($request->boolean('auto_generate_key')) { // If auto-generating, license_key is optional from input. If provided, validate it. $rules['license_key'] = [ 'nullable', 'string', 'max:255', Rule::unique('am_licenses', 'license_key'), // Corrected: unique rule on nullable field handles "unique if filled" ]; } else { // If not auto-generating, license_key is required and must be unique. $rules['license_key'] = [ 'required', 'string', 'max:255', Rule::unique('am_licenses', 'license_key'), ]; } $request->validate($rules); $data = $request->except(['auto_generate_key', 'does_not_expire']); if ($request->boolean('auto_generate_key') && empty($request->input('license_key'))) { $data['auto_generate_key_flag'] = true; // Model will handle generation } if ($request->boolean('does_not_expire')) { $data['expires_at'] = null; } if ($data['type'] !== 'codecanyon') { $data['purchase_code'] = null; // Ensure purchase code is null if not CodeCanyon type } License::create($data); return redirect()->route('admin.appmanager.licenses.index') ->with('success', 'License created successfully.'); } public function show(License $license) { $license->load(['managedScript', 'activationLogs' => function ($query) { $query->orderBy('activated_at', 'desc'); }]); return view('appmanager::admin.licenses.show', compact('license')); } public function edit(License $license) { $scripts = ManagedScript::orderBy('name')->pluck('name', 'id'); $types = ['internal' => 'Internal', 'codecanyon' => 'CodeCanyon', 'subscription' => 'Subscription']; $statuses = ['pending' => 'Pending', 'active' => 'Active', 'suspended' => 'Suspended', 'expired' => 'Expired', 'revoked' => 'Revoked']; return view('appmanager::admin.licenses.edit', compact('license', 'scripts', 'types', 'statuses')); } public function update(Request $request, License $license) { $rules = [ 'managed_script_id' => 'required|exists:am_managed_scripts,id', 'type' => 'required|in:internal,codecanyon,subscription', 'customer_email' => 'nullable|email|max:255', 'customer_name' => 'nullable|string|max:255', 'purchase_code' => [ 'nullable', 'string', 'max:255', Rule::unique('am_licenses')->where(function ($query) use ($request) { return $query->where('type', 'codecanyon'); })->ignore($license->id), ], 'activation_limit' => 'required|integer|min:0', 'status' => 'required|in:pending,active,suspended,expired,revoked', 'expires_at' => 'nullable|date', 'does_not_expire' => 'nullable|boolean', 'supported_until' => 'nullable|date', 'metadata' => 'nullable|json', 'notes' => 'nullable|string', ]; // Conditionally add rules for license_key for update if ($request->boolean('auto_generate_key')) { // If user checks auto_generate_key on update (perhaps after clearing the key) $rules['license_key'] = [ 'nullable', 'string', 'max:255', Rule::unique('am_licenses', 'license_key')->ignore($license->id), // Corrected: unique rule on nullable field handles "unique if filled" ]; } else { $rules['license_key'] = [ 'required', 'string', 'max:255', Rule::unique('am_licenses', 'license_key')->ignore($license->id), ]; } $request->validate($rules); $data = $request->except(['auto_generate_key', 'does_not_expire']); if ($request->boolean('does_not_expire')) { $data['expires_at'] = null; } // If user cleared the key and wants to auto-generate a new one on update if ($request->boolean('auto_generate_key') && empty($request->input('license_key'))) { $data['auto_generate_key_flag'] = true; // Model's 'updating' hook will handle generation } if ($data['type'] !== 'codecanyon') { $data['purchase_code'] = null; } $license->update($data); return redirect()->route('admin.appmanager.licenses.index') ->with('success', 'License updated successfully.'); } public function destroy(License $license) { $license->delete(); return redirect()->route('admin.appmanager.licenses.index') ->with('success', 'License deleted successfully.'); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Admin\ManagedScriptController.php --- orderBy('name'); // Eager load license count if ($request->filled('search')) { $searchTerm = $request->input('search'); $query->where(function ($q) use ($searchTerm) { $q->where('name', 'like', "%{$searchTerm}%") ->orWhere('slug', 'like', "%{$searchTerm}%") ->orWhere('description', 'like', "%{$searchTerm}%"); }); } $scripts = $query->paginate(15)->withQueryString(); // New Stats Data $totalScriptsCount = ManagedScript::count(); $totalLicensesCount = License::count(); $recentSuccessfulActivationsCount = ActivationLog::where('status', 'success') ->where('activated_at', '>=', now()->subDays(7)) ->count(); return view('appmanager::admin.managed_scripts.index', compact( 'scripts', 'totalScriptsCount', 'totalLicensesCount', 'recentSuccessfulActivationsCount' )); } /** * Show the form for creating a new resource. * @return \Illuminate\Contracts\View\View */ public function create() { $statuses = ['development' => 'Development', 'active' => 'Active', 'beta' => 'Beta', 'deprecated' => 'Deprecated', 'archived' => 'Archived']; return view('appmanager::admin.managed_scripts.create', compact('statuses')); } /** * Store a newly created resource in storage. * @param Request $request * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255|unique:am_managed_scripts,name', 'slug' => 'nullable|string|max:255|unique:am_managed_scripts,slug', 'current_version' => 'required|string|max:50', 'description' => 'nullable|string', 'changelog' => 'nullable|string', 'is_boilerplate_core' => 'boolean', 'status' => 'required|in:development,active,beta,deprecated,archived', 'envato_item_id' => 'nullable|string|max:50', // Add validation ]); $data = $request->all(); $data['slug'] = $request->input('slug') ?: Str::slug($request->input('name')); $data['is_boilerplate_core'] = $request->has('is_boilerplate_core'); ManagedScript::create($data); return redirect()->route('admin.appmanager.scripts.index') ->with('success', 'Managed script created successfully.'); } /** * Display the specified resource. * @param ManagedScript $script * @return \Illuminate\Contracts\View\View */ public function show(ManagedScript $script) { // Typically, for admin, edit is more common than show. // You might want to show associated files or licenses here. return view('appmanager::admin.managed_scripts.show', compact('script')); } /** * Show the form for editing the specified resource. * @param ManagedScript $script * @return \Illuminate\Contracts\View\View */ public function edit(ManagedScript $script) { $statuses = ['development' => 'Development', 'active' => 'Active', 'beta' => 'Beta', 'deprecated' => 'Deprecated', 'archived' => 'Archived']; return view('appmanager::admin.managed_scripts.edit', compact('script', 'statuses')); } /** * Update the specified resource in storage. * @param Request $request * @param ManagedScript $script * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, ManagedScript $script) { $request->validate([ 'name' => 'required|string|max:255|unique:am_managed_scripts,name,' . $script->id, 'slug' => 'nullable|string|max:255|unique:am_managed_scripts,slug,' . $script->id, 'current_version' => 'required|string|max:50', 'description' => 'nullable|string', 'changelog' => 'nullable|string', 'is_boilerplate_core' => 'boolean', 'status' => 'required|in:development,active,beta,deprecated,archived', 'envato_item_id' => 'nullable|string|max:50', // Add validation ]); $data = $request->all(); $data['slug'] = $request->input('slug') ?: Str::slug($request->input('name')); $data['is_boilerplate_core'] = $request->has('is_boilerplate_core'); $script->update($data); return redirect()->route('admin.appmanager.scripts.index') ->with('success', 'Managed script updated successfully.'); } /** * Remove the specified resource from storage. * @param ManagedScript $script * @return \Illuminate\Http\RedirectResponse */ public function destroy(ManagedScript $script) { // Consider what happens to associated licenses and files. // Soft delete or prevent deletion if dependencies exist? $script->delete(); return redirect()->route('admin.appmanager.scripts.index') ->with('success', 'Managed script deleted successfully.'); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\ActivationController.php --- all()); Log::debug('AppManager Activation Request Content:', ['content' => $request->getContent()]); // Log raw content Log::debug('AppManager Activation Request Headers:', $request->headers->all()); $validator = Validator::make($request->all(), [ 'license_key' => 'required_without:purchase_code|string|max:255', 'purchase_code' => 'required_without:license_key|string|max:255', 'product_slug' => 'required|string|exists:am_managed_scripts,slug', 'domain' => 'required|string|max:255', // Consider validating as URL/domain 'server_signature' => 'nullable|string|max:255', // Optional hash of server info ]); if ($validator->fails()) { return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400); } $product = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail(); $license = null; $envatoToken = config('appmanager.envato_personal_token'); $purchaseCode = trim($request->input('purchase_code')); // Trim whitespace if ($request->filled('license_key')) { $license = License::where('license_key', $request->input('license_key')) ->where('managed_script_id', $product->id) ->first(); } elseif ($request->filled('purchase_code') && $purchaseCode) { // Validate purchase code format before sending to Envato if (!preg_match("/^([a-f0-9]{8})-(([a-f0-9]{4})-){3}([a-f0-9]{12})$/i", $purchaseCode)) { $errorMsg = "Invalid purchase code format."; Log::info('AppManager: Invalid purchase code format received.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'ip' => $request->ip()]); $this->logActivationAttempt($request, $product, null, 'failed_invalid_purchase_code_format', $errorMsg); return response()->json(['status' => 'error', 'message' => $errorMsg], 400); // 400 for bad request } if ($product->envato_item_id && $envatoToken) { try { $envatoResponse = Http::withToken($envatoToken) ->withUserAgent(config('app.name', 'Laravel') . ' - AppManager Purchase Verification') ->timeout(15) // Set a reasonable timeout ->get("https://api.envato.com/v3/market/author/sale?code={$purchaseCode}"); if ($envatoResponse->successful()) { $saleData = $envatoResponse->json(); if (isset($saleData['item']['id']) && $saleData['item']['id'] == $product->envato_item_id) { // Purchase code is valid for this item $baseLicenseData = [ 'type' => 'codecanyon', 'customer_email' => $saleData['buyer'] ?? null, 'customer_name' => $saleData['buyer_username'] ?? ($saleData['buyer'] ?? null), 'status' => 'active', // Default to active on successful verification 'supported_until' => isset($saleData['supported_until']) ? \Carbon\Carbon::parse($saleData['supported_until']) : null, ]; // Find existing license by purchase code for this product, or create a new one $license = License::updateOrCreate( ['purchase_code' => $purchaseCode, 'managed_script_id' => $product->id], $baseLicenseData // These are values for creation or fields to be updated if found ); // Ensure metadata is correctly updated/set. // updateOrCreate handles the other fields from $baseLicenseData. $newMetadata = $license->metadata ?? []; $newMetadata['envato_license_type'] = $saleData['license'] ?? null; $license->metadata = $newMetadata; // Save if metadata changed or if updateOrCreate made changes not yet persisted // (though updateOrCreate typically persists immediately). A single save here is robust. $license->save(); } else { // Valid response from Envato, but item ID doesn't match $errorMsg = 'Purchase code is valid but for a different product.'; Log::warning('AppManager: Envato purchase code product mismatch.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'expected_item_id' => $product->envato_item_id, 'received_item_id' => $saleData['item']['id'] ?? 'N/A', 'ip' => $request->ip()]); $this->logActivationAttempt($request, $product, null, 'failed_envato_product_mismatch', $errorMsg); return response()->json(['status' => 'error', 'message' => $errorMsg], 403); } } else { // Envato request was not successful $statusCode = $envatoResponse->status(); $envatoErrorBody = $envatoResponse->json() ?? ['description' => $envatoResponse->body()]; $errorDescription = $envatoErrorBody['description'] ?? 'Unknown Envato API error.'; if ($statusCode === 404) { $errorMsg = 'Invalid purchase code or it does not belong to your items.'; } elseif ($statusCode === 403) { $errorMsg = 'Envato API permission issue or token invalid. Please check your Envato token.'; } elseif ($statusCode === 401) { $errorMsg = 'Envato API authentication failed. Please check your authorization header.'; } else { $errorMsg = "Envato API error ({$statusCode}): {$errorDescription}"; } Log::warning('AppManager: Envato API request failed.', ['code' => $purchaseCode, 'product_slug' => $product->slug, 'status_code' => $statusCode, 'response_body' => $envatoErrorBody, 'ip' => $request->ip()]); $this->logActivationAttempt($request, $product, null, "failed_envato_api_s{$statusCode}", $errorMsg); return response()->json(['status' => 'error', 'message' => $errorMsg], $statusCode >= 500 ? 503 : 403); } } catch (\Illuminate\Http\Client\ConnectionException $e) { Log::error('AppManager: Envato API connection error.', ['error' => $e->getMessage(), 'ip' => $request->ip()]); $this->logActivationAttempt($request, $product, null, 'failed_envato_api_connection', 'Could not connect to verification server.'); return response()->json(['status' => 'error', 'message' => 'Could not connect to verification server. Please try again later.'], 503); } } else { // Fallback to checking existing licenses if Envato details are not configured for the product $license = License::where('purchase_code', $purchaseCode) ->where('managed_script_id', $product->id) ->first(); } } if (!$license) { $this->logActivationAttempt($request, $product, null, 'failed_license_not_found', 'License key or purchase code not found or invalid for this product.'); return response()->json(['status' => 'error', 'message' => 'License key or purchase code not found or invalid for this product.'], 403); } // Check license status if (!in_array($license->status, ['pending', 'active'])) { $this->logActivationAttempt($request, $product, $license, 'failed_license_status_inactive', 'License is not active (Status: '.$license->status.').'); return response()->json(['status' => 'error', 'message' => 'License is not active. Current status: ' . $license->status], 403); } // Check expiry if ($license->expires_at && $license->expires_at->isPast()) { $this->logActivationAttempt($request, $product, $license, 'failed_license_expired', 'License has expired.'); return response()->json(['status' => 'error', 'message' => 'License has expired.'], 403); } // Check activation limit (0 means unlimited) $domainToActivate = strtolower($request->input('domain')); $existingActivationsForDomain = $license->activationLogs()->where('activated_domain', $domainToActivate)->where('status', 'success')->exists(); // Ensure this uses the correct relationship name if (!$existingActivationsForDomain && $license->activation_limit > 0 && $license->current_activations >= $license->activation_limit) { $this->logActivationAttempt($request, $product, $license, 'failed_activation_limit_reached', 'Activation limit reached for this license.'); return response()->json(['status' => 'error', 'message' => 'Activation limit reached for this license.'], 403); } // Log successful activation $activationLog = $this->logActivationAttempt($request, $product, $license, 'success', 'Activation successful.'); if (!$existingActivationsForDomain) { $license->increment('current_activations'); } if ($license->status === 'pending') { $license->status = 'active'; } $license->save(); // Generate a DownloadToken $downloadToken = DownloadToken::create([ 'license_id' => $license->id, // 'activation_log_id' => $activationLog->id, // Optional: If you add this FK to DownloadToken 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'expires_at' => now()->addMinutes(config('appmanager.download_token_ttl_minutes', 30)), 'max_uses' => config('appmanager.download_token_max_uses', 5), // Allow a few uses for a set of files ]); $activationToken = $downloadToken->token; return response()->json([ 'status' => 'success', 'message' => 'Application activated successfully.', 'activation_token' => $activationToken, // For subsequent file downloads 'product_info' => [ 'name' => $product->name, 'version' => $product->current_version, ], 'license_info' => [ 'status' => $license->status, 'expires_at' => $license->expires_at ? $license->expires_at->toIso8601String() : null, 'supported_until' => $license->supported_until ? $license->supported_until->toIso8601String() : null, 'type' => $license->type, 'envato_license_type' => $license->metadata['envato_license_type'] ?? null ] ]); } public function validateLicense(Request $request): JsonResponse { // Similar validation as activate, but doesn't increment activation count // Just checks if the license is still valid for the given domain/product $validator = Validator::make($request->all(), [ 'license_key' => 'required|string|max:255', 'product_slug' => 'required|string|exists:am_managed_scripts,slug', 'domain' => 'required|string|max:255', ]); if ($validator->fails()) { return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400); } $product = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail(); $license = License::where('license_key', $request->input('license_key')) ->where('managed_script_id', $product->id) ->first(); if (!$license || $license->status !== 'active' || ($license->expires_at && $license->expires_at->isPast())) { return response()->json(['status' => 'invalid', 'message' => 'License is invalid or expired.'], 403); } // Optionally, check if this domain is among the activated ones for this license $isActiveForDomain = $license->activationLogs() ->where('activated_domain', strtolower($request->input('domain'))) ->where('status', 'success') ->exists(); if (!$isActiveForDomain) { return response()->json(['status' => 'invalid', 'message' => 'License not activated for this domain.'], 403); } return response()->json(['status' => 'valid', 'message' => 'License is active.']); } private function logActivationAttempt(Request $request, ManagedScript $product, ?License $license, string $status, string $message): ActivationLog { return ActivationLog::create([ 'license_id' => $license?->id, 'managed_script_id' => $product->id, // Always log the product context 'license_key_attempt' => $request->input('license_key'), 'purchase_code_attempt' => trim($request->input('purchase_code')), // Log trimmed code 'activated_domain' => strtolower($request->input('domain')), 'ip_address' => $request->ip(), 'server_signature' => $request->input('server_signature'), 'status' => $status, 'message' => $message, 'activated_at' => now(), // This will be handled by am_activation_logs table definition if it has `useCurrent` ]); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\ActivationValidationController.php --- validate([ 'activation_token' => 'required|string', 'domain' => 'required|string|url', 'product_slug' => 'required|string', ]); $activationToken = $request->input('activation_token'); $domain = strtolower(trim($request->input('domain'))); $productSlug = $request->input('product_slug'); try { $license = License::where('activation_token', $activationToken)->first(); if (!$license) { return response()->json([ 'status' => 'error', 'is_valid' => false, 'message' => 'Activation token not found.', ], 404); } // Assuming License model has a relationship to ManagedScript (product) // and ManagedScript has a 'slug' field. if (!$license->managedScript || $license->managedScript->slug !== $productSlug) { return response()->json([ 'status' => 'error', 'is_valid' => false, 'message' => 'Product slug mismatch for this token.', ], 400); } // Ensure domain is consistently stored/checked in lowercase if (strtolower(trim($license->activated_domain)) !== $domain) { return response()->json([ 'status' => 'error', 'is_valid' => false, 'message' => 'Domain mismatch for this token.', ], 400); } // Check license status (e.g., 'active', 'expired', 'revoked') // This logic depends on how you manage license statuses in your AppManager if ($license->status !== 'active') { // Adjust 'active' to your actual active status value return response()->json([ 'status' => 'error', 'is_valid' => false, 'message' => "License is not active. Current status: {$license->status}.", ], 400); } // If all checks pass Log::info('AppManager: Activation token validated successfully.', ['token' => $activationToken, 'domain' => $domain, 'product_slug' => $productSlug]); return response()->json([ 'status' => 'success', 'is_valid' => true, 'message' => 'Token is valid.', ]); } catch (\Exception $e) { Log::error('AppManager: Error during activation validation API call.', ['error' => $e->getMessage(), 'token' => $activationToken, 'domain' => $domain]); return response()->json(['status' => 'error', 'is_valid' => false, 'message' => 'An internal error occurred during validation.'], 500); } } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\Api\FileDownloadController.php --- storageDisk = config('appmanager.storage_disk', 'local'); } public function download(Request $request) { $validator = Validator::make($request->all(), [ 'product_slug' => 'required|string|exists:am_managed_scripts,slug', 'file_identifier' => 'required|string', // This could be the target_path or a unique ID/file_name 'version' => 'required|string', 'activation_token' => 'required|string', // The token received from successful activation 'license_key' => 'sometimes|required|string', // Optional: For an extra layer of validation if token is tied to license 'domain' => 'sometimes|required|string', // Optional: If token is tied to a domain ]); if ($validator->fails()) { return response()->json(['status' => 'error', 'message' => 'Validation failed.', 'errors' => $validator->errors()], 400); } // --- Robust Token Validation --- $downloadToken = DownloadToken::where('token', $request->input('activation_token'))->first(); if (!$downloadToken) { Log::warning('AppManager: Invalid download token used.', ['token' => $request->input('activation_token'), 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Invalid or expired download token.'], 403); } if ($downloadToken->isExpired()) { Log::info('AppManager: Expired download token used.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Download token has expired.'], 403); } if ($downloadToken->isMaxUsesReached()) { Log::info('AppManager: Download token max uses reached.', ['token_id' => $downloadToken->id, 'ip' => $request->ip()]); return response()->json(['status' => 'error', 'message' => 'Download token usage limit reached.'], 403); } // Optional: Further checks if token is tied to specific license_key or domain // This requires the DownloadToken to have a direct relationship or stored metadata // For example, if DownloadToken belongs to a License: if ($request->filled('license_key') && $downloadToken->license->license_key !== $request->input('license_key')) { Log::warning('AppManager: Download token license mismatch.', [ 'token_id' => $downloadToken->id, 'expected_key' => $downloadToken->license->license_key, 'provided_key' => $request->input('license_key'), 'ip' => $request->ip() ]); return response()->json(['status' => 'error', 'message' => 'Token not valid for this license.'], 403); } // You might also check if the token was generated for the correct product_slug via $downloadToken->license->managed_script_id // --- End Robust Token Validation --- $script = ManagedScript::where('slug', $request->input('product_slug'))->firstOrFail(); $downloadableFile = DownloadableFile::where('managed_script_id', $script->id) ->where(function($query) use ($request) { // Allow lookup by target_path primarily, or file_name as a fallback $query->where('target_path', $request->input('file_identifier')) ->orWhere('file_name', $request->input('file_identifier')); }) ->where('version', $request->input('version')) ->first(); if (!$downloadableFile) { Log::info('AppManager: Downloadable file not found or version mismatch.', $request->all()); return response()->json(['status' => 'error', 'message' => 'File not found or version mismatch.'], 404); } $filePathOnDisk = $downloadableFile->file_path; // This path is relative to the disk's root if (!Storage::disk($this->storageDisk)->exists($filePathOnDisk)) { Log::error("AppManager: File source not found on server for download.", ['path' => $filePathOnDisk, 'request' => $request->all()]); return response()->json(['status' => 'error', 'message' => 'File source not found on server.'], 500); } // Optional: Verify hash before sending $currentHash = hash_file('sha256', Storage::disk($this->storageDisk)->path($filePathOnDisk)); if ($downloadableFile->file_hash && $currentHash !== $downloadableFile->file_hash) { Log::critical("AppManager: CRITICAL - File hash mismatch for download.", [ 'file_record_id' => $downloadableFile->id, 'path' => $filePathOnDisk, 'stored_hash' => $downloadableFile->file_hash, 'current_hash' => $currentHash ]); return response()->json(['status' => 'error', 'message' => 'File integrity check failed on server.'], 500); } $downloadToken->incrementUsage(); // Optionally, delete the token if it has reached max uses and you want it to be strictly single-session for a set of downloads // if ($downloadToken->isMaxUsesReached()) { $downloadToken->delete(); } return Storage::disk($this->storageDisk)->download($filePathOnDisk, $downloadableFile->file_name, [ 'Content-Type' => Storage::disk($this->storageDisk)->mimeType($filePathOnDisk) ?? 'application/octet-stream', 'X-File-Hash' => $downloadableFile->file_hash // Send hash for client-side verification ]); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Http\Controllers\AppManagerController.php --- registerTranslations(); $this->registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations')); // Keep migrations // The module's own RouteServiceProvider (registered in the register() method) // will handle loading its web.php routes. // The main application's RouteServiceProvider handles loading admin.php and api.php for all modules. $this->publishAssets(); } /** * Register the service provider. * * @return void */ public function register(): void { $this->app->register(RouteServiceProvider::class); } /** * Register config. * * @return void */ protected function registerConfig(): void { $this->publishes([ module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'), ], 'config'); $this->mergeConfigFrom( module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower ); } /** * Register views. * * @return void */ public function registerViews(): void { $viewPath = resource_path('views/modules/' . $this->moduleNameLower); $sourcePath = module_path($this->moduleName, 'resources/views'); $this->publishes([ $sourcePath => $viewPath ], ['views', $this->moduleNameLower . '-module-views']); $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower); } /** * Register translations. * * @return void */ public function registerTranslations(): void { // Similar to LandingPageServiceProvider, adapt if needed } /** * Get the services provided by the provider. * * @return array */ public function provides(): array { return []; } private function getPublishableViewPaths(): array { $paths = []; foreach (\Config::get('view.paths') as $path) { if (is_dir($path . '/modules/' . $this->moduleNameLower)) { $paths[] = $path . '/modules/' . $this->moduleNameLower; } } return $paths; } protected function publishAssets(): void { $this->publishes([ module_path($this->moduleName, 'public') => public_path('modules/'. $this->moduleNameLower), ], ['public', $this->moduleNameLower . '-module-assets']); } } --- File: D:\projects\digitalvocano\Modules\AppManager\Providers\EventServiceProvider.php --- > */ protected $listen = []; /** * Indicates if events should be discovered. * * @var bool */ protected static $shouldDiscoverEvents = true; /** * Configure the proper event listeners for email verification. */ protected function configureEmailVerification(): void {} } --- File: D:\projects\digitalvocano\Modules\AppManager\Providers\RouteServiceProvider.php --- mapWebRoutes(); } /** * Define the "web" routes for the application. * * These routes all receive session state, CSRF protection, etc. */ protected function mapWebRoutes(): void { Route::middleware('web')->group(module_path($this->name, '/routes/web.php')); } /** * Define the "api" routes for the application. * * These routes are typically stateless. */ protected function mapApiRoutes(): void { Route::middleware('api') // ->namespace($this->moduleNamespace . '\Api') // If your API controllers are in an 'Api' subfolder ->group(module_path($this->name, '/routes/api.php')); } } --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\activation_logs\index.blade.php --- @extends('layouts.admin') @section('title', 'Activation Logs') @section('header_title', 'Script Activation Logs') @section('content')
| Script | License Key | Domain | IP Address | Status | Message | Date |
|---|---|---|---|---|---|---|
| {{ $log->license->managedScript->name ?? 'N/A' }} | {{ $log->license->license_key ?? 'N/A' }} | {{ $log->activated_domain ?: '-' }} | {{ $log->ip_address ?: '-' }} | {{ ucfirst(str_replace('_', ' ', $log->status)) }} | {{ Str::limit($log->message, 50) ?: '-' }} | {{ $log->activated_at->format('M d, Y H:i') }} |
| No activation logs found. | ||||||
| Name | Slug | Version | Status | Boilerplate Core | Actions |
|---|---|---|---|---|---|
| {{ $script->name }} | {{ $script->slug }} | {{ $script->current_version }} | {{ ucfirst($script->status) }} | {{ $script->is_boilerplate_core ? 'Yes' : 'No' }} | |
| No managed scripts found. | |||||
Slug: {{ $script->slug }}
Status: {{ ucfirst($script->status) }}
@if($script->description){{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderrorCheck if this script represents the main boilerplate application itself.
@error('is_boilerplate_core'){{ $message }}
@enderror| License Key | Script | Customer | Type | Status | Activations | Expires At | Actions |
|---|---|---|---|---|---|---|---|
| {{ Str::limit($license->license_key, 25) }} | {{ $license->managedScript->name ?? 'N/A' }} |
{{ $license->customer_name ?: ($license->customer_email ?: 'N/A') }}
@if($license->customer_name && $license->customer_email) {{ $license->customer_email }}@endif |
{{ ucfirst($license->type) }} | {{ ucfirst($license->status) }} | {{ $license->current_activations }} / {{ $license->activation_limit == 0 ? 'Unlimited' : $license->activation_limit }} | {{ $license->expires_at ? $license->expires_at->format('M d, Y') : 'Never' }} | |
| No licenses found. | |||||||
{{ $license->license_key }}
{{ $license->managedScript->name ?? 'N/A' }}
{{ ucfirst($license->type) }}
{{ $license->customer_name ?: ($license->customer_email ?: 'N/A') }}
@if($license->customer_name && $license->customer_email){{ $license->customer_email }}
@endif{{ $license->purchase_code ?: 'N/A' }}
{{ $license->metadata['envato_license_type'] }}
{{ ucfirst($license->status) }}
{{ $license->current_activations }} / {{ $license->activation_limit == 0 ? 'Unlimited' : $license->activation_limit }}
{{ $license->expires_at ? $license->expires_at->format('M d, Y H:i') : 'Never' }}
{{ $license->supported_until ? $license->supported_until->format('M d, Y') : 'N/A' }}
{{ json_encode($license->metadata, JSON_PRETTY_PRINT) }}
No domains currently activated with this license.
@endif| Domain | IP Address | Status | Message | Date |
|---|---|---|---|---|
| {{ $log->activated_domain ?: '-' }} | {{ $log->ip_address ?: '-' }} | {{ ucfirst(str_replace('_', ' ', $log->status)) }} | {{ Str::limit($log->message, 50) ?: '-' }} | {{ $log->activated_at->format('M d, Y H:i') }} |
| No activation logs found for this license. | ||||
{{ $message }}
@enderror{{ $message }}
@enderrorIf checked, the license key field above will be ignored and a unique key generated.
{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderrorSet to 0 for unlimited activations.
@error('activation_limit'){{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderrorTotal Managed Scripts
{{ $totalScriptsCount ?? ($scripts->total() ?? 'N/A') }}
Total Licenses Issued
{{ $totalLicensesCount ?? 'N/A' }}
Successful Activations (Last 7 Days)
{{ $recentSuccessfulActivationsCount ?? 'N/A' }}
View All
Activation Logs
| Name | Slug | Version | Status | Boilerplate Core | Total Licenses | Actions |
|---|---|---|---|---|---|---|
|
{{ $script->name }}
@if($script->description) {{ Str::limit($script->description, 70) }} @else No description. @endif |
{{ $script->slug }} | {{ $script->current_version }} | {{ ucfirst($script->status) }} | {{ $script->is_boilerplate_core ? 'Yes' : 'No' }} | {{ $script->licenses_count ?? $script->licenses()->count() }} | |
| No managed scripts found. Add your first script. | ||||||
Slug: {{ $script->slug }}
No description provided.
' !!}No changelog provided.
@endifNo licenses associated with this script yet.
Create First License @endif{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderror{{ $message }}
@enderrorCheck if this script represents the main boilerplate application itself.
@error('is_boilerplate_core'){{ $message }}
@enderror{{ $message }}
@enderrorModule: {!! config('appmanager.name') !!}