Browse Source

Merge pull request #7650 from nocodb/nc-feat/sso-google

Nc feat/sso google
pull/7670/head
Pranav C 5 months ago committed by GitHub
parent
commit
6a68b0825b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      packages/nc-gui/layouts/general.vue
  2. 292
      packages/nc-gui/pages/account/index.vue
  3. 107
      packages/nc-gui/pages/forgot-password.vue
  4. 66
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId].vue
  5. 264
      packages/nc-gui/pages/profile/[[username]].vue
  6. 154
      packages/nc-gui/pages/projects/index.vue
  7. 118
      packages/nc-gui/pages/reset/[id].vue
  8. 190
      packages/nc-gui/pages/signin.vue
  9. 216
      packages/nc-gui/pages/signup/[[token]].vue
  10. 29
      tests/playwright/pages/Account/Authentication.ts
  11. 31
      tests/playwright/pages/SsoIdpPage/GoogleLoginPage.ts

8
packages/nc-gui/layouts/general.vue

@ -5,7 +5,9 @@ export default {
</script>
<template>
<NuxtLayout name="default">
<slot name="sidebar" />
</NuxtLayout>
<div>
<NuxtLayout name="default">
<slot name="sidebar" />
</NuxtLayout>
</div>
</template>

292
packages/nc-gui/pages/account/index.vue

@ -28,177 +28,179 @@ const logout = async () => {
</script>
<template>
<NuxtLayout name="empty">
<div class="mx-auto h-full">
<div class="h-full overflow-y-auto flex">
<!-- Side tabs -->
<div class="h-full bg-white nc-user-sidebar fixed">
<NcMenu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
:inline-indent="16"
class="tabs-menu h-full"
mode="inline"
>
<div
v-if="!$route.params.baseType"
v-e="['c:navbar:home']"
data-testid="nc-noco-brand-icon"
class="transition-all duration-200 px-2 mx-2 mt-1.5 cursor-pointer transform hover:bg-gray-100 my-1 nc-noco-brand-icon h-8 rounded-md min-w-60"
@click="navigateTo('/')"
>
<div class="flex flex-row gap-x-2 items-center h-8.5">
<GeneralIcon icon="arrowLeft" class="-mt-0.1" />
<div class="flex text-sm font-medium text-gray-800">{{ $t('labels.backToWorkspace') }}</div>
</div>
</div>
<div class="text-sm text-gray-600 ml-4 p-2 mt-3 gray-600 font-medium">{{ $t('labels.account') }}</div>
<NcMenuItem
key="profile"
class="item"
:class="{
active: $route.params.page === 'profile',
}"
@click="navigateTo('/account/profile')"
<div>
<NuxtLayout name="empty">
<div class="mx-auto h-full">
<div class="h-full overflow-y-auto flex">
<!-- Side tabs -->
<div class="h-full bg-white nc-user-sidebar fixed">
<NcMenu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
:inline-indent="16"
class="tabs-menu h-full"
mode="inline"
>
<div class="flex items-center space-x-2">
<GeneralIcon icon="user" class="!h-3.5 !w-3.5" />
<div class="select-none">{{ $t('labels.profile') }}</div>
</div>
</NcMenuItem>
<NcMenuItem
key="tokens"
class="item"
:class="{
active: $route.params.page === 'tokens',
}"
@click="navigateTo('/account/tokens')"
>
<div class="flex items-center space-x-2">
<component :is="iconMap.code" />
<div class="select-none">API {{ $t('title.tokens') }}</div>
<div
v-if="!$route.params.baseType"
v-e="['c:navbar:home']"
data-testid="nc-noco-brand-icon"
class="transition-all duration-200 px-2 mx-2 mt-1.5 cursor-pointer transform hover:bg-gray-100 my-1 nc-noco-brand-icon h-8 rounded-md min-w-60"
@click="navigateTo('/')"
>
<div class="flex flex-row gap-x-2 items-center h-8.5">
<GeneralIcon icon="arrowLeft" class="-mt-0.1" />
<div class="flex text-sm font-medium text-gray-800">{{ $t('labels.backToWorkspace') }}</div>
</div>
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('superAdminAppStore') && !isEeUI"
key="apps"
class="item"
:class="{
active: $route.params.page === 'apps',
}"
@click="navigateTo('/account/apps')"
>
<div class="flex items-center space-x-2">
<component :is="iconMap.appStore" />
<div class="select-none text-sm">{{ $t('title.appStore') }}</div>
</div>
</NcMenuItem>
<a-sub-menu key="users" class="!bg-white !my-0">
<template #icon>
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
</template>
<template #title>{{ $t('objects.users') }}</template>
<div class="text-sm text-gray-600 ml-4 p-2 mt-3 gray-600 font-medium">{{ $t('labels.account') }}</div>
<NcMenuItem
v-if="isUIAllowed('superAdminUserManagement') && !isEeUI"
key="list"
class="text-xs item"
key="profile"
class="item"
:class="{
active: $route.params.nestedPage === 'list',
active: $route.params.page === 'profile',
}"
@click="navigateTo('/account/users/list')"
@click="navigateTo('/account/profile')"
>
<span class="ml-4">{{ $t('title.userManagement') }}</span>
<div class="flex items-center space-x-2">
<GeneralIcon icon="user" class="!h-3.5 !w-3.5" />
<div class="select-none">{{ $t('labels.profile') }}</div>
</div>
</NcMenuItem>
<NcMenuItem
key="password-reset"
class="text-xs item"
key="tokens"
class="item"
:class="{
active: $route.params.nestedPage === 'password-reset',
active: $route.params.page === 'tokens',
}"
@click="navigateTo('/account/users/password-reset')"
@click="navigateTo('/account/tokens')"
>
<span class="ml-4">{{ $t('title.resetPasswordMenu') }}</span>
<div class="flex items-center space-x-2">
<component :is="iconMap.code" />
<div class="select-none">API {{ $t('title.tokens') }}</div>
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('superAdminAppSettings') && !isEeUI"
key="settings"
class="text-xs item"
v-if="isUIAllowed('superAdminAppStore') && !isEeUI"
key="apps"
class="item"
:class="{
active: $route.params.nestedPage === 'settings',
active: $route.params.page === 'apps',
}"
@click="navigateTo('/account/users/settings')"
@click="navigateTo('/account/apps')"
>
<span class="ml-4">{{ $t('activity.settings') }}</span>
</NcMenuItem>
</a-sub-menu>
</NcMenu>
</div>
<!-- Sub Tabs -->
<div class="flex items-center space-x-2">
<component :is="iconMap.appStore" />
<div class="flex flex-col w-full ml-65">
<div class="flex flex-row p-3 items-center h-14">
<div class="flex-1" />
<LazyGeneralReleaseInfo />
<a-tooltip v-if="!appInfo.ee" placement="bottom" :mouse-enter-delay="1">
<template #title>{{ $t('title.switchLanguage') }}</template>
<div class="select-none text-sm">{{ $t('title.appStore') }}</div>
</div>
</NcMenuItem>
<a-sub-menu key="users" class="!bg-white !my-0">
<template #icon>
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
</template>
<template #title>{{ $t('objects.users') }}</template>
<NcMenuItem
v-if="isUIAllowed('superAdminUserManagement') && !isEeUI"
key="list"
class="text-xs item"
:class="{
active: $route.params.nestedPage === 'list',
}"
@click="navigateTo('/account/users/list')"
>
<span class="ml-4">{{ $t('title.userManagement') }}</span>
</NcMenuItem>
<NcMenuItem
key="password-reset"
class="text-xs item"
:class="{
active: $route.params.nestedPage === 'password-reset',
}"
@click="navigateTo('/account/users/password-reset')"
>
<span class="ml-4">{{ $t('title.resetPasswordMenu') }}</span>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('superAdminAppSettings') && !isEeUI"
key="settings"
class="text-xs item"
:class="{
active: $route.params.nestedPage === 'settings',
}"
@click="navigateTo('/account/users/settings')"
>
<span class="ml-4">{{ $t('activity.settings') }}</span>
</NcMenuItem>
</a-sub-menu>
</NcMenu>
</div>
<div class="flex pr-4 items-center">
<LazyGeneralLanguage class="cursor-pointer text-2xl hover:text-gray-800" />
</div>
</a-tooltip>
<template v-if="signedIn">
<NcDropdown :trigger="['click']" overlay-class-name="nc-dropdown-user-accounts-menu">
<NcButton type="text" size="small">
<component
:is="iconMap.threeDotVertical"
data-testid="nc-menu-accounts"
class="md:text-lg cursor-pointer hover:text-gray-800 nc-menu-accounts"
@click.prevent
/>
</NcButton>
<template #overlay>
<div class="!py-1 !rounded-md bg-white overflow-hidden">
<div class="!rounded-b group" data-testid="nc-menu-accounts__sign-out">
<div v-e="['a:navbar:user:sign-out']" class="nc-account-dropdown-item group" @click="logout">
<component :is="iconMap.signout" class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }}
</span>
<!-- Sub Tabs -->
<div class="flex flex-col w-full ml-65">
<div class="flex flex-row p-3 items-center h-14">
<div class="flex-1" />
<LazyGeneralReleaseInfo />
<a-tooltip v-if="!appInfo.ee" placement="bottom" :mouse-enter-delay="1">
<template #title>{{ $t('title.switchLanguage') }}</template>
<div class="flex pr-4 items-center">
<LazyGeneralLanguage class="cursor-pointer text-2xl hover:text-gray-800" />
</div>
</a-tooltip>
<template v-if="signedIn">
<NcDropdown :trigger="['click']" overlay-class-name="nc-dropdown-user-accounts-menu">
<NcButton type="text" size="small">
<component
:is="iconMap.threeDotVertical"
data-testid="nc-menu-accounts"
class="md:text-lg cursor-pointer hover:text-gray-800 nc-menu-accounts"
@click.prevent
/>
</NcButton>
<template #overlay>
<div class="!py-1 !rounded-md bg-white overflow-hidden">
<div class="!rounded-b group" data-testid="nc-menu-accounts__sign-out">
<div v-e="['a:navbar:user:sign-out']" class="nc-account-dropdown-item group" @click="logout">
<component :is="iconMap.signout" class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }}
</span>
</div>
</div>
</div>
</div>
</template>
</NcDropdown>
</template>
</div>
<div
class="flex flex-col container mx-auto"
:style="{
height: 'calc(100vh - 3.5rem)',
}"
>
<div class="mt-2 h-full">
<NuxtPage />
</template>
</NcDropdown>
</template>
</div>
<div
class="flex flex-col container mx-auto"
:style="{
height: 'calc(100vh - 3.5rem)',
}"
>
<div class="mt-2 h-full">
<NuxtPage />
</div>
</div>
</div>
</div>
</div>
</div>
</NuxtLayout>
</NuxtLayout>
</div>
</template>
<style lang="scss" scoped>

