| Server IP : 65.108.144.40 / Your IP : 216.73.217.165 Web Server : Apache/2.4.52 (Ubuntu) System : Linux ubuntu-8gb-hel1-1 5.15.0-173-generic #183-Ubuntu SMP Fri Mar 6 13:29:34 UTC 2026 x86_64 User : dev ( 1000) PHP Version : 8.2.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /var/www/html/project-slim/app/Services/ |
Upload File : |
<?php
namespace App\Services;
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Response\ApiResponse;
use App\Exceptions\ValidationException;
use App\Exceptions\UnauthorizedException;
use App\Messages\MessageConstants;
use App\Messages\MessageHelper;
use App\Services\PublicIdService;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class AuthService {
protected UserRepositoryInterface $userRepo;
protected array $jwtConfig;
public function __construct(UserRepositoryInterface $userRepo, array $jwtConfig) {
$this->userRepo = $userRepo;
$this->jwtConfig = $jwtConfig;
}
public function login(string $email, string $password): array {
$user = $this->userRepo->findByEmail($email);
if (!$user || !$this->verifyPassword($password, $user->password_hash)) {
throw new UnauthorizedException(MessageConstants::AUTH_INVALID_CREDENTIALS);
}
if (!$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_INACTIVE);
}
$tokens = $this->generateTokens($user);
return [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
'token_type' => 'Bearer',
'expires_in' => $this->jwtConfig['expiry'],
'user' => $user->toArray()
];
}
public function register(array $data): array {
// Check if email already exists
$existingUser = $this->userRepo->findByEmail($data['email']);
if ($existingUser) {
throw new ValidationException(MessageHelper::alreadyExists(MessageConstants::FIELD_EMAIL), [
MessageConstants::FIELD_EMAIL => MessageConstants::EMAIL_ALREADY_REGISTERED
]);
}
// Prepare user data
$userData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'email' => $data['email'],
'company_name' => $data['company_name'],
'company_domain' => $data['company_domain'],
'password_hash' => $this->hashPassword($data['password']),
'is_active' => true,
'email_verified' => false,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
// Create user
$user = $this->userRepo->create($userData);
// Generate verification token (in real app, send email)
$verificationToken = $this->generateVerificationToken($user);
return [
'user' => $user->toArray(),
'message' => MessageConstants::USER_REGISTERED_VERIFY_EMAIL,
'verification_token' => $verificationToken
];
}
public function logout(string $token): array {
// In a real application, you would add this token to a blacklist
// For now, we'll just return success
return ['message' => MessageConstants::AUTH_LOGOUT_SUCCESSFUL];
}
public function refreshToken(string $refreshToken): array {
try {
$decoded = JWT::decode($refreshToken, new Key($this->jwtConfig['secret'], $this->jwtConfig['algorithm']));
if ($decoded->type !== 'refresh') {
throw new UnauthorizedException(MessageHelper::invalid('refresh token'));
}
$user = $this->userRepo->findByEmail($decoded->email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
$tokens = $this->generateTokens($user);
return [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
'token_type' => 'Bearer',
'expires_in' => $this->jwtConfig['expiry']
];
} catch (\Exception $e) {
throw new UnauthorizedException(MessageHelper::invalid('refresh token'));
}
}
public function forgotPassword(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
// Don't reveal if email exists or not
return ApiResponse::success(['message' => MessageConstants::PASSWORD_RESET_IF_EMAIL_EXISTS]);
}
$resetToken = $this->generatePasswordResetToken($user);
// In a real application, send email with reset link
// For now, return the token (remove this in production)
return ApiResponse::success([
'message' => MessageConstants::AUTH_RESET_LINK_SENT,
'reset_token' => $resetToken // Remove this in production
]);
}
public function resetPassword(string $token, string $newPassword): array {
try {
$decoded = JWT::decode($token, new Key($this->jwtConfig['secret'], $this->jwtConfig['algorithm']));
if ($decoded->type !== 'password_reset') {
throw new UnauthorizedException(MessageHelper::invalid('reset token'));
}
$user = $this->userRepo->findByEmail($decoded->email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
$hashedPassword = $this->hashPassword($newPassword);
$this->userRepo->updatePassword($user->id, $hashedPassword);
return ApiResponse::success(['message' => MessageConstants::AUTH_PASSWORD_RESET_SUCCESSFUL]);
} catch (\Exception $e) {
throw new UnauthorizedException(MessageHelper::invalid('reset token') . ' or ' . MessageHelper::expired('reset token'));
}
}
public function changePassword(int $userId, string $currentPassword, string $newPassword): array {
$user = $this->userRepo->findById($userId);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($currentPassword, $user->password_hash)) {
throw new UnauthorizedException(MessageHelper::incorrect('current password'));
}
$hashedPassword = $this->hashPassword($newPassword);
$this->userRepo->updatePassword($userId, $hashedPassword);
return ApiResponse::success(['message' => MessageConstants::AUTH_PASSWORD_CHANGED]);
}
public function verifyEmail(string $token): array {
try {
$decoded = JWT::decode($token, new Key($this->jwtConfig['secret'], $this->jwtConfig['algorithm']));
if ($decoded->type !== 'email_verification') {
throw new UnauthorizedException(MessageHelper::invalid('verification token'));
}
$user = $this->userRepo->findByEmail($decoded->email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if ($user->email_verified) {
return ApiResponse::success(['message' => MessageConstants::VERIFICATION_ALREADY_VERIFIED]);
}
$this->userRepo->verifyEmail($user->id);
return ApiResponse::success(['message' => MessageConstants::AUTH_EMAIL_VERIFIED]);
} catch (\Exception $e) {
throw new UnauthorizedException(MessageHelper::invalid('verification token') . ' or ' . MessageHelper::expired('verification token'));
}
}
public function resendVerificationEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
return ApiResponse::success(['message' => MessageConstants::VERIFICATION_IF_EMAIL_EXISTS]);
}
if ($user->email_verified) {
return ApiResponse::success(['message' => MessageConstants::VERIFICATION_ALREADY_VERIFIED]);
}
$verificationToken = $this->generateVerificationToken($user);
return ApiResponse::success([
'message' => MessageConstants::AUTH_VERIFICATION_SENT,
'verification_token' => $verificationToken // Remove this in production
]);
}
/**
* Alias for resendVerificationEmail for controller compatibility
*/
public function resendVerification(string $email): array {
return $this->resendVerificationEmail($email);
}
/**
* Get current user - alias for getAuthenticatedUser
*/
public function getCurrentUser(int $userId): array {
return $this->getAuthenticatedUser($userId);
}
/**
* Enable two-factor authentication
*/
public function enableTwoFactor(int $userId, string $password): array {
$user = $this->userRepo->findById($userId);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($password, $user->password_hash)) {
return false; // Return false for controller to handle
}
return $this->setupTwoFactor($userId);
}
/**
* Disable two-factor authentication
*/
public function disableTwoFactor(int $userId, string $password): array {
$user = $this->userRepo->findById($userId);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($password, $user->password_hash)) {
return false; // Return false for controller to handle
}
// Disable 2FA
$this->userRepo->updateTwoFactor($userId, false, null);
return ['message' => MessageConstants::AUTH_TWO_FACTOR_DISABLED];
}
public function checkToken(string $token): array {
try {
$decoded = JWT::decode($token, new Key($this->jwtConfig['secret'], $this->jwtConfig['algorithm']));
if ($decoded->type !== 'access') {
throw new UnauthorizedException(MessageHelper::invalid('token type'));
}
$user = $this->userRepo->findByEmail($decoded->email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
return ApiResponse::success([
'valid' => true,
'user' => $user->toArray()
]);
} catch (\Exception $e) {
throw new UnauthorizedException(MessageConstants::AUTH_TOKEN_INVALID);
}
}
public function setupTwoFactor(int $userId): array {
$user = $this->userRepo->findById($userId);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
// Generate secret for 2FA (in real app, use Google Authenticator library)
$secret = bin2hex(random_bytes(16));
$this->userRepo->updateTwoFactor($userId, false, $secret);
return ApiResponse::success([
'secret' => $secret,
'qr_code_url' => "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=" . urlencode("otpauth://totp/YourApp:$user->email?secret=$secret&issuer=YourApp"),
'message' => 'Scan the QR code with your authenticator app and verify to enable 2FA'
]);
}
public function verifyTwoFactor(int $userId, string $code): array {
$user = $this->userRepo->findById($userId);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
// In real app, verify the TOTP code
// For demo purposes, accept "123456" as valid code
if ($code === "123456" || $code === $user->two_factor_secret) {
$this->userRepo->updateTwoFactor($userId, true, $user->two_factor_secret);
return ApiResponse::success(['message' => MessageConstants::AUTH_TWO_FACTOR_ENABLED]);
}
throw new UnauthorizedException(MessageHelper::invalid('verification code'));
}
public function getUserSessions(int $userId): array {
// In a real application, you would fetch active sessions from a sessions table
// For demo purposes, return mock data
return [
[
'id' => '1',
'device' => 'Chrome on Windows',
'ip_address' => '192.168.1.100',
'location' => 'New York, US',
'last_activity' => date('Y-m-d H:i:s'),
'current' => true
],
[
'id' => '2',
'device' => 'Safari on iPhone',
'ip_address' => '192.168.1.101',
'location' => 'New York, US',
'last_activity' => date('Y-m-d H:i:s', strtotime('-1 hour')),
'current' => false
]
];
}
public function revokeSession(int $userId, string $sessionId): array {
// In a real application, you would revoke the specific session
// For demo purposes, just return success
return ApiResponse::success(['message' => MessageConstants::AUTH_SESSION_REVOKED]);
}
private function generateTokens($user): array {
$now = time();
$accessPayload = [
'iat' => $now,
'exp' => $now + $this->jwtConfig['expiry'],
'type' => 'access',
'email' => $user->email, // Use email as identifier instead of user_id
'role' => $user->role
];
$refreshPayload = [
'iat' => $now,
'exp' => $now + $this->jwtConfig['refresh_expiry'],
'type' => 'refresh',
'email' => $user->email // Use email as identifier instead of user_id
];
return [
'access_token' => JWT::encode($accessPayload, $this->jwtConfig['secret'], $this->jwtConfig['algorithm']),
'refresh_token' => JWT::encode($refreshPayload, $this->jwtConfig['secret'], $this->jwtConfig['algorithm'])
];
}
private function generateVerificationToken($user): string {
$payload = [
'iat' => time(),
'exp' => time() + 86400, // 24 hours
'type' => 'email_verification',
'email' => $user->email // Use email instead of user_id
];
return JWT::encode($payload, $this->jwtConfig['secret'], $this->jwtConfig['algorithm']);
}
/**
* Get authenticated user details
*/
public function getAuthenticatedUser(int $userId): array {
$user = $this->userRepo->findById($userId);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
return $user->toArray();
}
/**
* Revoke all user sessions
*/
public function revokeAllSessions(int $userId): array {
// In a real application, you would mark all tokens as revoked in a blacklist
// For now, we'll just return success
return ['message' => MessageConstants::AUTH_SESSIONS_REVOKED];
}
/**
* Get user profile information
*/
public function getUserProfile(int $userId): array {
$user = $this->userRepo->findById($userId);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
// Return user profile without sensitive data
$profile = $user->toArray();
unset($profile['password_hash']);
return $profile;
}
/**
* Update user profile
*/
public function updateUserProfile(int $userId, array $data): array {
$user = $this->userRepo->findById($userId);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
// Remove sensitive fields that shouldn't be updated via profile
$allowedFields = ['first_name', 'last_name', 'phone', 'email'];
$updateData = [];
foreach ($allowedFields as $field) {
if (isset($data[$field])) {
$updateData[$field] = $data[$field];
}
}
if (empty($updateData)) {
throw new \InvalidArgumentException(MessageHelper::required('valid fields to update'));
}
// If email is being updated, check if it's already taken
if (isset($updateData['email']) && $updateData['email'] !== $user->email) {
$existingUser = $this->userRepo->findByEmail($updateData['email']);
if ($existingUser) {
throw new \InvalidArgumentException(MessageConstants::EMAIL_ALREADY_REGISTERED);
}
}
$updatedUser = $this->userRepo->update($userId, $updateData);
return $updatedUser->toArray();
}
// Email-based authentication methods for enhanced security
/**
* Get current user by email
*/
public function getCurrentUserByEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
return $user->toArray();
}
/**
* Change password by email
*/
public function changePasswordByEmail(string $email, string $currentPassword, string $newPassword): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($currentPassword, $user->password_hash)) {
throw new UnauthorizedException(MessageHelper::incorrect('current password'));
}
$hashedPassword = $this->hashPassword($newPassword);
$this->userRepo->updatePassword($user->id, $hashedPassword);
return ['message' => MessageConstants::AUTH_PASSWORD_CHANGED];
}
/**
* Enable two-factor authentication by email
*/
public function enableTwoFactorByEmail(string $email, string $password): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($password, $user->password_hash)) {
throw new UnauthorizedException(MessageHelper::incorrect(MessageConstants::FIELD_PASSWORD));
}
// Generate 2FA secret and QR code
$secret = $this->generateTwoFactorSecret();
$qrCode = $this->generateQRCode($user->email, $secret);
// Save 2FA secret to user
$this->userRepo->update($user->id, ['two_factor_secret' => $secret]);
return [
'secret' => $secret,
'qr_code' => $qrCode,
'message' => MessageConstants::AUTH_TWO_FACTOR_ENABLED
];
}
/**
* Disable two-factor authentication by email
*/
public function disableTwoFactorByEmail(string $email, string $password): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
if (!$this->verifyPassword($password, $user->password_hash)) {
throw new UnauthorizedException(MessageHelper::incorrect(MessageConstants::FIELD_PASSWORD));
}
// Remove 2FA secret from user
$this->userRepo->update($user->id, ['two_factor_secret' => null, 'two_factor_enabled' => false]);
return ['message' => MessageConstants::AUTH_TWO_FACTOR_DISABLED];
}
/**
* Get authenticated user by email
*/
public function getAuthenticatedUserByEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
return $user->toArray();
}
/**
* Get user sessions by email
*/
public function getUserSessionsByEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
// Get user sessions (implementation depends on session storage)
return $this->getUserSessions($user->id);
}
/**
* Revoke all sessions by email
*/
public function revokeAllSessionsByEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
// Revoke all sessions for user
return $this->revokeAllSessions($user->id);
}
/**
* Revoke session by email and public session ID
*/
public function revokeSessionByEmail(string $email, string $publicSessionId): array {
$user = $this->userRepo->findByEmail($email);
if (!$user) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND);
}
// Convert public session ID to internal ID if needed
// For now, assume session ID is already public-safe
return $this->revokeSession($user->id, $publicSessionId);
}
/**
* Get user profile by email
*/
public function getUserProfileByEmail(string $email): array {
$user = $this->userRepo->findByEmail($email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
// Return user profile without sensitive data
$profile = $user->toArray();
unset($profile['password_hash']);
return $profile;
}
/**
* Update user profile by email
*/
public function updateUserProfileByEmail(string $email, array $data): array {
$user = $this->userRepo->findByEmail($email);
if (!$user || !$user->isActive()) {
throw new UnauthorizedException(MessageConstants::USER_NOT_FOUND . ' or ' . MessageConstants::USER_INACTIVE);
}
// Remove sensitive fields that shouldn't be updated via profile
$allowedFields = ['first_name', 'last_name', 'phone'];
$updateData = [];
foreach ($allowedFields as $field) {
if (isset($data[$field])) {
$updateData[$field] = $data[$field];
}
}
if (empty($updateData)) {
throw new \InvalidArgumentException(MessageHelper::required('valid fields to update'));
}
$updatedUser = $this->userRepo->update($user->id, $updateData);
return $updatedUser->toArray();
}
private function generatePasswordResetToken($user): string {
$payload = [
'iat' => time(),
'exp' => time() + 3600, // 1 hour
'type' => 'password_reset',
'email' => $user->email // Use email instead of user_id
];
return JWT::encode($payload, $this->jwtConfig['secret'], $this->jwtConfig['algorithm']);
}
private function generateTwoFactorSecret(): string {
// Generate a random 32-character secret for 2FA
return $this->base32_encode(random_bytes(20));
}
private function generateQRCode(string $email, string $secret): string {
// Generate QR code URL for 2FA setup
$appName = urlencode('Project Slim');
$label = urlencode($email);
return "otpauth://totp/{$label}?secret={$secret}&issuer={$appName}";
}
private function hashPassword(string $password): string {
return password_hash($password, PASSWORD_DEFAULT);
}
private function verifyPassword(string $password, string $hash): bool {
return password_verify($password, $hash);
}
/**
* Helper function for base32 encoding (simplified version)
*/
private function base32_encode(string $data): string {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$output = '';
$input = str_split($data);
$buffer = 0;
$bitsLeft = 0;
foreach ($input as $byte) {
$buffer = ($buffer << 8) | ord($byte);
$bitsLeft += 8;
while ($bitsLeft >= 5) {
$output .= $alphabet[($buffer >> ($bitsLeft - 5)) & 31];
$bitsLeft -= 5;
}
}
if ($bitsLeft > 0) {
$output .= $alphabet[($buffer << (5 - $bitsLeft)) & 31];
}
return $output;
}
}