Dynamic OauthDriver scope (#544)
* Dynamic OauthDriver scope * support migration for mysql * Refactor default scopes for integrations * Small UI changes * fix flet select tooltip * fix linter * Fix google token size in DB --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
89513e3b4a
commit
da0ea04475
|
|
@ -93,6 +93,7 @@ class OAuthController extends Controller
|
||||||
$oauthProvider->update([
|
$oauthProvider->update([
|
||||||
'access_token' => $socialiteUser->token,
|
'access_token' => $socialiteUser->token,
|
||||||
'refresh_token' => $socialiteUser->refreshToken,
|
'refresh_token' => $socialiteUser->refreshToken,
|
||||||
|
'scopes' => $socialiteUser->approvedScopes
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $oauthProvider->user;
|
return $oauthProvider->user;
|
||||||
|
|
@ -139,6 +140,7 @@ class OAuthController extends Controller
|
||||||
'refresh_token' => $socialiteUser->refreshToken,
|
'refresh_token' => $socialiteUser->refreshToken,
|
||||||
'name' => $socialiteUser->getName(),
|
'name' => $socialiteUser->getName(),
|
||||||
'email' => $socialiteUser->getEmail(),
|
'email' => $socialiteUser->getEmail(),
|
||||||
|
'scopes' => $socialiteUser->approvedScopes
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return $user;
|
return $user;
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ class OAuthProviderController extends Controller
|
||||||
$userId = Auth::id();
|
$userId = Auth::id();
|
||||||
cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5);
|
cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5);
|
||||||
|
|
||||||
|
// Connecting an account for integrations purposes
|
||||||
|
// Adding full scopes to the driver
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'url' => $service->getDriver()->getRedirectUrl(),
|
'url' => $service->getDriver()->fullScopes()->getRedirectUrl(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +49,7 @@ class OAuthProviderController extends Controller
|
||||||
'refresh_token' => $driverUser->refreshToken,
|
'refresh_token' => $driverUser->refreshToken,
|
||||||
'name' => $driverUser->getName(),
|
'name' => $driverUser->getName(),
|
||||||
'email' => $driverUser->getEmail(),
|
'email' => $driverUser->getEmail(),
|
||||||
|
'scopes' => $driverUser->approvedScopes
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class OAuthProviderResource extends JsonResource
|
||||||
fn () => OAuthProviderUserResource::make($this->resource->user),
|
fn () => OAuthProviderUserResource::make($this->resource->user),
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
'scopes' => $this->resource->scopes
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ use Laravel\Socialite\Contracts\User;
|
||||||
interface OAuthDriver
|
interface OAuthDriver
|
||||||
{
|
{
|
||||||
public function getRedirectUrl(): string;
|
public function getRedirectUrl(): string;
|
||||||
public function setRedirectUrl($url): self;
|
public function setRedirectUrl(string $url): self;
|
||||||
|
public function setScopes(array $scopes): self;
|
||||||
public function getUser(): User;
|
public function getUser(): User;
|
||||||
public function canCreateUser(): bool;
|
public function canCreateUser(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up all the scopes required by OpnForm for various integrations.
|
||||||
|
* This method configures the necessary permissions for the current OAuth driver.
|
||||||
|
*/
|
||||||
|
public function fullScopes(): self;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use Laravel\Socialite\Two\GoogleProvider;
|
||||||
class OAuthGoogleDriver implements OAuthDriver
|
class OAuthGoogleDriver implements OAuthDriver
|
||||||
{
|
{
|
||||||
private ?string $redirectUrl = null;
|
private ?string $redirectUrl = null;
|
||||||
|
private ?array $scopes = [];
|
||||||
|
|
||||||
protected GoogleProvider $provider;
|
protected GoogleProvider $provider;
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ class OAuthGoogleDriver implements OAuthDriver
|
||||||
public function getRedirectUrl(): string
|
public function getRedirectUrl(): string
|
||||||
{
|
{
|
||||||
return $this->provider
|
return $this->provider
|
||||||
->scopes([Sheets::DRIVE_FILE])
|
->scopes($this->scopes ?? [])
|
||||||
->stateless()
|
->stateless()
|
||||||
->redirectUrl($this->redirectUrl ?? config('services.google.redirect'))
|
->redirectUrl($this->redirectUrl ?? config('services.google.redirect'))
|
||||||
->with([
|
->with([
|
||||||
|
|
@ -46,10 +47,20 @@ class OAuthGoogleDriver implements OAuthDriver
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setRedirectUrl($url): OAuthDriver
|
public function setRedirectUrl(string $url): OAuthDriver
|
||||||
{
|
{
|
||||||
$this->redirectUrl = $url;
|
$this->redirectUrl = $url;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setScopes(array $scopes): OAuthDriver
|
||||||
|
{
|
||||||
|
$this->scopes = $scopes;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fullScopes(): OAuthDriver
|
||||||
|
{
|
||||||
|
return $this->setScopes([Sheets::DRIVE_FILE]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ class OAuthProvider extends Model
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'access_token', 'refresh_token',
|
'access_token',
|
||||||
|
'refresh_token',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts()
|
protected function casts()
|
||||||
|
|
@ -38,6 +39,7 @@ class OAuthProvider extends Model
|
||||||
return [
|
return [
|
||||||
'provider' => OAuthProviderService::class,
|
'provider' => OAuthProviderService::class,
|
||||||
'token_expires_at' => 'datetime',
|
'token_expires_at' => 'datetime',
|
||||||
|
'scopes' => 'array'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Query\Expression;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class () extends Migration {
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$driver = DB::getDriverName();
|
||||||
|
|
||||||
|
Schema::table('oauth_providers', function (Blueprint $table) use ($driver) {
|
||||||
|
if ($driver === 'mysql') {
|
||||||
|
$table->json('scopes')->default(new Expression('(JSON_OBJECT())'));
|
||||||
|
} else {
|
||||||
|
$table->json('scopes')->default('{}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('oauth_providers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('scopes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class () extends Migration {
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('oauth_providers', function (Blueprint $table) {
|
||||||
|
$table->text('access_token')->change();
|
||||||
|
$table->text('refresh_token')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('oauth_providers', function (Blueprint $table) {
|
||||||
|
$table->string('access_token')->change();
|
||||||
|
$table->string('refresh_token')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -33,6 +33,9 @@
|
||||||
theme.FlatSelectInput.spacing.vertical,
|
theme.FlatSelectInput.spacing.vertical,
|
||||||
theme.FlatSelectInput.fontSize,
|
theme.FlatSelectInput.fontSize,
|
||||||
theme.FlatSelectInput.option,
|
theme.FlatSelectInput.option,
|
||||||
|
{
|
||||||
|
'!cursor-not-allowed !bg-gray-200': disableOptions.includes(option[optionKey]),
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
@click="onSelect(option[optionKey])"
|
@click="onSelect(option[optionKey])"
|
||||||
>
|
>
|
||||||
|
|
@ -50,9 +53,15 @@
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<p class="flex-grow">
|
<UTooltip
|
||||||
{{ option[displayKey] }}
|
:text="disableOptionsTooltip"
|
||||||
</p>
|
:prevent="!disableOptions.includes(option[optionKey])"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<p class="flex-grow">
|
||||||
|
{{ option[displayKey] }}
|
||||||
|
</p>
|
||||||
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
|
|
@ -98,7 +107,9 @@ export default {
|
||||||
emitKey: {type: String, default: "value"},
|
emitKey: {type: String, default: "value"},
|
||||||
displayKey: {type: String, default: "name"},
|
displayKey: {type: String, default: "name"},
|
||||||
loading: {type: Boolean, default: false},
|
loading: {type: Boolean, default: false},
|
||||||
multiple: {type: Boolean, default: false},
|
multiple: { type: Boolean, default: false },
|
||||||
|
disableOptions: { type: Array, default: () => [] },
|
||||||
|
disableOptionsTooltip: {type: String, default: "Not allowed"},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -111,7 +122,7 @@ export default {
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {
|
||||||
onSelect(value) {
|
onSelect(value) {
|
||||||
if (this.disabled) {
|
if (this.disabled || this.disableOptions.includes(value)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,17 @@
|
||||||
:integration="props.integration"
|
:integration="props.integration"
|
||||||
:form="form"
|
:form="form"
|
||||||
>
|
>
|
||||||
<div class="my-5">
|
<div class="mb-4">
|
||||||
<select-input
|
<p class="text-gray-500 mb-4">
|
||||||
|
Adds new entry to spreadsheets on each form submission.
|
||||||
|
</p>
|
||||||
|
<FlatSelectInput
|
||||||
v-if="providers.length"
|
v-if="providers.length"
|
||||||
v-model="integrationData.oauth_id"
|
v-model="integrationData.oauth_id"
|
||||||
name="provider"
|
name="provider"
|
||||||
:options="providers"
|
:options="providers"
|
||||||
|
:disable-options="disableProviders"
|
||||||
|
disable-options-tooltip="Re-connect account to fix permissions"
|
||||||
display-key="email"
|
display-key="email"
|
||||||
option-key="id"
|
option-key="id"
|
||||||
emit-key="id"
|
emit-key="id"
|
||||||
|
|
@ -19,8 +24,8 @@
|
||||||
<template #help>
|
<template #help>
|
||||||
<InputHelp>
|
<InputHelp>
|
||||||
<span>
|
<span>
|
||||||
Add an entry to spreadsheets on each form submission.
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
|
class="text-blue-500"
|
||||||
:to="{ name: 'settings-connections' }"
|
:to="{ name: 'settings-connections' }"
|
||||||
>
|
>
|
||||||
Click here
|
Click here
|
||||||
|
|
@ -29,7 +34,7 @@
|
||||||
</span>
|
</span>
|
||||||
</InputHelp>
|
</InputHelp>
|
||||||
</template>
|
</template>
|
||||||
</select-input>
|
</FlatSelectInput>
|
||||||
|
|
||||||
<v-button
|
<v-button
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -44,6 +49,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import FlatSelectInput from '~/components/forms/FlatSelectInput.vue'
|
||||||
import IntegrationWrapper from './components/IntegrationWrapper.vue'
|
import IntegrationWrapper from './components/IntegrationWrapper.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -55,6 +61,7 @@ const props = defineProps({
|
||||||
|
|
||||||
const providersStore = useOAuthProvidersStore()
|
const providersStore = useOAuthProvidersStore()
|
||||||
const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'google'))
|
const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'google'))
|
||||||
|
const disableProviders = computed(() => providersStore.getAll.filter(provider => !provider.scopes.includes(providersStore.googleDrivePermission)).map((provider) => provider.id))
|
||||||
|
|
||||||
function connect () {
|
function connect () {
|
||||||
providersStore.connect('google', true)
|
providersStore.connect('google', true)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<small class="text-gray-600">Manage your external connections.</small>
|
<small class="text-gray-600">Manage your external connections.</small>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
label="Connect new account"
|
label="Connect account"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="providerModal = true"
|
@click="providerModal = true"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
||||||
const contentStore = useContentStore()
|
const contentStore = useContentStore()
|
||||||
const alert = useAlert()
|
const alert = useAlert()
|
||||||
|
|
||||||
|
const googleDrivePermission = 'https://www.googleapis.com/auth/drive.file'
|
||||||
|
|
||||||
const services = computed(() => {
|
const services = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -92,6 +94,7 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...contentStore,
|
...contentStore,
|
||||||
|
googleDrivePermission,
|
||||||
services,
|
services,
|
||||||
getService,
|
getService,
|
||||||
fetchOAuthProviders,
|
fetchOAuthProviders,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue