diff --git a/.env.example b/.env.example index 7a537c82..4e38edbf 100644 --- a/.env.example +++ b/.env.example @@ -124,11 +124,19 @@ THREADS_CLIENT_REDIRECT="${APP_URL}/accounts/threads/callback" # Google (https://console.cloud.google.com) # Used for YouTube social account connection AND Google login/signup +GOOGLE_AUTH_ENABLED=false GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_REDIRECT="${APP_URL}/accounts/youtube/callback" GOOGLE_AUTH_CALLBACK="${APP_URL}/auth/google/callback" +# GitHub (https://github.com/settings/developers) +# Used for GitHub login/signup +GITHUB_AUTH_ENABLED=false +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_AUTH_CALLBACK="${APP_URL}/auth/github/callback" + # Pinterest (https://developers.pinterest.com) PINTEREST_CLIENT_ID= PINTEREST_CLIENT_SECRET= diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index 85099135..f2b4f7e0 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -11,11 +11,12 @@ class CreateUser { /** - * @param array{name: string, email: string, password?: string, google_id?: string, email_verified_at?: \DateTimeInterface|null, is_invite?: bool} $data + * @param array{name: string, email: string, password?: string, google_id?: string, github_id?: string, email_verified_at?: \DateTimeInterface|null, is_invite?: bool, registration_ip?: string|null} $data + * @param array $utmParameters */ - public static function execute(array $data): User + public static function execute(array $data, array $utmParameters = []): User { - return DB::transaction(function () use ($data): User { + return DB::transaction(function () use ($data, $utmParameters): User { $isInviteRegistration = data_get($data, 'is_invite', false); $account = Account::create([ @@ -23,14 +24,16 @@ public static function execute(array $data): User 'billing_email' => data_get($data, 'email'), ]); - $user = User::create([ + $user = User::create(array_merge([ 'name' => data_get($data, 'name'), 'email' => data_get($data, 'email'), 'password' => data_get($data, 'password'), 'google_id' => data_get($data, 'google_id'), + 'github_id' => data_get($data, 'github_id'), 'email_verified_at' => data_get($data, 'email_verified_at', $isInviteRegistration ? now() : null), 'account_id' => $account->id, - ]); + 'registration_ip' => data_get($data, 'registration_ip'), + ], $utmParameters)); $account->update(['owner_id' => $user->id]); diff --git a/app/Http/Controllers/App/Settings/AuthenticationController.php b/app/Http/Controllers/App/Settings/AuthenticationController.php index b531bf4e..e093d9a4 100644 --- a/app/Http/Controllers/App/Settings/AuthenticationController.php +++ b/app/Http/Controllers/App/Settings/AuthenticationController.php @@ -17,7 +17,7 @@ class AuthenticationController extends Controller { - private const array PROVIDERS = ['google']; + private const array PROVIDERS = ['google', 'github']; public function edit(Request $request): Response { @@ -115,6 +115,7 @@ private function getConnectedAccounts(User $user): array { $labels = [ 'google' => 'Google', + 'github' => 'GitHub', ]; return collect(self::PROVIDERS)->map(fn (string $provider) => [ diff --git a/app/Http/Controllers/Auth/Concerns/PreservesUtmParameters.php b/app/Http/Controllers/Auth/Concerns/PreservesUtmParameters.php new file mode 100644 index 00000000..90b174c4 --- /dev/null +++ b/app/Http/Controllers/Auth/Concerns/PreservesUtmParameters.php @@ -0,0 +1,48 @@ + + */ + private function extractUtmParameters(Request $request): array + { + return array_filter( + array_map( + fn (string $value) => mb_substr($value, 0, 255), + array_filter($request->only(self::UTM_KEYS), 'is_string'), + ), + ); + } + + private function storeUtmParameters(Request $request): void + { + $utms = $this->extractUtmParameters($request); + + if ($utms !== []) { + $request->session()->put('utm_parameters', $utms); + } + } + + /** + * @return array + */ + private function retrieveUtmParameters(): array + { + return session()->pull('utm_parameters', []); + } +} diff --git a/app/Http/Controllers/Auth/GitHubController.php b/app/Http/Controllers/Auth/GitHubController.php new file mode 100644 index 00000000..05be8a6b --- /dev/null +++ b/app/Http/Controllers/Auth/GitHubController.php @@ -0,0 +1,92 @@ +storeUtmParameters($request); + + return Socialite::driver('github') + ->scopes(['read:user', 'user:email']) + ->redirect(); + } + + public function callback(): RedirectResponse + { + try { + $githubUser = Socialite::driver('github')->user(); + } catch (\Exception) { + return redirect()->route('login'); + } + + $user = User::where('github_id', (string) $githubUser->getId()) + ->when($githubUser->getEmail(), fn ($query, $email) => $query->orWhere('email', $email)) + ->first(); + + if ($user) { + return $this->loginExistingUser($user, (string) $githubUser->getId()); + } + + if (! $githubUser->getEmail()) { + return redirect()->route('login')->withErrors([ + 'email' => __('auth.github_email_unavailable'), + ]); + } + + return $this->registerNewUser($githubUser); + } + + private function loginExistingUser(User $user, string $githubId): RedirectResponse + { + if (! $user->github_id) { + $user->update(['github_id' => $githubId]); + } + + if (! $user->hasVerifiedEmail()) { + $user->markEmailAsVerified(); + } + + Auth::login($user, remember: true); + + $this->retrieveUtmParameters(); + + return redirect()->route('app.home'); + } + + private function registerNewUser(\Laravel\Socialite\Contracts\User $githubUser): RedirectResponse + { + $utmParameters = $this->retrieveUtmParameters(); + + $user = CreateUser::execute([ + 'name' => $githubUser->getName() ?? $githubUser->getNickname() ?? explode('@', $githubUser->getEmail())[0], + 'email' => $githubUser->getEmail(), + 'github_id' => (string) $githubUser->getId(), + 'email_verified_at' => now(), + 'registration_ip' => request()->ip(), + ], $utmParameters); + + event(new Registered($user)); + + Auth::login($user, remember: true); + + session()->flash('auth_provider', 'github'); + + return redirect()->route('register.success', $utmParameters); + } +} diff --git a/app/Http/Controllers/Auth/SocialLoginController.php b/app/Http/Controllers/Auth/GoogleController.php similarity index 77% rename from app/Http/Controllers/Auth/SocialLoginController.php rename to app/Http/Controllers/Auth/GoogleController.php index ae75291e..822b7a8e 100644 --- a/app/Http/Controllers/Auth/SocialLoginController.php +++ b/app/Http/Controllers/Auth/GoogleController.php @@ -5,17 +5,23 @@ namespace App\Http\Controllers\Auth; use App\Actions\User\CreateUser; +use App\Http\Controllers\Auth\Concerns\PreservesUtmParameters; use App\Http\Controllers\Controller; use App\Models\User; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; -class SocialLoginController extends Controller +class GoogleController extends Controller { - public function redirect(): RedirectResponse + use PreservesUtmParameters; + + public function redirect(Request $request): RedirectResponse { + $this->storeUtmParameters($request); + return Socialite::driver('google-auth')->redirect(); } @@ -50,17 +56,22 @@ private function loginExistingUser(User $user, string $googleId): RedirectRespon Auth::login($user, remember: true); + $this->retrieveUtmParameters(); + return redirect()->route('app.home'); } private function registerNewUser(\Laravel\Socialite\Contracts\User $googleUser): RedirectResponse { + $utmParameters = $this->retrieveUtmParameters(); + $user = CreateUser::execute([ 'name' => $googleUser->getName(), 'email' => $googleUser->getEmail(), 'google_id' => $googleUser->getId(), 'email_verified_at' => now(), - ]); + 'registration_ip' => request()->ip(), + ], $utmParameters); event(new Registered($user)); @@ -68,6 +79,6 @@ private function registerNewUser(\Laravel\Socialite\Contracts\User $googleUser): session()->flash('auth_provider', 'google'); - return redirect()->route('register.success'); + return redirect()->route('register.success', $utmParameters); } } diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 54c95c14..b030065a 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth; use App\Actions\User\CreateUser; +use App\Http\Controllers\Auth\Concerns\PreservesUtmParameters; use App\Http\Controllers\Controller; use App\Models\User; use App\Rules\Timezone; @@ -18,8 +19,12 @@ class RegisteredUserController extends Controller { + use PreservesUtmParameters; + public function create(Request $request): Response { + $this->storeUtmParameters($request); + return Inertia::render('auth/Register', [ 'email' => $request->query('email'), 'redirect' => $request->query('redirect'), @@ -37,13 +42,16 @@ public function store(Request $request): RedirectResponse $isInviteRegistration = str_contains($request->input('redirect', ''), '/invites/'); + $utmParameters = $this->retrieveUtmParameters(); + $user = CreateUser::execute([ 'name' => $request->name, 'email' => $request->email, 'password' => $request->password, 'timezone' => $request->input('timezone', 'UTC'), 'is_invite' => $isInviteRegistration, - ]); + 'registration_ip' => $request->ip(), + ], $utmParameters); event(new Registered($user)); @@ -57,6 +65,6 @@ public function store(Request $request): RedirectResponse session()->flash('auth_provider', 'email'); - return redirect()->route('register.success'); + return redirect()->route('register.success', $utmParameters); } } diff --git a/app/Http/Middleware/App/HandleInertiaRequests.php b/app/Http/Middleware/App/HandleInertiaRequests.php index f3b13785..da9300ed 100644 --- a/app/Http/Middleware/App/HandleInertiaRequests.php +++ b/app/Http/Middleware/App/HandleInertiaRequests.php @@ -59,6 +59,7 @@ public function share(Request $request): array 'aiEnabled' => ! empty(config('services.gemini.api_key')) || ! empty(config('services.openai.api_key')), 'selfHosted' => $isSelfHosted, 'googleAuthEnabled' => config('trypost.google_auth_enabled'), + 'githubAuthEnabled' => config('trypost.github_auth_enabled'), 'trialDays' => config('cashier.trial_days'), ]; } diff --git a/app/Models/User.php b/app/Models/User.php index 5b0eb8b6..dafee99d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,9 +32,16 @@ class User extends Authenticatable implements MustVerifyEmail, OAuthenticatable 'email', 'password', 'google_id', + 'github_id', 'account_id', 'current_workspace_id', 'email_verified_at', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'registration_ip', ]; /** diff --git a/config/services.php b/config/services.php index d5ca4f63..af7517ce 100644 --- a/config/services.php +++ b/config/services.php @@ -76,6 +76,13 @@ 'redirect' => env('GOOGLE_AUTH_CALLBACK'), ], + // GitHub OAuth (used for login/signup) + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => env('GITHUB_AUTH_CALLBACK'), + ], + // Facebook Pages 'facebook' => [ 'client_id' => env('FACEBOOK_CLIENT_ID'), diff --git a/config/trypost.php b/config/trypost.php index ea202f01..9ab2e2b9 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -46,6 +46,8 @@ 'google_auth_enabled' => env('GOOGLE_AUTH_ENABLED', false), + 'github_auth_enabled' => env('GITHUB_AUTH_ENABLED', false), + /* |-------------------------------------------------------------------------- | Social Platforms diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 0a435277..61edb164 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -33,12 +33,19 @@ public function definition(): array 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'google_id' => null, + 'github_id' => null, 'remember_token' => Str::random(10), 'account_id' => Account::factory(), 'current_workspace_id' => null, 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'two_factor_confirmed_at' => null, + 'utm_source' => null, + 'utm_medium' => null, + 'utm_campaign' => null, + 'utm_term' => null, + 'utm_content' => null, + 'registration_ip' => null, ]; } diff --git a/database/migrations/2026_05_04_212145_add_utm_columns_to_users_table.php b/database/migrations/2026_05_04_212145_add_utm_columns_to_users_table.php new file mode 100644 index 00000000..7f80b90b --- /dev/null +++ b/database/migrations/2026_05_04_212145_add_utm_columns_to_users_table.php @@ -0,0 +1,28 @@ +string('utm_source')->nullable()->after('current_workspace_id'); + $table->string('utm_medium')->nullable()->after('utm_source'); + $table->string('utm_campaign')->nullable()->after('utm_medium'); + $table->string('utm_term')->nullable()->after('utm_campaign'); + $table->string('utm_content')->nullable()->after('utm_term'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn(['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']); + }); + } +}; diff --git a/database/migrations/2026_05_04_212935_add_github_id_to_users_table.php b/database/migrations/2026_05_04_212935_add_github_id_to_users_table.php new file mode 100644 index 00000000..49cb5eaa --- /dev/null +++ b/database/migrations/2026_05_04_212935_add_github_id_to_users_table.php @@ -0,0 +1,24 @@ +string('github_id')->nullable()->unique()->after('google_id'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn('github_id'); + }); + } +}; diff --git a/database/migrations/2026_05_04_212935_add_registration_ip_to_users_table.php b/database/migrations/2026_05_04_212935_add_registration_ip_to_users_table.php new file mode 100644 index 00000000..f1c0bbfc --- /dev/null +++ b/database/migrations/2026_05_04_212935_add_registration_ip_to_users_table.php @@ -0,0 +1,24 @@ +ipAddress('registration_ip')->nullable()->after('utm_content'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn('registration_ip'); + }); + } +}; diff --git a/lang/en/auth.php b/lang/en/auth.php index ec66618b..9662c88d 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -54,6 +54,9 @@ 'or_continue_with' => 'Or continue with', 'google_login' => 'Log in with Google', 'google_signup' => 'Sign up with Google', + 'github_login' => 'Log in with GitHub', + 'github_signup' => 'Sign up with GitHub', + 'github_email_unavailable' => 'Unable to retrieve your email from GitHub. Make your GitHub email public or grant the email scope, then try again.', 'signup_success' => [ 'page_title' => 'Welcome', diff --git a/lang/es/auth.php b/lang/es/auth.php index 7c947049..383fb2ba 100644 --- a/lang/es/auth.php +++ b/lang/es/auth.php @@ -42,6 +42,9 @@ 'or_continue_with' => 'O continuar con', 'google_login' => 'Iniciar sesión con Google', 'google_signup' => 'Registrarse con Google', + 'github_login' => 'Iniciar sesión con GitHub', + 'github_signup' => 'Registrarse con GitHub', + 'github_email_unavailable' => 'No fue posible obtener tu correo de GitHub. Haz tu correo público en GitHub o concede el permiso de correo y vuelve a intentar.', 'signup_success' => [ 'page_title' => 'Bienvenido', diff --git a/lang/pt-BR/auth.php b/lang/pt-BR/auth.php index 1bc72741..192cbe0b 100644 --- a/lang/pt-BR/auth.php +++ b/lang/pt-BR/auth.php @@ -54,6 +54,9 @@ 'or_continue_with' => 'Ou continue com', 'google_login' => 'Entrar com Google', 'google_signup' => 'Cadastrar com Google', + 'github_login' => 'Entrar com GitHub', + 'github_signup' => 'Cadastrar com GitHub', + 'github_email_unavailable' => 'Não foi possível obter seu e-mail do GitHub. Torne seu e-mail público ou conceda a permissão de e-mail e tente novamente.', 'signup_success' => [ 'page_title' => 'Bem-vindo', diff --git a/public/images/social/github.svg b/public/images/social/github.svg new file mode 100644 index 00000000..77e01066 --- /dev/null +++ b/public/images/social/github.svg @@ -0,0 +1,10 @@ + + + diff --git a/resources/js/components/auth/GoogleAuthButton.vue b/resources/js/components/auth/GoogleAuthButton.vue deleted file mode 100644 index 6fa14435..00000000 --- a/resources/js/components/auth/GoogleAuthButton.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/resources/js/components/auth/SocialLogin.vue b/resources/js/components/auth/SocialLogin.vue new file mode 100644 index 00000000..ca396868 --- /dev/null +++ b/resources/js/components/auth/SocialLogin.vue @@ -0,0 +1,39 @@ + + + diff --git a/resources/js/pages/auth/Login.vue b/resources/js/pages/auth/Login.vue index cc3f0f9e..6a3376f7 100644 --- a/resources/js/pages/auth/Login.vue +++ b/resources/js/pages/auth/Login.vue @@ -1,7 +1,7 @@