107
packages/nc-gui/pages/forgot-password.vue

@ -61,61 +61,68 @@ function navigateSignIn() {
</script>
<template>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 forgot-password h-full min-h-[600px] flex flex-col justify-center items-center">
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<template v-if="!success">
<div class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</div>
<div class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</div>
</template>
<template v-else>
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</div>
<div>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 forgot-password h-full min-h-[600px] flex flex-col justify-center items-center">
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<template v-if="!success">
<div class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</div>
<div class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</div>
</template>
<template v-else>
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</div>
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</template>
</div>
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</template>
</div>
<a-form ref="formValidator" layout="vertical" :model="form" no-style @finish="resetPassword">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
<a-form ref="formValidator" layout="vertical" :model="form" no-style @finish="resetPassword">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<button class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<component :is="iconMap.signin" />
{{ $t('activity.sendEmail') }}
</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input v-model:value="form.email" size="large" :placeholder="$t('msg.info.signUp.workEmail')" @focus="resetError" />
</a-form-item>
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<button class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<component :is="iconMap.signin" />
{{ $t('activity.sendEmail') }}
</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</a-form>
</a-form>
</div>
</div>
</div>
</NuxtLayout>
</NuxtLayout>
</div>
</template>
<style lang="scss">

66
packages/nc-gui/pages/index/[typeOrId]/form/[viewId].vue

@ -62,38 +62,40 @@ watch(
</script>
<template>
<NuxtLayout>
<NuxtPage v-if="!passwordDlg" />
<a-modal
v-model:visible="passwordDlg"
:class="{ active: passwordDlg }"
:closable="false"
width="min(100%, 450px)"
centered
:footer="null"
:mask-closable="false"
wrap-class-name="nc-modal-shared-form-password-dlg"
@close="passwordDlg = false"
>
<div class="w-full flex flex-col gap-4">
<h2 class="text-xl font-semibold">{{ $t('msg.thisSharedViewIsProtected') }}</h2>
<a-form layout="vertical" no-style :model="form" @finish="loadSharedView">
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]">
<a-input-password v-model:value="form.password" size="large" :placeholder="$t('msg.enterPassword')" />
</a-form-item>
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
<!-- Unlock -->
<button type="submit" class="mt-4 scaling-btn bg-opacity-100">{{ $t('general.unlock') }}</button>
</a-form>
</div>
</a-modal>
</NuxtLayout>
<div>
<NuxtLayout>
<NuxtPage v-if="!passwordDlg" />
<a-modal
v-model:visible="passwordDlg"
:class="{ active: passwordDlg }"
:closable="false"
width="min(100%, 450px)"
centered
:footer="null"
:mask-closable="false"
wrap-class-name="nc-modal-shared-form-password-dlg"
@close="passwordDlg = false"
>
<div class="w-full flex flex-col gap-4">
<h2 class="text-xl font-semibold">{{ $t('msg.thisSharedViewIsProtected') }}</h2>
<a-form layout="vertical" no-style :model="form" @finish="loadSharedView">
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]">
<a-input-password v-model:value="form.password" size="large" :placeholder="$t('msg.enterPassword')" />
</a-form-item>
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
<!-- Unlock -->
<button type="submit" class="mt-4 scaling-btn bg-opacity-100">{{ $t('general.unlock') }}</button>
</a-form>
</div>
</a-modal>
</NuxtLayout>
</div>
</template>
<style lang="scss" scoped>

