Browse Source

Merge pull request #3252 from nocodb/feat/google-auth

feat(gui-v2): add google auth signup option
pull/3272/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
24271f3be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/nc-gui-v2/assets/style.scss
  2. 2
      packages/nc-gui-v2/components/general/SocialCard.vue
  3. 4
      packages/nc-gui-v2/components/general/Sponsors.vue
  4. 2
      packages/nc-gui-v2/composables/useTheme/index.ts
  5. 134
      packages/nc-gui-v2/layouts/base.vue
  6. 6
      packages/nc-gui-v2/layouts/default.vue
  7. 56
      packages/nc-gui-v2/middleware/auth.global.ts
  8. 9
      packages/nc-gui-v2/nuxt.config.ts
  9. 19
      packages/nc-gui-v2/package-lock.json
  10. 1
      packages/nc-gui-v2/package.json
  11. 14
      packages/nc-gui-v2/pages/index/index.vue
  12. 12
      packages/nc-gui-v2/pages/signup/[[token]].vue

3
packages/nc-gui-v2/assets/style.scss

@ -146,7 +146,7 @@ a {
.page-leave-active,
.layout-enter-active,
.layout-leave-active {
@apply transition-opacity duration-400 ease-in-out;
@apply transition-all duration-200 ease;
}
.page-enter-from,
@ -162,7 +162,6 @@ a {
}
.slide-enter-from,
.slide-leave-to {
transform: translate(-100%, 0);
}

2
packages/nc-gui-v2/components/general/SocialCard.vue

@ -7,7 +7,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</script>
<template>
<a-card :body-style="{ padding: '0' }" class="w-[300px] shadow-sm rounded-lg">
<a-card :body-style="{ padding: '0px' }" class="w-[300px] shadow-sm !rounded-lg">
<a-list class="w-full" dense>
<a-list-item>
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb" target="_blank">

4
packages/nc-gui-v2/components/general/Sponsors.vue

@ -7,9 +7,9 @@ const { nav = false } = defineProps<Props>()
</script>
<template>
<a-card class="w-[300px] shadow-sm rounded-lg">
<a-card class="w-[300px] shadow-sm !rounded-lg">
<template #cover>
<img class="max-h-[180px] rounded-t-lg" alt="cover" src="/ants-leaf-cutter.jpeg" />
<img class="max-h-[180px] !rounded-t-lg" alt="cover" src="/ants-leaf-cutter.jpeg" />
</template>
<a-card-meta>

2
packages/nc-gui-v2/composables/useTheme/index.ts

@ -43,7 +43,7 @@ const [setup, use] = useInjectionState((config?: Partial<ThemeConfig>) => {
theme: currentTheme,
setTheme,
}
})
}, 'theme')
export const provideTheme = setup

