--- 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')

Activation Logs

@include('admin.partials.alerts')
Clear
@forelse ($logs as $log) @empty @endforelse
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.
{{ $logs->links() }}
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\create.blade.php --- @extends('layouts.admin') @section('title', 'Create New Managed Script') @section('header_title', 'Create New Managed Script') @section('content')
@include('admin.partials.alerts')
@csrf @include('appmanager::admin.managed_scripts._form', ['script' => null, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\edit.blade.php --- @extends('layouts.admin') @section('title', 'Edit Managed Script: ' . $script->name) @section('header_title', 'Edit Managed Script: ' . $script->name) @section('content')
@include('admin.partials.alerts')
@csrf @method('PUT') @include('appmanager::admin.managed_scripts._form', ['script' => $script, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\index.blade.php --- @extends('layouts.admin') @section('title', 'Manage Scripts') @section('header_title', 'Manage Scripts & Applications') @section('content')

Scripts / Modules

Add New Script
@include('admin.partials.alerts')
@forelse ($scripts as $script) @empty @endforelse
Name Slug Version Status Boilerplate Core Actions
{{ $script->name }} {{ $script->slug }} {{ $script->current_version }} {{ ucfirst($script->status) }} {{ $script->is_boilerplate_core ? 'Yes' : 'No' }}
Files
@csrf @method('DELETE')
No managed scripts found.
{{ $scripts->links() }}
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\show.blade.php --- @extends('layouts.admin') @section('title', 'Script Details: ' . $script->name) @section('header_title', 'Script Details: ' . $script->name) @section('content')
← Back to Scripts

{{ $script->name }} (v{{ $script->current_version }})

Slug: {{ $script->slug }}

Status: {{ ucfirst($script->status) }}

@if($script->description)

Description:

{!! nl2br(e($script->description)) !!}
@endif @if($script->changelog)

Changelog:

{!! nl2br(e($script->changelog)) !!} {{-- Consider Markdown parsing here --}}
@endif
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\downloadable_files\_form.blade.php ---
@error('name')

{{ $message }}

@enderror
@error('slug')

{{ $message }}

@enderror
@error('current_version')

{{ $message }}

@enderror
@error('status')

{{ $message }}

@enderror
@error('description')

{{ $message }}

@enderror
@error('changelog')

{{ $message }}

@enderror

Check if this script represents the main boilerplate application itself.

@error('is_boilerplate_core')

{{ $message }}

@enderror
Cancel
--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\create.blade.php --- @extends('layouts.admin') @section('title', 'Create New License') @section('header_title', 'Create New License') @section('content')
@include('admin.partials.alerts')
@csrf @include('appmanager::admin.licenses._form', ['license' => null, 'scripts' => $scripts, 'types' => $types, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\edit.blade.php --- @extends('layouts.admin') @section('title', 'Edit License: ' . $license->license_key) @section('header_title', 'Edit License: ' . $license->license_key) @section('content')
@include('admin.partials.alerts')
@csrf @method('PUT') @include('appmanager::admin.licenses._form', ['license' => $license, 'scripts' => $scripts, 'types' => $types, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\index.blade.php --- @extends('layouts.admin') @section('title', 'Manage Licenses') @section('header_title', 'Manage Licenses') @section('content')

Licenses

Add New License
@include('admin.partials.alerts')
Clear
@forelse ($licenses as $license) @empty @endforelse
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' }}
@csrf @method('DELETE')
No licenses found.
{{ $licenses->links() }}
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\show.blade.php --- @extends('layouts.admin') @section('title', 'License Details: ' . $license->license_key) @section('header_title', 'License Details: ' . Str::limit($license->license_key, 40)) @section('content')
← Back to Licenses

License Key:

{{ $license->license_key }}

Script:

{{ $license->managedScript->name ?? 'N/A' }}

Type:

{{ ucfirst($license->type) }}

Customer:

{{ $license->customer_name ?: ($license->customer_email ?: 'N/A') }}

@if($license->customer_name && $license->customer_email)

{{ $license->customer_email }}

@endif
@if($license->type === 'codecanyon')

Purchase Code:

{{ $license->purchase_code ?: 'N/A' }}

@if(isset($license->metadata['envato_license_type']))

Envato License:

{{ $license->metadata['envato_license_type'] }}

@endif @endif

Status:

{{ ucfirst($license->status) }}

Activations:

{{ $license->current_activations }} / {{ $license->activation_limit == 0 ? 'Unlimited' : $license->activation_limit }}

Expires At:

{{ $license->expires_at ? $license->expires_at->format('M d, Y H:i') : 'Never' }}

Support Until:

{{ $license->supported_until ? $license->supported_until->format('M d, Y') : 'N/A' }}

@if($license->metadata)

Metadata:

{{ json_encode($license->metadata, JSON_PRETTY_PRINT) }}
@endif
{{-- Adjusted margin and removed unnecessary col-span --}}

Activated Domains:

@php $activeDomains = $license->getActiveDomains(); @endphp @if($activeDomains->isNotEmpty())
    @foreach($activeDomains as $domain)
  • {{ $domain }}
  • @endforeach
@else

No domains currently activated with this license.

@endif

Activation Logs

@forelse ($license->activationLogs as $log) @empty @endforelse
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.
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\licenses\_form.blade.php ---
@error('managed_script_id')

{{ $message }}

@enderror
@error('license_key')

{{ $message }}

@enderror

If checked, the license key field above will be ignored and a unique key generated.

@error('type')

{{ $message }}

@enderror
@error('purchase_code')

{{ $message }}

@enderror
@error('customer_name')

{{ $message }}

@enderror
@error('customer_email')

{{ $message }}

@enderror

Set to 0 for unlimited activations.

@error('activation_limit')

{{ $message }}

@enderror
@error('status')

{{ $message }}

@enderror
@error('expires_at')

{{ $message }}

@enderror
{{-- Ensure this checkbox is on a new line or positioned logically --}}
@error('supported_until')

{{ $message }}

@enderror
@error('metadata')

{{ $message }}

@enderror
@error('notes')

{{ $message }}

@enderror
Cancel
@pushOnce('scripts') @endPushOnce --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\create.blade.php --- @extends('layouts.admin') {{-- Or your AppManager specific admin layout if different --}} @section('title', 'Create New Script/App') @section('header_title', 'Create New Script/Application') @section('content')

Script/Application Details

@csrf @include('appmanager::admin.managed_scripts._form', ['script' => null, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\edit.blade.php --- @extends('layouts.admin') {{-- Or your AppManager specific admin layout if different --}} @section('title', 'Edit Script: ' . $script->name) @section('header_title', 'Edit Script/Application: ' . $script->name) @section('content')
← Back to Script Details

Edit {{ $script->name }}

@csrf @method('PUT') {{-- Important for update operations --}} @include('appmanager::admin.managed_scripts._form', ['script' => $script, 'statuses' => $statuses])
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\index.blade.php --- @extends('layouts.admin') @section('title', 'App Manager Dashboard') @section('header_title', 'App Manager Dashboard') @section('content')

Welcome to App Manager

Add New Script/App
{{-- Removed include for non-existent partial --}}

Total 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

Find a Script/Application

@if(request('search')) Clear @endif
@forelse ($scripts as $script) @empty @endforelse
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() }}
View Files
@csrf @method('DELETE')
No managed scripts found. Add your first script.
{{ $scripts->links() }}
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\show.blade.php --- @extends('layouts.admin') {{-- Or your AppManager specific admin layout --}} @section('title', 'Script Details: ' . $script->name) @section('header_title', 'Script Details: ' . $script->name) @section('content')

{{ $script->name }}

Slug: {{ $script->slug }}

Details

Current Version
{{ $script->current_version ?: 'N/A' }}
Status
{{ ucfirst($script->status) }}
Boilerplate Core
{{ $script->is_boilerplate_core ? 'Yes' : 'No' }}
Envato Item ID
{{ $script->envato_item_id ?: 'N/A' }}
Created At
{{ $script->created_at->format('M d, Y H:i A') }}
Last Updated
{{ $script->updated_at->format('M d, Y H:i A') }}

Description

{!! $script->description ? nl2br(e($script->description)) : '

No description provided.

' !!}

Changelog

@if($script->changelog) {!! \Illuminate\Support\Str::markdown(e($script->changelog)) !!} @else

No changelog provided.

@endif

Associated Licenses

@if($script->licenses()->count() > 0) View {{ $script->licenses()->count() }} License(s) @else

No licenses associated with this script yet.

Create First License @endif
@endsection --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\admin\managed_scripts\_form.blade.php ---
@error('name')

{{ $message }}

@enderror
@error('slug')

{{ $message }}

@enderror
@error('current_version')

{{ $message }}

@enderror
@error('status')

{{ $message }}

@enderror
@error('description')

{{ $message }}

@enderror
@error('changelog')

{{ $message }}

@enderror

Check if this script represents the main boilerplate application itself.

@error('is_boilerplate_core')

{{ $message }}

@enderror
@error('envato_item_id')

{{ $message }}

@enderror
Cancel
--- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\activation-logs.blade.php --- --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\licenses.blade.php --- --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\managed-scripts.blade.php --- --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\icons\recent-activations.blade.php --- --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\components\layouts\master.blade.php --- AppManager Module - {{ config('app.name', 'Laravel') }} {{-- Vite CSS --}} {{-- {{ module_vite('build-appmanager', 'resources/assets/sass/app.scss') }} --}} {{ $slot }} {{-- Vite JS --}} {{-- {{ module_vite('build-appmanager', 'resources/assets/js/app.js') }} --}} --- File: D:\projects\digitalvocano\Modules\AppManager\resources\views\index.blade.php ---

Hello World

Module: {!! config('appmanager.name') !!}

--- File: D:\projects\digitalvocano\Modules\AppManager\routes\admin.php --- group(function () { Route::resource('scripts', ManagedScriptController::class); Route::resource('scripts.files', DownloadableFileController::class)->except(['show']); // files nested under scripts Route::resource('licenses', LicenseController::class); Route::get('activation-logs', [ActivationLogController::class, 'index'])->name('activationlogs.index'); // Add other AppManager admin routes here }); // Note: Ensure your controller namespaces are correct, e.g., // Modules\AppManager\Http\Controllers\Admin\ManagedScriptController --- File: D:\projects\digitalvocano\Modules\AppManager\routes\api.php --- name('activate'); Route::post('validate-license', [ActivationController::class, 'validateLicense'])->name('validate'); // For periodic checks Route::post('download-file', [FileDownloadController::class, 'download'])->name('download.file'); Route::post('validate-activation', [ActivationValidationController::class, 'validateActivation'])->name('validate.activation'); }); --- File: D:\projects\digitalvocano\Modules\AppManager\routes\web.php --- group(function () { Route::resource('appmanagers', AppManagerController::class)->names('appmanager'); }); --- File: D:\projects\digitalvocano\Modules\AppManager\tests\Feature\AppManagerApiTest.php --- script = ManagedScript::factory()->create([ 'slug' => 'test-script', 'current_version' => '1.0.0', 'envato_item_id' => '12345678' // Example Envato ID ]); $this->license = License::factory()->create([ 'managed_script_id' => $this->script->id, 'license_key' => 'VALID-LICENSE-KEY', 'status' => 'active', 'activation_limit' => 1, 'current_activations' => 0, ]); } /** * @test */ public function can_activate_with_valid_license_key(): void { $payload = [ 'product_slug' => $this->script->slug, 'domain' => 'https://client.example.com', 'license_key' => $this->license->license_key, ]; $response = $this->postJson(route('api.appmanager.activate'), $payload); $response->assertStatus(200) ->assertJson([ 'status' => 'success', 'message' => 'Application activated successfully.', ]) ->assertJsonStructure([ 'status', 'message', 'activation_token', 'product_info' => ['name', 'version'], 'license_info' => ['status', 'expires_at', 'supported_until', 'type'], ]); $this->assertDatabaseHas('am_licenses', [ 'id' => $this->license->id, 'current_activations' => 1, ]); $this->assertDatabaseHas('am_activation_logs', [ 'license_id' => $this->license->id, 'activated_domain' => 'https://client.example.com', 'status' => 'success', ]); $this->assertDatabaseCount('am_download_tokens', 1); } /** * @test */ public function can_activate_with_valid_purchase_code_and_mocked_envato_success(): void { Http::fake([ 'api.envato.com/*' => Http::sequence() ->push([ 'item' => ['id' => $this->script->envato_item_id], 'buyer' => 'testbuyer@example.com', 'supported_until' => now()->addYear()->toIso8601String(), 'license' => 'Regular License' ], 200) ]); $payload = [ 'product_slug' => $this->script->slug, 'domain' => 'https://client-envato.example.com', 'purchase_code' => 'VALID-PURCHASE-CODE-FORMAT-12345', // Format doesn't matter for mock ]; $response = $this->postJson(route('api.appmanager.activate'), $payload); $response->assertStatus(200) ->assertJsonPath('status', 'success'); $this->assertDatabaseHas('am_licenses', [ 'managed_script_id' => $this->script->id, 'purchase_code' => 'VALID-PURCHASE-CODE-FORMAT-12345', 'type' => 'codecanyon', 'current_activations' => 1, ]); $this->assertDatabaseHas('am_activation_logs', [ 'purchase_code_attempt' => 'VALID-PURCHASE-CODE-FORMAT-12345', 'activated_domain' => 'https://client-envato.example.com', 'status' => 'success', ]); } /** * @test */ public function activate_fails_if_license_not_found(): void { $payload = [ 'product_slug' => $this->script->slug, 'domain' => 'https://client.example.com', 'license_key' => 'NON-EXISTENT-KEY', ]; $response = $this->postJson(route('api.appmanager.activate'), $payload); $response->assertStatus(403) ->assertJson(['status' => 'error', 'message' => 'License key or purchase code not found or invalid for this product.']); } /** * @test */ public function activate_fails_if_activation_limit_reached(): void { $this->license->update(['current_activations' => 1, 'activation_limit' => 1]); $payload = [ 'product_slug' => $this->script->slug, 'domain' => 'https://another-client.example.com', 'license_key' => $this->license->license_key, ]; $response = $this->postJson(route('api.appmanager.activate'), $payload); $response->assertStatus(403) ->assertJson(['status' => 'error', 'message' => 'Activation limit reached for this license.']); } /** * @test */ public function activate_fails_with_invalid_payload(): void { $response = $this->postJson(route('api.appmanager.activate'), []); $response->assertStatus(400) ->assertJsonValidationErrors(['product_slug', 'domain']); } /** * @test */ public function can_validate_license_successfully(): void { // Simulate a successful activation first ActivationLog::factory()->create([ 'license_id' => $this->license->id, 'managed_script_id' => $this->script->id, 'activated_domain' => 'https://client.example.com', 'status' => 'success', ]); $payload = [ 'license_key' => $this->license->license_key, 'product_slug' => $this->script->slug, 'domain' => 'https://client.example.com', ]; $response = $this->postJson(route('api.appmanager.validate'), $payload); $response->assertStatus(200) ->assertJson(['status' => 'valid', 'message' => 'License is active.']); } /** * @test */ public function validate_license_fails_if_not_active_for_domain(): void { $payload = [ 'license_key' => $this->license->license_key, 'product_slug' => $this->script->slug, 'domain' => 'https://unknown-domain.example.com', // Not activated for this domain ]; $response = $this->postJson(route('api.appmanager.validate'), $payload); $response->assertStatus(403) ->assertJson(['status' => 'invalid', 'message' => 'License not activated for this domain.']); } /** * @test */ public function can_download_file_with_valid_token(): void { Storage::fake(config('appmanager.storage_disk')); // Use fake storage $downloadableFile = DownloadableFile::factory()->create([ 'managed_script_id' => $this->script->id, 'file_name' => 'test_file.zip', 'file_path' => 'app_manager_files/' . $this->script->slug . '/1.0.0/test_file.zip', 'version' => '1.0.0', 'target_path' => 'app/test_file.zip', ]); // Create a dummy file in fake storage Storage::disk(config('appmanager.storage_disk'))->put($downloadableFile->file_path, 'dummy content'); $downloadToken = DownloadToken::factory()->create([ 'license_id' => $this->license->id, 'expires_at' => now()->addHour(), 'max_uses' => 1, ]); $payload = [ 'activation_token' => $downloadToken->token, 'product_slug' => $this->script->slug, 'file_identifier' => $downloadableFile->target_path, // Assuming identifier is target_path 'version' => $downloadableFile->version, ]; $response = $this->postJson(route('api.appmanager.download.file'), $payload); $response->assertStatus(200) ->assertHeader('Content-Disposition', 'attachment; filename="' . $downloadableFile->file_name . '"'); $this->assertEquals('dummy content', $response->streamedContent()); $this->assertDatabaseHas('am_download_tokens', [ 'id' => $downloadToken->id, 'uses' => 1, ]); } /** * @test */ public function download_file_fails_with_invalid_token(): void { $payload = [ 'activation_token' => 'INVALID-TOKEN', 'product_slug' => $this->script->slug, 'file_identifier' => 'app/some_file.zip', 'version' => '1.0.0', ]; $response = $this->postJson(route('api.appmanager.download.file'), $payload); $response->assertStatus(403); // Or 404 depending on controller logic } /** * @test */ public function can_validate_activation_token_successfully(): void { $downloadToken = DownloadToken::factory()->create([ 'license_id' => $this->license->id, 'expires_at' => now()->addHour(), ]); // Simulate an activation for the domain associated with this license ActivationLog::factory()->create([ 'license_id' => $this->license->id, 'managed_script_id' => $this->script->id, 'activated_domain' => 'https://client.example.com', 'status' => 'success', ]); $payload = [ 'activation_token' => $downloadToken->token, 'domain' => 'https://client.example.com', 'product_slug' => $this->script->slug, ]; $response = $this->postJson(route('api.appmanager.validate.activation'), $payload); $response->assertStatus(200) ->assertJson(['status' => 'success', 'is_valid' => true]); } /** * @test */ public function validate_activation_token_fails_if_domain_mismatch(): void { $downloadToken = DownloadToken::factory()->create([ 'license_id' => $this->license->id, 'expires_at' => now()->addHour(), ]); // Simulate an activation for a DIFFERENT domain ActivationLog::factory()->create([ 'license_id' => $this->license->id, 'managed_script_id' => $this->script->id, 'activated_domain' => 'https://original-activation.example.com', 'status' => 'success', ]); $payload = [ 'activation_token' => $downloadToken->token, 'domain' => 'https://different-client.example.com', // Mismatched domain 'product_slug' => $this->script->slug, ]; $response = $this->postJson(route('api.appmanager.validate.activation'), $payload); $response->assertStatus(403) // Or appropriate error code ->assertJson(['status' => 'error', 'is_valid' => false]); } } --- File: D:\projects\digitalvocano\Modules\AppManager\tests\Unit\AppManagerApiTest.php --- assertTrue(true); } }