Skip to content

Commit 686da0b

Browse files
committed
implement: x authentication
1 parent 28f48ea commit 686da0b

File tree

10 files changed

+357
-12
lines changed

10 files changed

+357
-12
lines changed

app/Http/Controllers/Auth/AuthController.php

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,5 +697,272 @@ function ($user, $password) {
697697
->withErrors(['email' => __($status)]);
698698
}
699699

700+
public function xRedirect()
701+
{
702+
try {
703+
Log::channel('audit_trail')->info('Redirecting to X for authentication.', [
704+
'time' => microtime(true)
705+
]);
706+
return Socialite::driver('x')->redirect();
707+
} catch (Exception $e) {
708+
Log::error('X redirect failed: ' . $e->getMessage(), [
709+
'trace' => $e->getTraceAsString()
710+
]);
711+
return redirect()->route('login')->with('error', __('messages.error_x_auth_failed'));
712+
}
713+
}
714+
715+
public function xCallback(Request $request): RedirectResponse
716+
{
717+
$time_start_callback = microtime(true);
718+
Log::channel('audit_trail')->info('X callback initiated.', [
719+
'ip_address' => $request->ip(),
720+
'query_params' => $request->query(),
721+
'time_start_callback' => $time_start_callback
722+
]);
723+
724+
try {
725+
if ($request->has('error')) {
726+
Log::channel('audit_trail')->error('X callback returned an error.', [
727+
'error' => $request->input('error'),
728+
'error_description' => $request->input('error_description'),
729+
'ip_address' => $request->ip(),
730+
'time' => microtime(true),
731+
]);
732+
return redirect()->route('login')
733+
->with('error', __('messages.error_x_auth_denied_or_failed'));
734+
}
735+
736+
if (!$request->has('code')) {
737+
Log::channel('audit_trail')->error('X callback missing authorization code.', [
738+
'ip_address' => $request->ip(),
739+
'query_params' => $request->query(),
740+
'time' => microtime(true),
741+
]);
742+
return redirect()->route('login')
743+
->with('error', __('messages.error_x_auth_failed') . ' (Missing authorization code)');
744+
}
745+
746+
$time_before_socialite_user = microtime(true);
747+
Log::channel('audit_trail')->info('Attempting to fetch X user from Socialite.', ['time' => $time_before_socialite_user]);
748+
749+
$xUser = Socialite::driver('x')->stateless()->user();
750+
751+
$time_after_socialite_user = microtime(true);
752+
$duration_socialite_user_call = $time_after_socialite_user - $time_before_socialite_user;
753+
Log::channel('audit_trail')->info('Successfully fetched X user from Socialite.', [
754+
'x_user_id' => $xUser->getId(),
755+
'x_user_email' => $xUser->getEmail(),
756+
'time' => $time_after_socialite_user,
757+
'duration_socialite_user_call_seconds' => $duration_socialite_user_call
758+
]);
759+
760+
$time_before_db_lookup = microtime(true);
761+
$userModel = User::where('x_id', $xUser->getId())->first();
762+
$time_after_db_lookup = microtime(true);
763+
Log::channel('audit_trail')->info('DB lookup for existing X ID.', [
764+
'duration_db_lookup_seconds' => $time_after_db_lookup - $time_before_db_lookup,
765+
'found_user_by_x_id' => !is_null($userModel)
766+
]);
767+
768+
$action = "Logged in";
769+
$initialUserModelNull = is_null($userModel);
770+
771+
if (!$userModel) {
772+
$time_before_handle_user = microtime(true);
773+
Log::channel('audit_trail')->info('No existing user by X ID. Calling handleXUser.', [
774+
'x_email' => $xUser->getEmail(),
775+
'time' => $time_before_handle_user
776+
]);
777+
778+
$userModel = $this->handleXUser($xUser);
779+
780+
$time_after_handle_user = microtime(true);
781+
Log::channel('audit_trail')->info('Finished handleXUser call.', [
782+
'user_id_returned' => $userModel->id,
783+
'was_recently_created' => $userModel->wasRecentlyCreated,
784+
'duration_handle_user_seconds' => $time_after_handle_user - $time_before_handle_user
785+
]);
786+
787+
if ($userModel->wasRecentlyCreated) {
788+
$action = 'Registered and logged in';
789+
event(new UserRegistered($userModel));
790+
} elseif ($initialUserModelNull && $userModel->x_id == $xUser->getId()) {
791+
$action = 'Logged in (linked X to existing email account)';
792+
} else {
793+
$action = 'Logged in (handleXUser resolved)';
794+
}
795+
} else {
796+
$action = "Logged in (existing X ID)";
797+
}
798+
799+
$user = $userModel;
800+
801+
Auth::login($user, true);
802+
$request->session()->regenerate();
803+
804+
$time_end_callback = microtime(true);
805+
$total_callback_duration = $time_end_callback - $time_start_callback;
806+
Log::channel('audit_trail')->info("User $action via X.", [
807+
'user_id' => $user->id,
808+
'username' => $user->username,
809+
'email' => $user->email,
810+
'x_id' => $xUser->getId(),
811+
'ip_address' => $request->ip(),
812+
'time_end_callback' => $time_end_callback,
813+
'total_callback_duration_seconds' => $total_callback_duration
814+
]);
815+
816+
return redirect()->intended(route('home'))->with('success', __('messages.x_login_success'));
817+
818+
} catch (InvalidStateException $e) {
819+
Log::channel('audit_trail')->error('X authentication failed: Invalid State.', [
820+
'error' => $e->getMessage(),
821+
'ip_address' => $request->ip(),
822+
'time_exception' => microtime(true),
823+
]);
824+
Log::error('X Auth Invalid State Error', [
825+
'error' => $e->getMessage(),
826+
'trace' => $e->getTraceAsString(),
827+
'ip' => $request->ip()
828+
]);
829+
return redirect()->route('login')
830+
->with('error', __('messages.error_x_auth_failed_state') ?: 'X authentication failed due to an invalid state. Please try again.');
831+
} catch (ConnectException $e) {
832+
Log::channel('audit_trail')->error('X authentication failed: Connection issue (Guzzle).', [
833+
'error' => $e->getMessage(),
834+
'ip_address' => $request->ip(),
835+
'time_exception' => microtime(true),
836+
]);
837+
Log::error('X Auth Guzzle ConnectException', [
838+
'error' => $e->getMessage(),
839+
'trace' => $e->getTraceAsString(),
840+
'ip' => $request->ip()
841+
]);
842+
return redirect()->route('login')
843+
->with('error', __('messages.error_x_auth_network') ?: 'Could not connect to X for authentication. Please check your internet connection and try again.');
844+
} catch (Exception $e) {
845+
Log::channel('audit_trail')->error('X authentication/callback failed with generic Exception.', [
846+
'error' => $e->getMessage(),
847+
'exception_type' => get_class($e),
848+
'ip_address' => $request->ip(),
849+
'time_exception' => microtime(true),
850+
]);
851+
Log::error('X Auth System Error (Generic Exception)', [
852+
'error' => $e->getMessage(),
853+
'exception_type' => get_class($e),
854+
'trace' => $e->getTraceAsString(),
855+
'ip' => $request->ip()
856+
]);
857+
858+
return redirect()->route('login')
859+
->with('error', __('messages.error_x_login_failed'));
860+
}
861+
}
862+
863+
private function handleXUser($xUser): User
864+
{
865+
$time_start_handle = microtime(true);
866+
Log::channel('audit_trail')->info('Inside handleXUser.', [
867+
'x_email' => $xUser->getEmail(),
868+
'x_id_from_socialite' => $xUser->getId(),
869+
'time_start' => $time_start_handle
870+
]);
871+
872+
$existingUserByEmail = User::where('email', $xUser->getEmail())->first();
873+
$time_after_email_lookup = microtime(true);
874+
Log::channel('audit_trail')->info('DB lookup for existing email in handleXUser.', [
875+
'duration_email_lookup_seconds' => $time_after_email_lookup - $time_start_handle,
876+
'found_user_by_email' => !is_null($existingUserByEmail)
877+
]);
878+
879+
if ($existingUserByEmail) {
880+
Log::channel('audit_trail')->info('Existing user found by email in handleXUser. Updating with X ID if not set.', [
881+
'user_id' => $existingUserByEmail->id,
882+
'current_x_id_on_user' => $existingUserByEmail->x_id
883+
]);
884+
$userToReturn = $this->updateExistingUserWithX($existingUserByEmail, $xUser);
885+
$log_action = 'updated_existing_user_with_x_info';
886+
} else {
887+
Log::channel('audit_trail')->info('No existing user by email in handleXUser. Creating new user from X info.');
888+
$userToReturn = $this->createUserFromX($xUser);
889+
$log_action = 'created_new_user_from_x_info';
890+
}
891+
892+
$time_end_handle = microtime(true);
893+
Log::channel('audit_trail')->info('Finished handleXUser.', [
894+
'user_id_processed' => $userToReturn->id,
895+
'action_taken' => $log_action,
896+
'was_recently_created_flag' => $userToReturn->wasRecentlyCreated,
897+
'final_x_id_on_user' => $userToReturn->x_id,
898+
'duration_handle_user_total_seconds' => $time_end_handle - $time_start_handle,
899+
]);
900+
901+
return $userToReturn;
902+
}
903+
904+
private function updateExistingUserWithX(User $user, $xUser): User
905+
{
906+
if (is_null($user->x_id)) {
907+
$user->x_id = $xUser->getId();
908+
}
909+
910+
if (!$user->profile_picture && $xUser->getAvatar()) {
911+
$user->profile_picture = $xUser->getAvatar();
912+
}
913+
914+
$user->email_verified_at = $user->email_verified_at ?? now();
915+
$user->save();
916+
Log::channel('audit_trail')->info('Updated existing user with X info.', ['user_id' => $user->id, 'x_id_set' => $user->x_id]);
917+
return $user;
918+
}
919+
920+
private function createUserFromX($xUser): User
921+
{
922+
$time_start_create = microtime(true);
923+
Log::channel('audit_trail')->info('Creating new user from X data.', [
924+
'x_email' => $xUser->getEmail(),
925+
'x_name' => $xUser->getName(),
926+
'time_start' => $time_start_create
927+
]);
928+
929+
$username = $this->generateUniqueUsername($xUser->getName() ?: $xUser->getNickname() ?: 'user');
930+
931+
$name = $xUser->getName() ?: $xUser->getNickname() ?: '';
932+
$nameParts = explode(' ', $name, 2);
933+
$firstName = $nameParts[0] ?? Str::studly(Str::before($xUser->getEmail() ?: 'user@x.com', '@'));
934+
$lastName = $nameParts[1] ?? null;
935+
936+
$user = User::create([
937+
'first_name' => $firstName,
938+
'last_name' => $lastName,
939+
'username' => $username,
940+
'email' => $xUser->getEmail() ?: $username . '@x-user.local',
941+
'x_id' => $xUser->getId(),
942+
'email_verified_at' => now(),
943+
'password' => Hash::make(Str::random(24)),
944+
]);
945+
Log::channel('audit_trail')->info('User model created in DB.', ['user_id' => $user->id, 'username' => $username, 'time_after_eloquent_create' => microtime(true)]);
946+
947+
if ($xUser->getAvatar()) {
948+
$user->profile_picture = $xUser->getAvatar();
949+
} else {
950+
$profilePicturePath = $this->avatarService->generateInitialsAvatar(
951+
$user->first_name,
952+
$user->last_name ?? '',
953+
$user->id
954+
);
955+
$user->profile_picture = $profilePicturePath;
956+
}
957+
$user->save();
958+
959+
$time_end_create = microtime(true);
960+
Log::channel('audit_trail')->info('Finished creating user from X and saved profile picture.', [
961+
'user_id' => $user->id,
962+
'duration_create_user_seconds' => $time_end_create - $time_start_create
963+
]);
964+
965+
return $user;
966+
}
700967
}
701968

