generateSecureSecret(); // Create in Laravel database (encrypted) $credential = PBXApiCredential::create([ 'name' => $options['name'] ?? 'API Credential - ' . now()->format('Y-m-d H:i:s'), 'api_user' => $username, 'api_secret' => $secret, // Will be encrypted by model accessor 'api_url' => $options['api_url'] ?? 'http://127.0.0.1', 'is_active' => true, 'metadata' => $options['metadata'] ?? [], ]); // Note: Asterisk database activation is now handled by GenerateCredentialsCommand // to ensure direct insertion with proper error handling \Log::info('PBX API credential generated', [ 'credential_id' => $credential->id, 'username' => $username, ]); return $credential; } /** * Generate a secure random secret * * @return string */ protected function generateSecureSecret(): string { return Str::random(64); } /** * Activate credentials in FreePBX by inserting into asterisk database * * This method is now primarily used by the GenerateCredentialsCommand * which handles the insertion directly with proper error handling * Uses the same method as UserService: DB::table('asterisk.{table}') * * @param string $username * @param string $secret * @return bool */ protected function activateInFreePBX(string $username, string $secret): bool { try { // Check if already exists $existing = DB::table('asterisk.rest_users') ->where('user', $username) ->first(); if ($existing) { // Update existing DB::table('asterisk.rest_users') ->where('user', $username) ->update([ 'password' => $secret, 'updated_at' => now(), ]); } else { // Insert new DB::table('asterisk.rest_users') ->insert([ 'user' => $username, 'password' => $secret, 'created_at' => now(), 'updated_at' => now(), ]); } return true; } catch (\Exception $e) { // Log warning but don't fail the entire operation // Credentials are already stored encrypted in Laravel database \Log::warning('Could not activate credentials in FreePBX asterisk database', [ 'username' => $username, 'error' => $e->getMessage(), 'note' => 'Credentials are stored in Laravel database. GenerateCredentialsCommand should handle asterisk DB insertion.', ]); // Return true anyway - Laravel DB has the credentials return true; } } /** * Revoke/deactivate credentials * * @param int $credentialId * @return bool */ public function revoke(int $credentialId): bool { $credential = PBXApiCredential::find($credentialId); if (!$credential) { return false; } // Deactivate in Laravel database $credential->update(['is_active' => false]); // Try to remove from asterisk database (optional, logs warning if fails) try { DB::table('asterisk.rest_users') ->where('user', $credential->api_user) ->delete(); } catch (\Exception $e) { \Log::warning('Could not remove PBX credential from FreePBX database', [ 'username' => $credential->api_user, 'error' => $e->getMessage(), 'note' => 'Credential deactivated in Laravel. Please manually remove from FreePBX rest_users if needed.', ]); } \Log::info('PBX API credential revoked', [ 'credential_id' => $credentialId, 'username' => $credential->api_user, ]); return true; } /** * Get active credentials * * @return PBXApiCredential|null */ public function getActiveCredential(): ?PBXApiCredential { return PBXApiCredential::getPrimary(); } /** * Rotate credentials (generate new, revoke old) * * @return PBXApiCredential */ public function rotate(): PBXApiCredential { // Generate new credential $newCredential = $this->generate(); // Deactivate old credentials (but don't delete) PBXApiCredential::where('id', '!=', $newCredential->id) ->where('is_active', true) ->update(['is_active' => false]); \Log::info('PBX API credentials rotated', [ 'new_credential_id' => $newCredential->id, ]); return $newCredential; } /** * Ensure primary credential exists * * @return PBXApiCredential */ public function ensurePrimaryCredential(): PBXApiCredential { $credential = $this->getActiveCredential(); if (!$credential || !$credential->isValid()) { $credential = $this->generate(); } return $credential; } /** * Setup FreePBX API application with scopes/permissions for the credentials * * Valid FreePBX scope keys: * - "rest" → grants access to all REST API endpoints * - "gql" → grants access to all GraphQL API endpoints * - "rest:{module}" → all access to a specific REST module * - "rest:{module}:read" → read access to a REST module * - "rest:{module}:write" → write access to a REST module * * @param PBXApiCredential $credential * @param array $scopes FreePBX scope keys (defaults to full REST+GraphQL access) * @return bool */ public function setupApiApplication(PBXApiCredential $credential, array $scopes = []): bool { try { // Default to full REST and GraphQL access — necessary for extension management $scopes = $scopes ?: ['rest', 'gql']; // Check if application already exists for this user $existing = DB::table('asterisk.api_applications') ->where('client_id', $credential->api_user) ->first(); if ($existing) { // Update existing application with scopes DB::table('asterisk.api_applications') ->where('id', $existing->id) ->update([ 'allowed_scopes' => json_encode($scopes), ]); \Log::info('Updated API application scopes', [ 'app_id' => $existing->id, 'client_id' => $credential->api_user, 'scopes' => $scopes, ]); return true; } // Create new API application // FreePBX stores client_secret as sha256(raw_secret) — see Applications.php::add() $rawSecret = $credential->getDecryptedSecret(); $hashedSecret = hash('sha256', $rawSecret); DB::table('asterisk.api_applications') ->insert([ 'name' => $credential->name, 'description' => 'Laravel CCS System - ' . $credential->name, 'grant_type' => 'client_credentials', 'client_id' => $credential->api_user, 'client_secret' => $hashedSecret, 'algo' => 'sha256', // null = no restriction → FreePBX defaults to full gql + rest access 'allowed_scopes' => null, ]); \Log::info('Created API application', [ 'client_id' => $credential->api_user, 'scopes' => $scopes, ]); return true; } catch (\Exception $e) { \Log::warning('Failed to setup API application scopes', [ 'credential_id' => $credential->id, 'error' => $e->getMessage(), ]); return false; } } }