264
packages/nc-gui/pages/profile/[[username]].vue

@ -11,153 +11,157 @@ await loadProfile(route.params.username as string)
</script>
<template>
<NuxtLayout>
<a-layout class="h-[75vh] overflow-y-auto flex">
<a-layout-sider :collapsed="false" width="320" class="h-max px-5" :trigger="null" collapsible theme="light">
<div class="mx-2">
<div class="my-3">
<a-avatar v-if="profile.avatar" :size="150" :src="profile.avatar" />
<a-avatar v-else :size="150">
<template #icon>
<MdiAccount class="w-full h-full p-[15px]" />
</template>
</a-avatar>
</div>
<div class="nc-profile-display-name text-[30px] my-2 font-bold">
{{ profile.display_name }}
</div>
<div class="nc-profile-user-name text-[18px] my-2">@{{ profile.user_name }}</div>
<div>
<NuxtLayout>
<a-layout class="h-[75vh] overflow-y-auto flex">
<a-layout-sider :collapsed="false" width="320" class="h-max px-5" :trigger="null" collapsible theme="light">
<div class="mx-2">
<div class="my-3">
<a-avatar v-if="profile.avatar" :size="150" :src="profile.avatar" />
<a-avatar v-else :size="150">
<template #icon>
<MdiAccount class="w-full h-full p-[15px]" />
</template>
</a-avatar>
</div>
<div class="nc-profile-display-name text-[30px] my-2 font-bold">
{{ profile.display_name }}
</div>
<div class="nc-profile-bio text-[15px] my-4">
{{ profile.bio }}
</div>
<div class="nc-profile-user-name text-[18px] my-2">@{{ profile.user_name }}</div>
<div class="nc-profile-follower my-2">
<div class="flex items-center mr-4">
<MdiAccountSupervisorOutline class="text-lg" />
{{ profile.followerCount }} followers{{ profile.followingCount }} Following
<div class="nc-profile-bio text-[15px] my-4">
{{ profile.bio }}
</div>
</div>
<div v-if="profile.id !== user.id" class="nc-profile-follow-btn my-4">
<a-button
v-if="!isFollowing"
class="!bg-primary !border-none w-full text-center !text-white rounded"
size="large"
@click="followUser(profile.id)"
>
Follow
</a-button>
<a-button
v-else
class="!bg-primary !border-none w-full text-center !text-white rounded"
size="large"
@click="unfollowUser(profile.id)"
>
Unfollow
</a-button>
</div>
<div class="nc-profile-follower my-2">
<div class="flex items-center mr-4">
<MdiAccountSupervisorOutline class="text-lg" />
{{ profile.followerCount }} followers{{ profile.followingCount }} Following
</div>
</div>
<div class="nc-profile-edit-btn my-4">
<a-button class="!bg-primary !border-none w-full text-center !text-white rounded" size="large">
Edit Profile
</a-button>
</div>
<div v-if="profile.id !== user.id" class="nc-profile-follow-btn my-4">
<a-button
v-if="!isFollowing"
class="!bg-primary !border-none w-full text-center !text-white rounded"
size="large"
@click="followUser(profile.id)"
>
Follow
</a-button>
<a-button
v-else
class="!bg-primary !border-none w-full text-center !text-white rounded"
size="large"
@click="unfollowUser(profile.id)"
>
Unfollow
</a-button>
</div>
<div v-if="profile.location" class="nc-profile-location my-2">
<div class="flex items-center mr-4"><MdiMapMarkerOutline class="text-lg mr-2" /> {{ profile.location }}</div>
</div>
<div class="nc-profile-edit-btn my-4">
<a-button class="!bg-primary !border-none w-full text-center !text-white rounded" size="large">
Edit Profile
</a-button>
</div>
<div v-if="profile.website" class="nc-profile-website my-2">
<div class="flex items-center mr-4">
<MdiLinkVariant class="text-lg mr-2" />
<a class="!no-underline" :href="profile.website" rel="noopener noreferrer" target="_blank">{{ profile.website }}</a>
<div v-if="profile.location" class="nc-profile-location my-2">
<div class="flex items-center mr-4"><MdiMapMarkerOutline class="text-lg mr-2" /> {{ profile.location }}</div>
</div>
</div>
<div class="nc-profile-statistics my-10">
<div class="text-[20px] font-bold">Statistics</div>
<div class="mt-5">
<div v-if="profile.website" class="nc-profile-website my-2">
<div class="flex items-center mr-4">
<MdiCircleMedium class="text-lg text-[#4E2BDC]" />
<span class="text-[16px]">Database</span>
</div>
<div class="mt-2">
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Columns </a-button>
<span class="ml-2 text-[12px]">x5</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Views </a-button>
<span class="ml-2 text-[12px]">x21</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Tables </a-button>
<span class="ml-2 text-[12px]">x158</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Databases </a-button>
<span class="ml-2 text-[12px]">x9</span>
</div>
<MdiLinkVariant class="text-lg mr-2" />
<a class="!no-underline" :href="profile.website" rel="noopener noreferrer" target="_blank">{{
profile.website
}}</a>
</div>
</div>
<div class="mt-5">
<div class="flex items-center mr-4">
<MdiCircleMedium class="text-lg text-[#E9ED2A]" />
<span class="text-[16px]">Automations</span>
</div>
<div class="mt-2">
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Columns </a-button>
<span class="ml-2 text-[12px]">x5</span>
<div class="nc-profile-statistics my-10">
<div class="text-[20px] font-bold">Statistics</div>
<div class="mt-5">
<div class="flex items-center mr-4">
<MdiCircleMedium class="text-lg text-[#4E2BDC]" />
<span class="text-[16px]">Database</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Views </a-button>
<span class="ml-2 text-[12px]">x21</span>
<div class="mt-2">
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Columns </a-button>
<span class="ml-2 text-[12px]">x5</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Views </a-button>
<span class="ml-2 text-[12px]">x21</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Tables </a-button>
<span class="ml-2 text-[12px]">x158</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Databases </a-button>
<span class="ml-2 text-[12px]">x9</span>
</div>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Tables </a-button>
<span class="ml-2 text-[12px]">x158</span>
</div>
<div class="mt-5">
<div class="flex items-center mr-4">
<MdiCircleMedium class="text-lg text-[#E9ED2A]" />
<span class="text-[16px]">Automations</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Databases </a-button>
<span class="ml-2 text-[12px]">x9</span>
<div class="mt-2">
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Columns </a-button>
<span class="ml-2 text-[12px]">x5</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Views </a-button>
<span class="ml-2 text-[12px]">x21</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Tables </a-button>
<span class="ml-2 text-[12px]">x158</span>
</div>
<div class="nc-profile-statistics-badge inline-block m-1">
<a-button class="!rounded-2xl !bg-[#F2F4F7]" size="small"> Databases </a-button>
<span class="ml-2 text-[12px]">x9</span>
</div>
</div>
</div>
</div>
</div>
</div>
</a-layout-sider>
<a-layout-content class="h-max px-5 py-2 scrollbar-thumb-gray-500">
<a-tabs>
<a-tab-pane key="overview" class="w-full">
<template #tab>
<div class="flex items-center mr-4">
<MdiBookOpenBlankVariant class="text-lg mr-2" />
Overview
</div>
</template>
<LazyProfileOverview />
</a-tab-pane>
<a-tab-pane key="stars" class="w-full">
<template #tab>
<div class="flex items-center mr-4">
<MdiStarOutline class="text-lg mr-2" />
<span class="mr-2">Stars</span>
<a-badge
count="25"
:number-style="{
backgroundColor: '#B5B5B7',
color: '#FFFFFF',
}"
/>
</div>
</template>
<LazyProfileStars />
</a-tab-pane>
</a-tabs>
</a-layout-content>
</a-layout>
</NuxtLayout>
</a-layout-sider>
<a-layout-content class="h-max px-5 py-2 scrollbar-thumb-gray-500">
<a-tabs>
<a-tab-pane key="overview" class="w-full">
<template #tab>
<div class="flex items-center mr-4">
<MdiBookOpenBlankVariant class="text-lg mr-2" />
Overview
</div>
</template>
<LazyProfileOverview />
</a-tab-pane>
<a-tab-pane key="stars" class="w-full">
<template #tab>
<div class="flex items-center mr-4">
<MdiStarOutline class="text-lg mr-2" />
<span class="mr-2">Stars</span>
<a-badge
count="25"
:number-style="{
backgroundColor: '#B5B5B7',
color: '#FFFFFF',
}"
/>
</div>
</template>
<LazyProfileStars />
</a-tab-pane>
</a-tabs>
</a-layout-content>
</a-layout>
</NuxtLayout>
</div>
</template>