app/Models/User.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class User extends Authenticatable
2222
'profile_picture',
2323
'header_background',
2424
'google_id',
25+
'x_id',
2526
'email_verified_at',
2627
'email_verification_token',
2728
'show_voted_posts_publicly',
@@ -38,6 +39,7 @@ class User extends Authenticatable
3839
'password',
3940
'remember_token',
4041
'google_id',
42+
'x_id',
4143
];
4244

4345
protected $casts = [

config/services.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,8 @@
4141
'api_secret_key' => env('X_API_SECRET_KEY'),
4242
'access_token' => env('X_ACCESS_TOKEN'),
4343
'access_token_secret' => env('X_ACCESS_TOKEN_SECRET'),
44+
'client_id' => env('X_CLIENT_ID'),
45+
'client_secret' => env('X_CLIENT_SECRET'),
46+
'redirect' => env('APP_URL') . '/auth/x/callback',
4447
],
4548
];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
public function up(): void
9+
{
10+
Schema::table('users', function (Blueprint $table) {
11+
$table->string('x_id')->nullable()->unique()->after('google_id');
12+
});
13+
}
14+
15+
public function down(): void
16+
{
17+
Schema::table('users', function (Blueprint $table) {
18+
$table->dropColumn('x_id');
19+
});
20+
}
21+
};

