598 lines
16 KiB
PHP
598 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\RestaurantDelivery\Services\Firebase;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Kreait\Firebase\Auth;
|
|
use Kreait\Firebase\Auth\CreateRequest;
|
|
use Kreait\Firebase\Exception\Auth\FailedToVerifyToken;
|
|
use Kreait\Firebase\Exception\FirebaseException;
|
|
use Kreait\Firebase\Factory;
|
|
|
|
/**
|
|
* Firebase Authentication Service
|
|
*
|
|
* Handles user authentication, custom tokens, and claims for role-based access.
|
|
*/
|
|
class FirebaseAuthService
|
|
{
|
|
protected ?Auth $auth = null;
|
|
|
|
protected Factory $factory;
|
|
|
|
protected array $config;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->config = config('restaurant-delivery.firebase');
|
|
$this->initializeFirebase();
|
|
}
|
|
|
|
protected function initializeFirebase(): void
|
|
{
|
|
if (! $this->config['enabled']) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$this->factory = (new Factory)
|
|
->withServiceAccount($this->config['credentials_path']);
|
|
} catch (\Exception $e) {
|
|
Log::error('Firebase Auth initialization failed', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function getAuth(): ?Auth
|
|
{
|
|
if (! $this->auth && isset($this->factory)) {
|
|
$this->auth = $this->factory->createAuth();
|
|
}
|
|
|
|
return $this->auth;
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Custom Token Generation
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Create a custom token for a user with custom claims
|
|
*
|
|
* @param string $uid User ID
|
|
* @param array $claims Custom claims (role, permissions, etc.)
|
|
* @return string|null JWT token
|
|
*/
|
|
public function createCustomToken(string $uid, array $claims = []): ?string
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
$customToken = $auth->createCustomToken($uid, $claims);
|
|
|
|
return $customToken->toString();
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to create custom token', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a custom token for a rider with appropriate claims
|
|
*/
|
|
public function createRiderToken(string $riderId, array $additionalClaims = []): ?string
|
|
{
|
|
$claims = array_merge([
|
|
'role' => 'rider',
|
|
'rider_id' => $riderId,
|
|
'can_update_location' => true,
|
|
'can_accept_deliveries' => true,
|
|
], $additionalClaims);
|
|
|
|
return $this->createCustomToken($riderId, $claims);
|
|
}
|
|
|
|
/**
|
|
* Create a custom token for a restaurant with appropriate claims
|
|
*/
|
|
public function createRestaurantToken(string $restaurantId, array $additionalClaims = []): ?string
|
|
{
|
|
$claims = array_merge([
|
|
'role' => 'restaurant',
|
|
'restaurant_id' => $restaurantId,
|
|
'restaurant' => true,
|
|
'can_create_deliveries' => true,
|
|
'can_assign_riders' => true,
|
|
], $additionalClaims);
|
|
|
|
return $this->createCustomToken($restaurantId, $claims);
|
|
}
|
|
|
|
/**
|
|
* Create a custom token for a customer with appropriate claims
|
|
*/
|
|
public function createCustomerToken(string $customerId, array $additionalClaims = []): ?string
|
|
{
|
|
$claims = array_merge([
|
|
'role' => 'customer',
|
|
'customer_id' => $customerId,
|
|
'can_track_deliveries' => true,
|
|
'can_rate_riders' => true,
|
|
], $additionalClaims);
|
|
|
|
return $this->createCustomToken($customerId, $claims);
|
|
}
|
|
|
|
/**
|
|
* Create a custom token for an admin with full permissions
|
|
*/
|
|
public function createAdminToken(string $adminId, array $additionalClaims = []): ?string
|
|
{
|
|
$claims = array_merge([
|
|
'role' => 'admin',
|
|
'admin' => true,
|
|
'admin_id' => $adminId,
|
|
'full_access' => true,
|
|
], $additionalClaims);
|
|
|
|
return $this->createCustomToken($adminId, $claims);
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Token Verification
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Verify an ID token and return the decoded claims
|
|
*/
|
|
public function verifyIdToken(string $idToken): ?array
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
$verifiedToken = $auth->verifyIdToken($idToken);
|
|
|
|
return [
|
|
'uid' => $verifiedToken->claims()->get('sub'),
|
|
'email' => $verifiedToken->claims()->get('email'),
|
|
'email_verified' => $verifiedToken->claims()->get('email_verified'),
|
|
'claims' => $verifiedToken->claims()->all(),
|
|
];
|
|
} catch (FailedToVerifyToken $e) {
|
|
Log::warning('Failed to verify ID token', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Firebase error during token verification', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if token has specific role
|
|
*/
|
|
public function hasRole(string $idToken, string $role): bool
|
|
{
|
|
$decoded = $this->verifyIdToken($idToken);
|
|
|
|
if (! $decoded) {
|
|
return false;
|
|
}
|
|
|
|
return ($decoded['claims']['role'] ?? null) === $role;
|
|
}
|
|
|
|
/**
|
|
* Check if token has admin access
|
|
*/
|
|
public function isAdmin(string $idToken): bool
|
|
{
|
|
$decoded = $this->verifyIdToken($idToken);
|
|
|
|
if (! $decoded) {
|
|
return false;
|
|
}
|
|
|
|
return ($decoded['claims']['admin'] ?? false) === true;
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Custom Claims Management
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Set custom claims for a user
|
|
*/
|
|
public function setCustomClaims(string $uid, array $claims): bool
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return false;
|
|
}
|
|
|
|
$auth->setCustomUserClaims($uid, $claims);
|
|
|
|
// Invalidate cached claims
|
|
Cache::forget("firebase_claims_{$uid}");
|
|
|
|
return true;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to set custom claims', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's custom claims
|
|
*/
|
|
public function getCustomClaims(string $uid): array
|
|
{
|
|
// Try cache first
|
|
$cached = Cache::get("firebase_claims_{$uid}");
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return [];
|
|
}
|
|
|
|
$user = $auth->getUser($uid);
|
|
$claims = $user->customClaims ?? [];
|
|
|
|
// Cache for 5 minutes
|
|
Cache::put("firebase_claims_{$uid}", $claims, 300);
|
|
|
|
return $claims;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to get custom claims', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add role to user
|
|
*/
|
|
public function addRole(string $uid, string $role): bool
|
|
{
|
|
$claims = $this->getCustomClaims($uid);
|
|
$claims['role'] = $role;
|
|
|
|
// Add role-specific flags
|
|
switch ($role) {
|
|
case 'admin':
|
|
$claims['admin'] = true;
|
|
break;
|
|
case 'restaurant':
|
|
$claims['restaurant'] = true;
|
|
break;
|
|
case 'rider':
|
|
$claims['rider'] = true;
|
|
break;
|
|
case 'customer':
|
|
$claims['customer'] = true;
|
|
break;
|
|
}
|
|
|
|
return $this->setCustomClaims($uid, $claims);
|
|
}
|
|
|
|
/**
|
|
* Remove role from user
|
|
*/
|
|
public function removeRole(string $uid, string $role): bool
|
|
{
|
|
$claims = $this->getCustomClaims($uid);
|
|
|
|
if (isset($claims['role']) && $claims['role'] === $role) {
|
|
unset($claims['role']);
|
|
}
|
|
|
|
// Remove role-specific flags
|
|
unset($claims[$role]);
|
|
|
|
return $this->setCustomClaims($uid, $claims);
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| User Management
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Create a new Firebase user
|
|
*/
|
|
public function createUser(array $properties): ?array
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
$request = CreateRequest::new();
|
|
|
|
if (isset($properties['email'])) {
|
|
$request = $request->withEmail($properties['email']);
|
|
}
|
|
if (isset($properties['password'])) {
|
|
$request = $request->withPassword($properties['password']);
|
|
}
|
|
if (isset($properties['phone'])) {
|
|
$request = $request->withPhoneNumber($properties['phone']);
|
|
}
|
|
if (isset($properties['display_name'])) {
|
|
$request = $request->withDisplayName($properties['display_name']);
|
|
}
|
|
if (isset($properties['photo_url'])) {
|
|
$request = $request->withPhotoUrl($properties['photo_url']);
|
|
}
|
|
if (isset($properties['disabled'])) {
|
|
$request = $properties['disabled'] ? $request->markAsDisabled() : $request->markAsEnabled();
|
|
}
|
|
if (isset($properties['email_verified'])) {
|
|
$request = $properties['email_verified'] ? $request->markEmailAsVerified() : $request->markEmailAsUnverified();
|
|
}
|
|
if (isset($properties['uid'])) {
|
|
$request = $request->withUid($properties['uid']);
|
|
}
|
|
|
|
$user = $auth->createUser($request);
|
|
|
|
return [
|
|
'uid' => $user->uid,
|
|
'email' => $user->email,
|
|
'phone' => $user->phoneNumber,
|
|
'display_name' => $user->displayName,
|
|
'photo_url' => $user->photoUrl,
|
|
'disabled' => $user->disabled,
|
|
'email_verified' => $user->emailVerified,
|
|
];
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to create Firebase user', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user by UID
|
|
*/
|
|
public function getUser(string $uid): ?array
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
$user = $auth->getUser($uid);
|
|
|
|
return [
|
|
'uid' => $user->uid,
|
|
'email' => $user->email,
|
|
'phone' => $user->phoneNumber,
|
|
'display_name' => $user->displayName,
|
|
'photo_url' => $user->photoUrl,
|
|
'disabled' => $user->disabled,
|
|
'email_verified' => $user->emailVerified,
|
|
'custom_claims' => $user->customClaims,
|
|
'created_at' => $user->metadata->createdAt,
|
|
'last_login' => $user->metadata->lastLoginAt,
|
|
];
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to get Firebase user', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user by email
|
|
*/
|
|
public function getUserByEmail(string $email): ?array
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
$user = $auth->getUserByEmail($email);
|
|
|
|
return [
|
|
'uid' => $user->uid,
|
|
'email' => $user->email,
|
|
'phone' => $user->phoneNumber,
|
|
'display_name' => $user->displayName,
|
|
'disabled' => $user->disabled,
|
|
];
|
|
} catch (FirebaseException $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user properties
|
|
*/
|
|
public function updateUser(string $uid, array $properties): bool
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return false;
|
|
}
|
|
|
|
$auth->updateUser($uid, $properties);
|
|
|
|
return true;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to update Firebase user', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable a user
|
|
*/
|
|
public function disableUser(string $uid): bool
|
|
{
|
|
return $this->updateUser($uid, ['disabled' => true]);
|
|
}
|
|
|
|
/**
|
|
* Enable a user
|
|
*/
|
|
public function enableUser(string $uid): bool
|
|
{
|
|
return $this->updateUser($uid, ['disabled' => false]);
|
|
}
|
|
|
|
/**
|
|
* Delete a user
|
|
*/
|
|
public function deleteUser(string $uid): bool
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return false;
|
|
}
|
|
|
|
$auth->deleteUser($uid);
|
|
|
|
// Clear cached claims
|
|
Cache::forget("firebase_claims_{$uid}");
|
|
|
|
return true;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to delete Firebase user', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Password Management
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Generate password reset link
|
|
*/
|
|
public function generatePasswordResetLink(string $email): ?string
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
return $auth->getPasswordResetLink($email);
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to generate password reset link', [
|
|
'email' => $email,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate email verification link
|
|
*/
|
|
public function generateEmailVerificationLink(string $email): ?string
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return null;
|
|
}
|
|
|
|
return $auth->getEmailVerificationLink($email);
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to generate email verification link', [
|
|
'email' => $email,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Revoke all refresh tokens for a user
|
|
*/
|
|
public function revokeRefreshTokens(string $uid): bool
|
|
{
|
|
try {
|
|
$auth = $this->getAuth();
|
|
if (! $auth) {
|
|
return false;
|
|
}
|
|
|
|
$auth->revokeRefreshTokens($uid);
|
|
|
|
return true;
|
|
} catch (FirebaseException $e) {
|
|
Log::error('Failed to revoke refresh tokens', [
|
|
'uid' => $uid,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|