154
packages/nc-gui/pages/projects/index.vue

@ -57,88 +57,90 @@ const deleteProject = (base: BaseType) => {
</script>
<template>
<NuxtLayout>
<template #sidebar>
<div class="flex flex-col h-full">
<div class="flex p-4">
<v-menu class="select-none">
<template #activator="{ props }">
<div
class="color-transition hover:(bg-gray-100) mr-auto select-none flex items-center gap-2 leading-8 cursor-pointer rounded-full border-1 border-gray-300 px-5 py-2 shadow prose-lg font-semibold"
@click="props.onClick"
>
<component :is="iconMap.plus" class="text-primary text-2xl" />
{{ $t('title.newProj') }}
<div>
<NuxtLayout>
<template #sidebar>
<div class="flex flex-col h-full">
<div class="flex p-4">
<v-menu class="select-none">
<template #activator="{ props }">
<div
class="color-transition hover:(bg-gray-100) mr-auto select-none flex items-center gap-2 leading-8 cursor-pointer rounded-full border-1 border-gray-300 px-5 py-2 shadow prose-lg font-semibold"
@click="props.onClick"
>
<component :is="iconMap.plus" class="text-primary text-2xl" />
{{ $t('title.newProj') }}
</div>
</template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2">
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/base/create')"
>
<component :is="iconMap.plus" class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/base/create-external')"
>
<component :is="iconMap.database" class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</v-list>
</v-menu>
</div>
<a-menu class="pr-4 flex-1 border-0">
<a-menu-item
v-for="(option, index) in navDrawerOptions"
:key="index"
class="!rounded-r-lg"
@click="activePage = option.title"
>
<div class="flex items-center gap-4">
<component :is="option.icon" />
<span class="font-semibold">
{{ option.title }}
</span>
</div>
</template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2">
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/base/create')"
>
<component :is="iconMap.plus" class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/base/create-external')"
>
<component :is="iconMap.database" class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</v-list>
</v-menu>
</div>
</a-menu-item>
</a-menu>
<a-menu class="pr-4 flex-1 border-0">
<a-menu-item
v-for="(option, index) in navDrawerOptions"
:key="index"
class="!rounded-r-lg"
@click="activePage = option.title"
>
<div class="flex items-center gap-4">
<component :is="option.icon" />
<span class="font-semibold">
{{ option.title }}
</span>
</div>
</a-menu-item>
</a-menu>
<general-social />
<general-sponsors :nav="true" />
</div>
</template>
<general-social />
<div class="flex-1 mb-12">
<div class="flex">
<div class="flex-1 text-2xl md:text-4xl font-bold text-gray-500 p-4">
{{ activePage }}
<general-sponsors :nav="true" />
</div>
<div class="self-end flex text-4xl mb-1">
<MaterialSymbolsGridView
:class="route.name === 'index-index' ? '!text-primary' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/')"
/>
<MaterialSymbolsFormatListBulletedRounded
:class="route.name === 'index-index-list' ? '!text-primary' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/list')"
/>
</template>
<div class="flex-1 mb-12">
<div class="flex">
<div class="flex-1 text-2xl md:text-4xl font-bold text-gray-500 p-4">
{{ activePage }}
</div>
<div class="self-end flex text-4xl mb-1">
<MaterialSymbolsGridView
:class="route.name === 'index-index' ? '!text-primary' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/')"
/>
<MaterialSymbolsFormatListBulletedRounded
:class="route.name === 'index-index-list' ? '!text-primary' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/list')"
/>
</div>
</div>
</div>
<a-divider class="!mb-4 lg:(!mb-8)" />
<a-divider class="!mb-4 lg:(!mb-8)" />
<NuxtPage :bases="bases" @delete-base="deleteProject" />
</div>
<NuxtPage :bases="bases" @delete-base="deleteProject" />
</div>
<a-modal></a-modal>
</NuxtLayout>
<a-modal></a-modal>
</NuxtLayout>
</div>
</template>

118
packages/nc-gui/pages/reset/[id].vue

@ -61,72 +61,76 @@ function resetError() {
</script>
<template>
<NuxtLayout>
<div class="md:bg-primary signin bg-opacity-5 forgot-password h-full min-h-[600px] flex flex-col justify-center items-center">
<div>
<NuxtLayout>
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
class="md:bg-primary signin bg-opacity-5 forgot-password h-full min-h-[600px] flex flex-col justify-center items-center"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</div>
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</div>
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
<a-form ref="formValidator" layout="vertical" :model="form" no-style @finish="resetPassword">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
<a-form ref="formValidator" layout="vertical" :model="form" no-style @finish="resetPassword">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item
:label="$t('placeholder.password.new')"
name="password"
:rules="[{ required: true, message: t('msg.error.signUpRules.passwdRequired') }]"
>
<a-input-password
v-model:value="form.password"
:placeholder="$t('placeholder.password.new')"
class="password"
@focus="resetError"
/>
</a-form-item>
<a-form-item
:label="$t('placeholder.password.confirm')"
name="newPassword"
:rules="[{ required: true, message: t('msg.error.signUpRules.passwdRequired') }]"
>
<a-input-password
v-model:value="form.newPassword"
type="password"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<NcButton type="primary" :is-loading="isLoading" html-type="submit">
<span class="flex items-center gap-2">
<component :is="iconMap.signin" />
{{ $t('general.reset') }}
</span>
</NcButton>
</div>
</Transition>
<a-form-item
:label="$t('placeholder.password.new')"
name="password"
:rules="[{ required: true, message: t('msg.error.signUpRules.passwdRequired') }]"
>
<a-input-password
v-model:value="form.password"
:placeholder="$t('placeholder.password.new')"
class="password"
@focus="resetError"
/>
</a-form-item>
<a-form-item
:label="$t('placeholder.password.confirm')"
name="newPassword"
:rules="[{ required: true, message: t('msg.error.signUpRules.passwdRequired') }]"
>
<a-input-password
v-model:value="form.newPassword"
type="password"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<NcButton type="primary" :is-loading="isLoading" html-type="submit">
<span class="flex items-center gap-2">
<component :is="iconMap.signin" />
{{ $t('general.reset') }}
</span>
</NcButton>
</div>
</a-form>
</a-form>
</div>
</div>
</div>
</NuxtLayout>
</NuxtLayout>
</div>
</template>
<style lang="scss">

190
packages/nc-gui/pages/signin.vue

@ -92,114 +92,116 @@ function navigateForgotPassword() {
</script>
<template>
<NuxtLayout>
<div
data-testid="nc-form-signin"
class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin"
>
<div>
<NuxtLayout>
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
data-testid="nc-form-signin"
class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1>
<a-form ref="formValidator" :model="form" layout="vertical" no-style @finish="signIn">
<template v-if="!appInfo.disableEmailAuth">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
<a-form ref="formValidator" :model="form" layout="vertical" no-style @finish="signIn">
<template v-if="!appInfo.disableEmailAuth">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
data-testid="nc-form-signin__email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-testid="nc-form-signin__password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@focus="resetError"
/>
</a-form-item>
<div class="hidden md:block text-right">
<nuxt-link class="prose-sm" @click="navigateForgotPassword">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
data-testid="nc-form-signin__email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-testid="nc-form-signin__password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@focus="resetError"
/>
</a-form-item>
<div class="hidden md:block text-right">
<nuxt-link class="prose-sm" @click="navigateForgotPassword">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
</div>
</template>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<template v-if="!appInfo.disableEmailAuth">
<button data-testid="nc-form-signin__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<component :is="iconMap.signin" />
{{ $t('general.signIn') }}
</span>
</button>
</template>
<a
v-if="appInfo.googleAuthEnabled"
:href="`${appInfo.ncSiteUrl}/auth/google`"
class="scaling-btn bg-opacity-100 after:(!bg-white) !text-primary !no-underline"
>
<span class="flex items-center gap-2">
<LogosGoogleGmail />
{{ $t('labels.signInWithProvider', { provider: 'Google' }) }}
</span>
</a>
<div
v-if="appInfo.oidcAuthEnabled"
class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center"
>
<a :href="`${appInfo.ncSiteUrl}/auth/oidc`" class="!text-primary !no-underline">
<button type="button" class="scaling-btn bg-opacity-100">
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<template v-if="!appInfo.disableEmailAuth">
<button data-testid="nc-form-signin__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiLogin />
<template v-if="!appInfo.disableEmailAuth">
{{ $t('labels.signUpWithProvider', { provider: appInfo.oidcProviderName || 'OpenID Connect' }) }}
</template>
<template v-else>
{{ $t('general.signIn') }}
</template>
<component :is="iconMap.signin" />
{{ $t('general.signIn') }}
</span>
</button>
</template>
<a
v-if="appInfo.googleAuthEnabled"
:href="`${appInfo.ncSiteUrl}/auth/google`"
class="scaling-btn bg-opacity-100 after:(!bg-white) !text-primary !no-underline"
>
<span class="flex items-center gap-2">
<LogosGoogleGmail />
{{ $t('labels.signInWithProvider', { provider: 'Google' }) }}
</span>
</a>
</div>
<div v-if="!appInfo.inviteOnlySignup" class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link @click="navigateSignUp">{{ $t('general.signUp') }}</nuxt-link>
</div>
<template v-if="!appInfo.disableEmailAuth">
<div class="md:hidden">
<nuxt-link class="prose-sm" @click="navigateForgotPassword">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
<div
v-if="appInfo.oidcAuthEnabled"
class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center"
>
<a :href="`${appInfo.ncSiteUrl}/auth/oidc`" class="!text-primary !no-underline">
<button type="button" class="scaling-btn bg-opacity-100">
<span class="flex items-center gap-2">
<MdiLogin />
<template v-if="!appInfo.disableEmailAuth">
{{ $t('labels.signUpWithProvider', { provider: appInfo.oidcProviderName || 'OpenID Connect' }) }}
</template>
<template v-else>
{{ $t('general.signIn') }}
</template>
</span>
</button>
</a>
</div>
</template>
</div>
</a-form>
<div v-if="!appInfo.inviteOnlySignup" class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link @click="navigateSignUp">{{ $t('general.signUp') }}</nuxt-link>
</div>
<template v-if="!appInfo.disableEmailAuth">
<div class="md:hidden">
<nuxt-link class="prose-sm" @click="navigateForgotPassword">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
</div>
</template>
</div>
</a-form>
</div>
</div>
</div>
</NuxtLayout>
</NuxtLayout>
</div>
</template>
<style lang="scss">

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

@ -127,124 +127,126 @@ onMounted(async () => {
</script>
<template>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 signup h-full min-h-[600px] flex flex-col justify-center items-center">
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<h1 class="prose-2xl font-bold self-center my-4">
{{ $t('general.signUp') }}
{{ $route.query.redirect_to === '/referral' ? '& REFER' : '' }}
{{ $route.query.redirect_to === '/pricing' ? '& BUY' : '' }}
</h1>
<h2 v-if="appInfo.firstUser" class="prose !text-primary font-semibold self-center">
{{ $t('msg.info.signUp.superAdmin') }}
</h2>
<a-form ref="formValidator" :model="form" layout="vertical" no-style @finish="signUp">
<template v-if="!appInfo.disableEmailAuth">
<Transition name="layout">
<div
v-if="error"
class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1"
data-testid="nc-signup-error"
>
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@focus="resetError"
/>
</a-form-item>
</template>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4">
<template v-if="!appInfo.disableEmailAuth">
<button class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline />
<div>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 signup h-full min-h-[600px] flex flex-col justify-center items-center">
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
{{ $t('general.signUp') }}
</span>
</button>
<h1 class="prose-2xl font-bold self-center my-4">
{{ $t('general.signUp') }}
{{ $route.query.redirect_to === '/referral' ? '& REFER' : '' }}
{{ $route.query.redirect_to === '/pricing' ? '& BUY' : '' }}
</h1>
<h2 v-if="appInfo.firstUser" class="prose !text-primary font-semibold self-center">
{{ $t('msg.info.signUp.superAdmin') }}
</h2>
<a-form ref="formValidator" :model="form" layout="vertical" no-style @finish="signUp">
<template v-if="!appInfo.disableEmailAuth">
<Transition name="layout">
<div
v-if="error"
class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1"
data-testid="nc-signup-error"
>
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@focus="resetError"
/>
</a-form-item>
</template>
<a
v-if="appInfo.googleAuthEnabled"
:href="`${appInfo.ncSiteUrl}/auth/google`"
class="scaling-btn bg-opacity-100 after:(!bg-white) !text-primary !no-underline"
>
<span class="flex items-center gap-2">
<LogosGoogleGmail />
{{ $t('labels.signUpWithProvider', { provider: 'Google' }) }}
</span>
</a>
<div
v-if="appInfo.oidcAuthEnabled"
class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center"
>
<a :href="`${appInfo.ncSiteUrl}/auth/oidc`" class="!text-primary !no-underline">
<button type="button" class="scaling-btn bg-opacity-100">
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4">
<template v-if="!appInfo.disableEmailAuth">
<button class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiLogin />
<template v-if="!appInfo.disableEmailAuth">
{{ $t('labels.signUpWithProvider', { provider: appInfo.oidcProviderName || 'OpenID Connect' }) }}
</template>
<template v-else>
{{ $t('general.signUp') }}
</template>
<MaterialSymbolsRocketLaunchOutline />
{{ $t('general.signUp') }}
</span>
</button>
</template>
<a
v-if="appInfo.googleAuthEnabled"
:href="`${appInfo.ncSiteUrl}/auth/google`"
class="scaling-btn bg-opacity-100 after:(!bg-white) !text-primary !no-underline"
>
<span class="flex items-center gap-2">
<LogosGoogleGmail />
{{ $t('labels.signUpWithProvider', { provider: 'Google' }) }}
</span>
</a>
</div>
<div v-if="!appInfo.disableEmailAuth" class="flex items-center gap-2">
<a-switch
v-model:checked="subscribe"
size="small"
class="my-1 hover:(ring ring-accent ring-opacity-100) focus:(!ring !ring-accent ring-opacity-100)"
/>
<div class="prose-xs text-gray-500">{{ $t('msg.subscribeToOurWeeklyNewsletter') }}</div>
</div>
<div
v-if="appInfo.oidcAuthEnabled"
class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center"
>
<a :href="`${appInfo.ncSiteUrl}/auth/oidc`" class="!text-primary !no-underline">
<button type="button" class="scaling-btn bg-opacity-100">
<span class="flex items-center gap-2">
<MdiLogin />
<template v-if="!appInfo.disableEmailAuth">
{{ $t('labels.signUpWithProvider', { provider: appInfo.oidcProviderName || 'OpenID Connect' }) }}
</template>
<template v-else>
{{ $t('general.signUp') }}
</template>
</span>
</button>
</a>
</div>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<div v-if="!appInfo.disableEmailAuth" class="flex items-center gap-2">
<a-switch
v-model:checked="subscribe"
size="small"
class="my-1 hover:(ring ring-accent ring-opacity-100) focus:(!ring !ring-accent ring-opacity-100)"
/>
<div class="prose-xs text-gray-500">{{ $t('msg.subscribeToOurWeeklyNewsletter') }}</div>
</div>
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</a-form>
</div>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<div class="prose-sm mt-4 text-gray-500">
{{ $t('msg.bySigningUp') }}
<a class="prose-sm !text-gray-500 underline" target="_blank" href="https://nocodb.com/policy-nocodb" rel="noopener">
{{ $t('title.termsOfService') }}</a
>
<nuxt-link @click="navigateSignIn">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</a-form>
</div>
<div class="prose-sm mt-4 text-gray-500">
{{ $t('msg.bySigningUp') }}
<a class="prose-sm !text-gray-500 underline" target="_blank" href="https://nocodb.com/policy-nocodb" rel="noopener">
{{ $t('title.termsOfService') }}</a
>
</div>
</div>
</div>
</NuxtLayout>
</NuxtLayout>
</div>
</template>
<style lang="scss">

29
tests/playwright/pages/Account/Authentication.ts

@ -35,8 +35,10 @@ export class AccountAuthenticationPage extends BasePage {
return this.rootPage.locator(`[data-test-id="nc-${provider}-provider-${title}"]`);
}
async deleteProvider(provider: 'saml' | 'oidc', title: string) {
await this.rootPage.locator(`.nc-${provider}-${title}-more-option`).click();
async deleteProvider(provider: 'saml' | 'oidc' | 'google', title: string) {
await this.rootPage
.locator(provider === 'google' ? '.nc-google-more-option' : `.nc-${provider}-${title}-more-option`)
.click();
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`[data-test-id="nc-${provider}-delete"]`).click(),
httpMethodsToMatch: ['DELETE'],
@ -44,7 +46,7 @@ export class AccountAuthenticationPage extends BasePage {
});
}
async toggleProvider(provider: 'saml' | 'oidc', title: string) {
async toggleProvider(provider: 'saml' | 'oidc' | 'google', title: string) {
await this.waitForResponse({
uiAction: () => this.get().locator(`.nc-${provider}-${title}-enable .nc-switch`).click(),
httpMethodsToMatch: ['PATCH'],
@ -159,4 +161,25 @@ export class AccountAuthenticationPage extends BasePage {
requestUrlPathToMatch: '/api/v2/sso-client',
});
}
async createGoogleProvider(p: { clientId: string; clientSecret: string }) {
await this.rootPage.locator(`.nc-google-more-option`).click();
await this.rootPage.locator(`[data-test-id="nc-google-edit"]`).click();
const googleModal = this.accountPage.rootPage.locator('.nc-google-modal');
// wait until redirect url is generated
await googleModal.locator('[data-test-id="nc-google-redirect-url"]:has-text("http://")').waitFor();
await googleModal.locator('[data-test-id="nc-google-client-id"]').fill(p.clientId);
await googleModal.locator('[data-test-id="nc-google-client-secret"]').fill(p.clientSecret);
await this.waitForResponse({
uiAction: () => googleModal.locator('[data-test-id="nc-google-save-btn"]').click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: '/api/v2/sso-client',
});
}
async verifyGoogleProviderCount(param: { count: number }) {}
}

31
tests/playwright/pages/SsoIdpPage/GoogleLoginPage.ts

@ -0,0 +1,31 @@
import { Page } from '@playwright/test';
import BasePage from '../Base';
import { ProjectsPage } from '../ProjectsPage';
import { expect } from '@playwright/test';
export class GoogleLoginPage extends BasePage {
readonly projectsPage: ProjectsPage;
constructor(rootPage: Page) {
super(rootPage);
this.projectsPage = new ProjectsPage(rootPage);
}
async goto(title = 'test') {
// reload page to get latest app info
await this.rootPage.reload({ waitUntil: 'networkidle' });
// click sign in with SAML
await this.rootPage.locator(`a:has-text("Sign in with google")`).click();
await this.rootPage.waitForNavigation({ url: /accounts\.google\.com/ });
}
get() {
return this.rootPage.locator('html');
}
async signIn(_: { email: string }) {
// skipping for now as it requires google account
// todo: later we can mock backend(google oauth2 endpoint calls) to test this
}
}
Loading…
Cancel
Save