lang/en/messages.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@
276276
'create_post.subject_2_placeholder' => 'Subject 2',
277277
'create_post.submit_button' => 'Submit',
278278
'create_post.js.fill_all_fields_warning' => 'Please fill all required fields, including images.',
279-
'create_post.js.moderation_in_progress'=> 'Please wait a moment. We are checking your post to ensure it meets our community guidelines.',
279+
'create_post.js.moderation_in_progress' => 'Please wait a moment. We are checking your post to ensure it meets our community guidelines.',
280280
'create_post.js.overlay.title' => 'Finalizing Your Post...',
281281
'create_post.js.overlay.message' => 'We\'re running a quick automated check to ensure everything meets our community standards. This usually takes just a few seconds. Thanks for your patience!',
282282

@@ -506,9 +506,6 @@
506506

507507
'password_set_required_info' => 'For your security, you must set a password to access this feature.',
508508
'password_set_successfully' => 'Your password has been successfully set. You can now proceed.',
509-
'auth' => [
510-
'forgot_password' => 'Forgot Your Password?',
511-
],
512509

513510
'settings' => [
514511
'display_preferences_title' => 'Display Preferences',
@@ -521,4 +518,16 @@
521518
'ai_insight_hidden' => 'Hidden by Default',
522519
'ai_insight_hidden_desc' => 'Hide the panel completely. You can open it by clicking the icon next to the question.',
523520
],
521+
522+
'auth' => [
523+
'login_with_x' => 'Login with X',
524+
'signup_with_x' => 'Sign up with X',
525+
'forgot_password' => 'Forgot Your Password?',
526+
],
527+
'x_login_success' => 'Successfully logged in with X!',
528+
'error_x_auth_failed' => 'X authentication failed. Please try again.',
529+
'error_x_auth_denied_or_failed' => 'X authentication was denied or failed.',
530+
'error_x_auth_failed_state' => 'X authentication failed due to invalid state.',
531+
'error_x_auth_network' => 'Could not connect to X for authentication.',
532+
'error_x_login_failed' => 'X login failed. Please try again.',
524533
];

