7a137 google auth (#520)
* google oauth * fix lint * cleanup debug * Oauth changes, alert message, email validation * fix exception and inline return condition * fix google oauth * UI fixes * fix provider user --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
5049ba7fb1
commit
7ac8503201
|
|
@ -84,5 +84,6 @@ CADDY_AUTHORIZED_IPS=
|
|||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_REDIRECT_URL=http://localhost:3000/settings/connections/callback/google
|
||||
GOOGLE_AUTH_REDIRECT_URL=http://localhost:3000/oauth/google/callback
|
||||
|
||||
GOOGLE_FONTS_API_KEY=
|
||||
GOOGLE_FONTS_API_KEY=
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Exceptions\EmailTakenException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Integrations\OAuth\OAuthProviderService;
|
||||
use App\Models\OAuthProvider;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
|
|
@ -31,11 +31,11 @@ class OAuthController extends Controller
|
|||
* @param string $provider
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirect($provider)
|
||||
public function redirect(OAuthProviderService $provider)
|
||||
{
|
||||
return [
|
||||
'url' => Socialite::driver($provider)->stateless()->redirect()->getTargetUrl(),
|
||||
];
|
||||
return response()->json([
|
||||
'url' => $provider->getDriver()->setRedirectUrl(config('services.google.auth_redirect'))->getRedirectUrl()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,69 +44,103 @@ class OAuthController extends Controller
|
|||
* @param string $driver
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function handleCallback($provider)
|
||||
public function handleCallback(OAuthProviderService $provider)
|
||||
{
|
||||
$user = Socialite::driver($provider)->stateless()->user();
|
||||
$user = $this->findOrCreateUser($provider, $user);
|
||||
try {
|
||||
$driverUser = $provider->getDriver()->setRedirectUrl(config('services.google.auth_redirect'))->getUser();
|
||||
} catch (\Exception $e) {
|
||||
return $this->error([
|
||||
"message" => "OAuth service failed to authenticate: " . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
$user = $this->findOrCreateUser($provider, $driverUser);
|
||||
|
||||
if (!$user) {
|
||||
return $this->error([
|
||||
"message" => "User not found."
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->has_registered) {
|
||||
return $this->error([
|
||||
"message" => "This email is already registered. Please sign in with your password."
|
||||
]);
|
||||
}
|
||||
|
||||
$this->guard()->setToken(
|
||||
$token = $this->guard()->login($user)
|
||||
);
|
||||
|
||||
return view('oauth/callback', [
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => $this->guard()->getPayload()->get('exp') - time(),
|
||||
'new_user' => $user->new_user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $provider
|
||||
* @param \Laravel\Socialite\Contracts\User $sUser
|
||||
* @return \App\Models\User
|
||||
* @p aram \Laravel\Socialite\Contracts\User $socialiteUser
|
||||
* @return \App\Models\User | null
|
||||
*/
|
||||
protected function findOrCreateUser($provider, $user)
|
||||
protected function findOrCreateUser($provider, $socialiteUser)
|
||||
{
|
||||
$oauthProvider = OAuthProvider::where('provider', $provider)
|
||||
->where('provider_user_id', $user->getId())
|
||||
->where('provider_user_id', $socialiteUser->getId())
|
||||
->first();
|
||||
|
||||
if ($oauthProvider) {
|
||||
$oauthProvider->update([
|
||||
'access_token' => $user->token,
|
||||
'refresh_token' => $user->refreshToken,
|
||||
'access_token' => $socialiteUser->token,
|
||||
'refresh_token' => $socialiteUser->refreshToken,
|
||||
]);
|
||||
|
||||
return $oauthProvider->user;
|
||||
}
|
||||
|
||||
if (User::where('email', $user->getEmail())->exists()) {
|
||||
throw new EmailTakenException();
|
||||
|
||||
if (!$provider->getDriver()->canCreateUser()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createUser($provider, $user);
|
||||
}
|
||||
$email = strtolower($socialiteUser->getEmail());
|
||||
$user = User::whereEmail($email)->first();
|
||||
|
||||
if ($user) {
|
||||
$user->has_registered = true;
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $provider
|
||||
* @param \Laravel\Socialite\Contracts\User $sUser
|
||||
* @return \App\Models\User
|
||||
*/
|
||||
protected function createUser($provider, $sUser)
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => $sUser->getName(),
|
||||
'email' => $sUser->getEmail(),
|
||||
'name' => $socialiteUser->getName(),
|
||||
'email' => $email,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$user->oauthProviders()->create([
|
||||
'provider' => $provider,
|
||||
'provider_user_id' => $sUser->getId(),
|
||||
'access_token' => $sUser->token,
|
||||
'refresh_token' => $sUser->refreshToken,
|
||||
// Create and sync workspace
|
||||
$workspace = Workspace::create([
|
||||
'name' => 'My Workspace',
|
||||
'icon' => '🧪',
|
||||
]);
|
||||
|
||||
$user->workspaces()->sync([
|
||||
$workspace->id => [
|
||||
'role' => User::ROLE_ADMIN,
|
||||
],
|
||||
], false);
|
||||
$user->new_user = true;
|
||||
|
||||
OAuthProvider::create(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'provider' => $provider,
|
||||
'provider_user_id' => $socialiteUser->getId(),
|
||||
'access_token' => $socialiteUser->token,
|
||||
'refresh_token' => $socialiteUser->refreshToken,
|
||||
'name' => $socialiteUser->getName(),
|
||||
'email' => $socialiteUser->getEmail(),
|
||||
]
|
||||
);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class FontsController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
return \Cache::remember('google_fonts', 60 * 60, function () {
|
||||
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google_fonts_api_key');
|
||||
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google.fonts_api_key');
|
||||
$response = Http::get($url);
|
||||
if ($response->successful()) {
|
||||
$fonts = collect($response->json()['items'])->filter(function ($font) {
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@ use Laravel\Socialite\Contracts\User;
|
|||
interface OAuthDriver
|
||||
{
|
||||
public function getRedirectUrl(): string;
|
||||
public function setRedirectUrl($url): self;
|
||||
public function getUser(): User;
|
||||
public function canCreateUser(): bool;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use Laravel\Socialite\Two\GoogleProvider;
|
|||
|
||||
class OAuthGoogleDriver implements OAuthDriver
|
||||
{
|
||||
private ?string $redirectUrl = null;
|
||||
|
||||
protected GoogleProvider $provider;
|
||||
|
||||
public function __construct()
|
||||
|
|
@ -22,6 +24,7 @@ class OAuthGoogleDriver implements OAuthDriver
|
|||
return $this->provider
|
||||
->scopes([Sheets::DRIVE_FILE])
|
||||
->stateless()
|
||||
->redirectUrl($this->redirectUrl ?? config('services.google.redirect'))
|
||||
->with([
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent select_account'
|
||||
|
|
@ -34,6 +37,19 @@ class OAuthGoogleDriver implements OAuthDriver
|
|||
{
|
||||
return $this->provider
|
||||
->stateless()
|
||||
->redirectUrl($this->redirectUrl ?? config('services.google.redirect'))
|
||||
->user();
|
||||
}
|
||||
|
||||
public function canCreateUser(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setRedirectUrl($url): OAuthDriver
|
||||
{
|
||||
$this->redirectUrl = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
/>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="relative flex items-center my-5">
|
||||
<div class="relative flex items-start mt-5">
|
||||
<CheckboxInput
|
||||
v-model="remember"
|
||||
class="w-full md:w-1/2"
|
||||
|
|
@ -52,15 +52,28 @@
|
|||
|
||||
<!-- Submit Button -->
|
||||
<v-button
|
||||
dusk="btn_login"
|
||||
class="w-full flex"
|
||||
:loading="form.busy || loading"
|
||||
>
|
||||
Log in to continue
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
native-type="button"
|
||||
color="white"
|
||||
class="space-x-4 mt-4 flex items-center w-full"
|
||||
:loading="false"
|
||||
@click.prevent="signInwithGoogle"
|
||||
>
|
||||
<Icon
|
||||
name="devicon:google"
|
||||
class="w-4 h-4 -mt-1"
|
||||
/>
|
||||
<span class="mx-2">Sign in with Google</span>
|
||||
</v-button>
|
||||
<p
|
||||
v-if="!appStore.selfHosted"
|
||||
class="text-gray-500 mt-4"
|
||||
class="text-gray-500 text-sm text-center mt-4"
|
||||
>
|
||||
Don't have an account?
|
||||
<a
|
||||
|
|
@ -106,6 +119,7 @@ export default {
|
|||
authStore: useAuthStore(),
|
||||
formsStore: useFormsStore(),
|
||||
workspaceStore: useWorkspacesStore(),
|
||||
providersStore: useOAuthProvidersStore()
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -119,6 +133,8 @@ export default {
|
|||
showForgotModal: false,
|
||||
}),
|
||||
|
||||
computed: {},
|
||||
|
||||
methods: {
|
||||
login() {
|
||||
// Submit the form.
|
||||
|
|
@ -170,6 +186,9 @@ export default {
|
|||
router.push({ name: "home" })
|
||||
}
|
||||
},
|
||||
signInwithGoogle() {
|
||||
this.providersStore.guestConnect('google', true)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<form
|
||||
class="mt-4"
|
||||
@submit.prevent="register"
|
||||
@keydown="form.onKeydown($event)"
|
||||
>
|
||||
|
|
@ -56,6 +55,7 @@
|
|||
<checkbox-input
|
||||
:form="form"
|
||||
name="agree_terms"
|
||||
class="mb-3"
|
||||
:required="true"
|
||||
>
|
||||
<template #label>
|
||||
|
|
@ -63,6 +63,7 @@
|
|||
<NuxtLink
|
||||
:to="{ name: 'terms-conditions' }"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Terms and conditions
|
||||
</NuxtLink>
|
||||
|
|
@ -70,6 +71,7 @@
|
|||
<NuxtLink
|
||||
:to="{ name: 'privacy-policy' }"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Privacy policy
|
||||
</NuxtLink>
|
||||
|
|
@ -78,16 +80,33 @@
|
|||
</checkbox-input>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<v-button :loading="form.busy">
|
||||
Create an account
|
||||
<v-button
|
||||
class="w-full mt-4"
|
||||
:loading="form.busy"
|
||||
>
|
||||
Create account
|
||||
</v-button>
|
||||
|
||||
<p class="text-gray-500 mt-4">
|
||||
<p class="text-gray-600/50 text-sm text-center my-4">
|
||||
Or
|
||||
</p>
|
||||
<v-button
|
||||
native-type="buttom"
|
||||
color="white"
|
||||
class="space-x-4 flex items-center w-full"
|
||||
:loading="false"
|
||||
@click.prevent="signInwithGoogle"
|
||||
>
|
||||
<Icon name="devicon:google" />
|
||||
<span class="mx-2">Sign in with Google</span>
|
||||
</v-button>
|
||||
|
||||
<p class="text-gray-500 mt-4 text-sm text-center">
|
||||
Already have an account?
|
||||
<a
|
||||
v-if="isQuick"
|
||||
href="#"
|
||||
class="font-semibold ml-1"
|
||||
class="font-medium ml-1"
|
||||
@click.prevent="$emit('openLogin')"
|
||||
>Log In</a>
|
||||
<NuxtLink
|
||||
|
|
@ -123,6 +142,7 @@ export default {
|
|||
authStore: useAuthStore(),
|
||||
formsStore: useFormsStore(),
|
||||
workspaceStore: useWorkspacesStore(),
|
||||
providersStore: useOAuthProvidersStore(),
|
||||
logEvent: useAmplitude().logEvent,
|
||||
}
|
||||
},
|
||||
|
|
@ -241,6 +261,9 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
signInwithGoogle() {
|
||||
this.providersStore.guestConnect('google', true)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ export default defineNuxtRouteMiddleware((from, to, next) => {
|
|||
const runtimeConfig = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
if (runtimeConfig.public?.selfHosted) {
|
||||
console.log('in')
|
||||
if (from.name === 'register' && route.query?.email && route.query?.invite_token) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
<h2 class="font-semibold text-2xl">
|
||||
Login to OpnForm
|
||||
</h2>
|
||||
<small>Welcome back! Please enter your details.</small>
|
||||
<p class="text-sm text-gray-500">
|
||||
Welcome back! Please enter your details.
|
||||
</p>
|
||||
|
||||
<login-form />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<template>
|
||||
<div class="flex flex-grow mt-6 mb-10">
|
||||
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4">
|
||||
<div class="m-10" v-if="loading">
|
||||
<h3 class="my-6 text-center">
|
||||
Please wait...
|
||||
</h3>
|
||||
<Loader class="h-6 w-6 mx-auto m-10" />
|
||||
</div>
|
||||
<div class="m-6 flex flex-col items-center space-y-4" v-else>
|
||||
<p class="text-center"> Unable to sign it at the moment. </p>
|
||||
<v-button
|
||||
:to="{ name: 'login' }"
|
||||
>Back to login</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
const formsStore = useFormsStore()
|
||||
const logEvent = useAmplitude().logEvent
|
||||
const loading = ref(true);
|
||||
|
||||
definePageMeta({
|
||||
alias: [
|
||||
'/oauth/:provider/callback'
|
||||
]
|
||||
})
|
||||
|
||||
function handleCallback() {
|
||||
|
||||
const code = route.query.code
|
||||
const provider = route.params.provider
|
||||
opnFetch(`/oauth/${provider}/callback`, {
|
||||
method: 'POST',
|
||||
params: {
|
||||
code
|
||||
}
|
||||
}).then(async (data) => {
|
||||
authStore.setToken(data.token)
|
||||
const [userDataResponse, workspacesResponse] = await Promise.all([
|
||||
opnFetch("user"),
|
||||
fetchAllWorkspaces(),
|
||||
])
|
||||
authStore.setUser(userDataResponse)
|
||||
workspacesStore.set(workspacesResponse.data.value)
|
||||
|
||||
// Load forms
|
||||
formsStore.loadAll(workspacesStore.currentId)
|
||||
if (!data.new_user) {
|
||||
logEvent("login", { source: provider })
|
||||
try {
|
||||
useGtm().trackEvent({
|
||||
event: 'login',
|
||||
source: provider
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
router.push({ name: "home" })
|
||||
return
|
||||
} else {
|
||||
logEvent("register", { source: provider })
|
||||
router.push({ name: "forms-create" })
|
||||
useAlert().success("Success! You're now registered with your Google account! Welcome to OpnForm.")
|
||||
try {
|
||||
useGtm().trackEvent({
|
||||
event: 'register',
|
||||
source: provider
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
useAlert().error(error)
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
useAlert().error(error.response._data.message)
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
handleCallback()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
@ -10,7 +10,9 @@
|
|||
<h2 class="font-semibold text-2xl">
|
||||
Create an account
|
||||
</h2>
|
||||
<small>Sign up in less than 2 minutes.</small>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Sign up in less than 2 minutes.
|
||||
</p>
|
||||
<template v-if="!appStore.selfHosted || isInvited">
|
||||
<register-form />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -61,6 +61,33 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
|||
})
|
||||
}
|
||||
|
||||
const guestConnect = (service, redirect = false) => {
|
||||
contentStore.resetState()
|
||||
contentStore.startLoading()
|
||||
|
||||
const intention = new URL(window.location.href).pathname
|
||||
|
||||
opnFetch(`/oauth/connect/${service}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...redirect ? { intention } : {},
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
window.location.href = data.url
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
alert.error(error.data.message)
|
||||
} catch (e) {
|
||||
alert.error("An error occurred while connecting an account")
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
contentStore.stopLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const providers = computed(() => contentStore.getAll.value)
|
||||
|
||||
return {
|
||||
|
|
@ -69,6 +96,7 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
|||
getService,
|
||||
fetchOAuthProviders,
|
||||
providers,
|
||||
connect
|
||||
connect,
|
||||
guestConnect
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ return [
|
|||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URL'),
|
||||
'auth_redirect' => env('GOOGLE_AUTH_REDIRECT_URL'),
|
||||
|
||||
'fonts_api_key' => env('GOOGLE_FONTS_API_KEY'),
|
||||
],
|
||||
|
||||
'google_fonts_api_key' => env('GOOGLE_FONTS_API_KEY'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -269,8 +269,9 @@ Route::group(['middleware' => 'guest:api'], function () {
|
|||
Route::post('email/verify/{user}', [VerificationController::class, 'verify'])->name('verification.verify');
|
||||
Route::post('email/resend', [VerificationController::class, 'resend']);
|
||||
|
||||
Route::post('oauth/{driver}', [OAuthController::class, 'redirect']);
|
||||
Route::get('oauth/{driver}/callback', [OAuthController::class, 'handleCallback'])->name('oauth.callback');
|
||||
Route::post('oauth/{provider}', [OAuthController::class, 'redirect']);
|
||||
Route::post('oauth/connect/{provider}', [OAuthController::class, 'redirect'])->name('oauth.redirect');
|
||||
Route::post('oauth/{provider}/callback', [OAuthController::class, 'handleCallback'])->name('oauth.callback');
|
||||
});
|
||||
|
||||
Route::group(['prefix' => 'appsumo'], function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue