mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 11:26:59 +00:00
204 lines
7.0 KiB
Vue
204 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
const route = useRoute()
|
|
|
|
const { api } = useApi()
|
|
const { user } = useGlobal()
|
|
|
|
const { getPossibleAttachmentSrc } = useAttachment()
|
|
|
|
const clientId = computed(() => route.query.client_id as string)
|
|
const redirect_uri = computed(() => route.query.redirect_uri as string)
|
|
const responseType = computed(() => route.query.response_type as string)
|
|
const state = computed(() => route.query.state as string)
|
|
const codeChallenge = computed(() => route.query.code_challenge as string)
|
|
const resource = computed(() => route.query.resource as string)
|
|
const codeChallengeMethod = computed(() => route.query.code_challenge_method as string)
|
|
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const authorizing = ref(false)
|
|
const clientInfo = ref<any>({})
|
|
|
|
const selectedWorkspace = ref()
|
|
const selectedBase = ref()
|
|
|
|
const isValidRequest = computed(() => {
|
|
return clientId.value && redirect_uri.value && responseType.value === 'code'
|
|
})
|
|
|
|
const isSelectionValid = computed(() => {
|
|
if (isEeUI) {
|
|
return selectedWorkspace.value && selectedBase.value
|
|
} else {
|
|
return selectedBase.value
|
|
}
|
|
})
|
|
|
|
const canAuthorize = computed(() => {
|
|
return isValidRequest.value && isSelectionValid.value && !authorizing.value
|
|
})
|
|
|
|
async function loadAuthorizationRequest() {
|
|
if (!isValidRequest.value) {
|
|
error.value = 'Invalid OAuth request parameters'
|
|
loading.value = false
|
|
return
|
|
}
|
|
|
|
try {
|
|
const response = await api.oAuth.v2PublicOauthClientDetail(clientId.value)
|
|
|
|
if (!response.redirect_uris.includes(redirect_uri.value)) {
|
|
error.value = 'Invalid redirect URI'
|
|
return
|
|
}
|
|
|
|
clientInfo.value = response
|
|
} catch (err: any) {
|
|
console.error('Failed to load client info:', err)
|
|
if (err.response?.status === 404) {
|
|
error.value = 'Application not found'
|
|
} else {
|
|
error.value = 'Failed to load application information'
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function approveAuthorization() {
|
|
if (!canAuthorize.value) {
|
|
return
|
|
}
|
|
|
|
authorizing.value = true
|
|
|
|
try {
|
|
const authParams: any = {
|
|
client_id: clientId.value,
|
|
redirect_uri: redirect_uri.value,
|
|
state: state.value,
|
|
approved: true,
|
|
code_challenge: codeChallenge.value,
|
|
code_challenge_method: codeChallengeMethod.value,
|
|
base_id: selectedBase.value,
|
|
resource: resource.value,
|
|
}
|
|
|
|
if (isEeUI && selectedWorkspace.value) {
|
|
authParams.workspace_id = selectedWorkspace.value
|
|
}
|
|
|
|
const response = await api.oAuth.authorize(authParams)
|
|
|
|
window.location.href = response.redirect_url
|
|
} catch (err: any) {
|
|
console.error('Authorization failed:', err)
|
|
error.value = 'Authorization failed. Please try again.'
|
|
} finally {
|
|
authorizing.value = false
|
|
}
|
|
}
|
|
|
|
function denyAuthorization() {
|
|
const errorUrl = new URL(redirect_uri.value)
|
|
errorUrl.searchParams.set('error', 'access_denied')
|
|
errorUrl.searchParams.set('error_description', 'User denied the request')
|
|
if (state.value) {
|
|
errorUrl.searchParams.set('state', state.value)
|
|
}
|
|
|
|
window.location.href = errorUrl.toString()
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadAuthorizationRequest()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="oauth-authorize-page bg-nc-bg-gray-extralight min-h-screen flex items-center justify-center p-4">
|
|
<div v-if="loading" class="flex items-center bg-nc-bg-default flex-col gap-4">
|
|
<GeneralLoader size="xlarge" />
|
|
<p class="text-nc-content-gray-subtle2">Loading authorization request...</p>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="error"
|
|
class="rounded-xl shadow-sm bg-nc-bg-default border-1 border-nc-border-gray-medium p-8 max-w-md w-full text-center"
|
|
>
|
|
<GeneralIcon icon="alertTriangle" class="w-12 h-12 text-nc-content-red-medium mx-auto mb-4" />
|
|
<h2 class="text-xl font-semibold text-nc-content-gray-extreme mb-2">Authorization Error</h2>
|
|
<p class="text-nc-content-gray-subtle2 mb-6">{{ error }}</p>
|
|
<NcButton size="small" type="primary" @click="$router.push('/')"> Back to NocoDB </NcButton>
|
|
</div>
|
|
|
|
<div v-else class="rounded-xl bg-nc-bg-default shadow-sm border-1 border-nc-border-gray-medium p-8 max-w-lg w-full">
|
|
<div class="flex items-center justify-center flex-1 w-full">
|
|
<CellAttachmentPreviewImage
|
|
v-if="clientInfo.logo_uri"
|
|
:srcs="getPossibleAttachmentSrc(clientInfo.logo_uri)"
|
|
class="!object-contain max-h-12 max-w-12 !rounded-full"
|
|
:is-cell-preview="false"
|
|
/>
|
|
<div v-else class="w-16 h-16 rounded-full bg-primary flex items-center justify-center text-white text-xl font-bold">
|
|
{{ clientInfo.client_name?.charAt(0) || 'A' }}
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="text-xl font-bold text-center mt-4 text-nc-content-gray-extreme">
|
|
Authorize {{ clientInfo.client_name }} to access your account?
|
|
</h1>
|
|
|
|
<div class="text-nc-content-gray-muted text-center">
|
|
<span class="font-semibold"> {{ clientInfo.client_name }} </span> needs access to your account to provide you service. We
|
|
value your privacy and only request necessary permissions.
|
|
</div>
|
|
|
|
<div class="flex items-center justify-center my-6 gap-2">
|
|
<GeneralUserIcon :user="user" size="medium" />
|
|
{{ user.display_name || user.email }}
|
|
</div>
|
|
|
|
<NcDivider />
|
|
|
|
<div class="my-6">
|
|
<div class="flex gap-3 flex-col">
|
|
<NcListWorkspaceSelector v-if="isEeUI" v-model:value="selectedWorkspace" force-layout="vertical" />
|
|
|
|
<NcListBaseSelector v-model:value="selectedBase" force-layout="vertical" :workspace-id="selectedWorkspace" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-nc-bg-gray-extralight mt-4 rounded-lg p-4">
|
|
<h3 class="font-medium text-nc-content-gray-emphasis mb-3">This application will be able to:</h3>
|
|
<ul class="space-y-2">
|
|
<li class="flex items-center text-sm text-nc-content-gray-subtle">
|
|
<GeneralIcon icon="ncCheck" class="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
|
Access the selected base
|
|
</li>
|
|
<li class="flex items-center text-sm text-nc-content-gray-subtle">
|
|
<GeneralIcon icon="ncCheck" class="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
|
Read, create, update, and delete records in this base
|
|
</li>
|
|
<li class="flex items-center text-sm text-nc-content-gray-subtle">
|
|
<GeneralIcon icon="ncCheck" class="w-4 h-4 text-green-600 mr-2 flex-shrink-0" />
|
|
Act on your behalf within the selected resources
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="flex gap-3 mt-4">
|
|
<NcButton type="secondary" :disabled="authorizing" class="flex-1" @click="denyAuthorization"> Cancel </NcButton>
|
|
<NcButton :disabled="!canAuthorize" :loading="authorizing" class="flex-1" @click="approveAuthorization">
|
|
Authorize
|
|
</NcButton>
|
|
</div>
|
|
|
|
<div v-if="!isSelectionValid && !loading" class="text-sm text-red-600 text-center mt-2">
|
|
{{ isEeUI ? 'Please select both a workspace and base to continue.' : 'Please select a base to continue.' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|