lang/ru/messages.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,6 @@
486486

487487
'password_set_required_info' => 'Для обеспечения безопасности вам необходимо установить пароль для доступа к этой функции.',
488488
'password_set_successfully' => 'Ваш пароль успешно установлен. Теперь вы можете продолжить.',
489-
'auth' => [
490-
'forgot_password' => 'Забыли пароль?',
491-
],
492489

493490
'settings' => [
494491
'display_preferences_title' => 'Настройки отображения',
@@ -503,4 +500,17 @@
503500
],
504501

505502
'app.telegram_ad' => 'Подписывайтесь на наш телеграм-канал!',
503+
504+
505+
'auth' => [
506+
'login_with_x' => 'Войти через X',
507+
'signup_with_x' => 'Зарегистрироваться через X',
508+
'forgot_password' => 'Забыли пароль?',
509+
],
510+
'x_login_success' => 'Вы успешно вошли в систему через X!',
511+
'error_x_auth_failed' => 'Ошибка аутентификации X. Пожалуйста, попробуйте еще раз.',
512+
'error_x_auth_denied_or_failed' => 'Ошибка аутентификации X: доступ отклонен или не удалось получить токен доступа.',
513+
'error_x_auth_failed_state' => 'Ошибка аутентификации X: состояние не совпадает. Пожалуйста, попробуйте еще раз.',
514+
'error_x_auth_network' => 'Ошибка сети при попытке аутентификации через X. Пожалуйста, проверьте ваше интернет-соединение и попробуйте еще раз.',
515+
'error_x_login_failed' => 'Не удалось войти через X. Пожалуйста, проверьте свои учетные данные и попробуйте еще раз.',
506516
];

0 commit comments

Comments
 (0)