134
packages/nc-gui-v2/layouts/base.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
import { computed, useGlobal, useRoute } from '#imports'
import { computed, navigateTo, ref, useGlobal, useNuxtApp, useProject, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal()
@ -10,85 +9,96 @@ const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>()
const logout = () => {
signOut()
navigateTo('/signin')
}
const { hooks } = useNuxtApp()
/** when page suspensions have finished, check if a sidebar element was teleported into the layout */
hooks.hook('page:finish', () => {
if (sidebar.value) {
hasSider.value = sidebar.value?.children.length > 0
}
})
</script>
<template>
<a-layout id="nc-app" has-sider>
<div id="nc-sidebar-left" />
<Transition name="slide">
<div v-show="hasSider" id="nc-sidebar-left" ref="sidebar" />
</Transition>
<a-layout class="!flex-col">
<Transition name="layout">
<a-layout-header
v-if="!route.meta.public && signedIn && !route.meta.hideHeader"
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
<a-layout-header
v-if="!route.meta.public && signedIn && !route.meta.hideHeader"
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
>
<div
v-if="route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
<div
v-if="
route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'
"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="!text-white flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<div class="!text-white flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
<div class="flex-1" />
<GeneralReleaseInfo />
<div class="flex-1" />
<GeneralShareBaseButton v-if="!isSharedBase" />
<GeneralReleaseInfo />
<a-tooltip placement="bottom" :mouse-enter-delay="1">
<template #title> Switch language</template>
<GeneralShareBaseButton v-if="!isSharedBase" />
<div class="flex pr-4 items-center text-white">
<GeneralLanguage class="cursor-pointer text-2xl hover:text-accent" />
</div>
</a-tooltip>
<a-tooltip placement="bottom" :mouse-enter-delay="1">
<template #title> Switch language</template>
<template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white" @click.prevent />
<template #overlay>
<a-menu class="!py-0 leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary"> {{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b group">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
</Transition>
<div class="flex pr-4 items-center text-white">
<GeneralLanguage class="cursor-pointer text-2xl hover:text-accent" />
</div>
</a-tooltip>
<template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white" @click.prevent />
<template #overlay>
<a-menu class="!py-0 leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary"> {{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b group">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
<a-tooltip placement="bottom">
<template #title> Switch language</template>

6
packages/nc-gui-v2/layouts/default.vue

@ -13,15 +13,15 @@ useHead({
<script lang="ts">
export default {
name: 'Default',
name: 'DefaultLayout',
}
</script>
<template>
<div class="w-full h-full">
<teleport v-if="$slots.sidebar" to="#nc-sidebar-left">
<Teleport to="#nc-sidebar-left">
<slot name="sidebar" />
</teleport>
</Teleport>
<a-layout-content>
<slot />

56
packages/nc-gui-v2/middleware/auth.global.ts

@ -1,5 +1,6 @@
import { message } from 'ant-design-vue'
import { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { useGlobal } from '#imports'
import { useApi, useGlobal } from '#imports'
/**
* Global auth middleware
@ -10,7 +11,19 @@ import { useGlobal } from '#imports'
* the user is redirected to the home page.
*
* By default, we assume that auth is required
* If not required, mark the page as `requiresAuth: false` using `definePageMeta`
* If not required, mark the page as
* ```
* definePageMeta({
* requiresAuth: false
* })
* ```
*
* If auth should be circumvented completely mark the page as public
* ```
* definePageMeta({
* public: true
* })
* ```
*
* @example
* ```
@ -20,17 +33,18 @@ import { useGlobal } from '#imports'
* })
* ```
*/
export default defineNuxtRouteMiddleware((to, from) => {
export default defineNuxtRouteMiddleware(async (to, from) => {
const state = useGlobal()
/** if public allow */
/** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */
if (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth()
/** if public allow all visitors */
if (to.meta.public) return
/** if shred base allow without validating */
/** if shared base allow without validating */
if (to.params?.projectType === 'base') return
if (to.meta.public) return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin')
@ -48,3 +62,31 @@ export default defineNuxtRouteMiddleware((to, from) => {
}
}
})
/**
* If present, try using google auth data to sign user in before navigating to the next page
*/
async function tryGoogleAuth() {
const { signIn } = useGlobal()
const { api } = useApi()
if (window.location.search && /\bscope=|\bstate=/.test(window.location.search) && /\bcode=/.test(window.location.search)) {
try {
const {
data: { token },
} = await api.instance.post(
`/auth/${window.location.search.includes('state=github') ? 'github' : 'google'}/genTokenByCode${window.location.search}`,
)
signIn(token)
} catch (e: any) {
if (e.response && e.response.data && e.response.data.msg) {
message.error({ content: e.response.data.msg })
}
}
const newURL = window.location.href.split('?')[0]
window.history.pushState('object', document.title, newURL)
}
}

9
packages/nc-gui-v2/nuxt.config.ts

@ -98,4 +98,13 @@ export default defineNuxtConfig({
dirs: ['./context', './utils', './lib'],
imports: [{ name: 'useI18n', from: 'vue-i18n' }],
},
pageTransition: {
name: 'page',
mode: 'out-in',
},
layoutTransition: {
name: 'layout',
mode: 'out-in',
},
})

19
packages/nc-gui-v2/package-lock.json generated

@ -36,6 +36,7 @@
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
@ -1041,6 +1042,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/logos": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.1.14.tgz",
"integrity": "sha512-SvSxKubQbP/7Wdb3loShUeRGv82ejkNo5gjzvJzQeauntuU4aZjDrx0mnkhFZgNYd3li/mxvzPn79Xc5SGVliw==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/lucide": {
"version": "1.1.38",
"resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.1.38.tgz",
@ -15853,6 +15863,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/logos": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.1.14.tgz",
"integrity": "sha512-SvSxKubQbP/7Wdb3loShUeRGv82ejkNo5gjzvJzQeauntuU4aZjDrx0mnkhFZgNYd3li/mxvzPn79Xc5SGVliw==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/lucide": {
"version": "1.1.38",
"resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.1.38.tgz",

1
packages/nc-gui-v2/package.json

@ -42,6 +42,7 @@
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",

14
packages/nc-gui-v2/pages/index/index.vue

@ -112,14 +112,14 @@ onMounted(() => {
</button>
<template #overlay>
<a-menu>
<a-menu class="!py-0 rounded">
<a-menu-item>
<div
v-t="['c:project:create:xcdb']"
class="nc-project-menu-item gap-4"
class="nc-project-menu-item group"
@click="navigateTo('/project/create')"
>
<MdiPlusOutline class="text-lg" />
<MdiPlusOutline class="group-hover:text-accent" />
<div>{{ $t('activity.createProject') }}</div>
</div>
@ -128,10 +128,10 @@ onMounted(() => {
<a-menu-item>
<div
v-t="['c:project:create:extdb']"
class="nc-project-menu-item gap-4"
class="nc-project-menu-item group"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="text-lg" />
<MdiDatabaseOutline class="group-hover:text-accent" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
@ -165,7 +165,7 @@ onMounted(() => {
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div
class="capitalize color-transition group-hover:text-accent !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
class="capitalize color-transition group-hover:text-primary !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
>
{{ text }}
</div>
@ -204,7 +204,7 @@ onMounted(() => {
<style scoped>
.nc-action-btn {
@apply text-gray-500 hover:(text-accent ring) active:(ring ring-accent) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
@apply text-gray-500 group-hover:text-accent active:(ring ring-accent) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}
.nc-new-project-menu {

12
packages/nc-gui-v2/pages/signup/[[token]].vue

@ -152,6 +152,18 @@ function resetError() {
</span>
</button>
<a
v-if="appInfo.googleAuthEnabled"
:href="`${api.instance.defaults.baseURL}/auth/google`"
class="submit after:(!bg-white) !text-primary border-1 border-primary !no-underline"
>
<span class="flex items-center gap-2">
<LogosGoogleGmail />
Sign up with Google
</span>
</a>
<div class="flex items-center gap-2">
<a-switch
v-model:checked="subscribe"

Loading…
Cancel
Save