Browse Source

feat: moved reset password to client side

pull/6459/head
sreehari jayaraj 1 year ago
parent
commit
853c35799a
  1. 134
      packages/nc-gui/components/account/UserList.vue
  2. 4
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  3. 138
      packages/nc-gui/pages/reset/[id].vue
  4. 2
      packages/nocodb/src/services/org-users.service.ts
  5. 1
      packages/nocodb/src/services/users/users.service.ts

134
packages/nc-gui/components/account/UserList.vue

@ -4,7 +4,10 @@ import type { OrgUserReqType, RequestParams, Roles, UserType } from 'nocodb-sdk'
import type { User } from '#imports' import type { User } from '#imports'
import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useNuxtApp } from '#imports' import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useNuxtApp } from '#imports'
const { api } = useApi() const { api, isLoading } = useApi()
// for loading screen
isLoading.value = true
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -34,7 +37,7 @@ const pagination = reactive({
position: ['bottomCenter'], position: ['bottomCenter'],
}) })
const loadUsers = async (page = currentPage.value, limit = currentLimit.value) => { const loadUsers = useDebounceFn(async (page = currentPage.value, limit = currentLimit.value) => {
currentPage.value = page currentPage.value = page
try { try {
const response: any = await api.orgUsers.list({ const response: any = await api.orgUsers.list({
@ -55,7 +58,7 @@ const loadUsers = async (page = currentPage.value, limit = currentLimit.value) =
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }, 500)
loadUsers() loadUsers()
@ -135,6 +138,7 @@ const copyPasswordResetUrl = async (user: User) => {
<template> <template>
<div data-testid="nc-super-user-list"> <div data-testid="nc-super-user-list">
<<<<<<< HEAD
<div class="max-w-[900px] mx-auto"> <div class="max-w-[900px] mx-auto">
<div class="text-xl my-4 text-left font-weight-bold">{{ $t('title.userManagement') }}</div> <div class="text-xl my-4 text-left font-weight-bold">{{ $t('title.userManagement') }}</div>
<div class="py-2 flex gap-4 items-center"> <div class="py-2 flex gap-4 items-center">
@ -306,6 +310,124 @@ const copyPasswordResetUrl = async (user: User) => {
======= =======
</span> </span>
</div> </div>
=======
<div class="max-w-195 mx-auto">
<div class="text-xl my-4 text-left font-weight-bold">User Management</div>
<div class="py-2 flex gap-4 items-center justify-between">
<a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<div class="flex gap-3 items-center justify-center">
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" />
<NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
Invite new user
</div>
</NcButton>
</div>
</div>
<div class="w-[780px] mt-5 border-1 rounded-md h-[613px]">
<div class="flex w-full bg-gray-50 border-b-1">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-10">{{ $t('labels.email') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-20">{{ $t('objects.role') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-end pl-42">Actions</span>
</div>
<div v-if="isLoading" class="flex items-center justify-center text-center h-[513px]">
<GeneralLoader size="xlarge" />
</div>
<!-- if users are empty -->
<div v-else-if="!users.length" class="flex items-center justify-center text-center h-[513px]">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</div>
<section v-else class="tbody">
<div
v-for="el of users"
:key="el.id"
data-testid="nc-token-list"
class="flex py-3 justify-around px-5 border-b-1"
:class="{
'py-4': el.roles?.includes('super'),
}"
>
<span class="text-3.5 text-start w-1/3 pl-5">
{{ el.email }}
</span>
<span class="text-3.5 text-start w-1/3 pl-18">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
v-else
v-model:value="el.roles"
class="w-[220px] nc-user-roles"
:dropdown-match-select-width="false"
@change="updateRole(el.id, el.roles as string)"
>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</a-select-option>
<a-select-option
class="nc-users-list-role-option"
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</a-select-option>
</a-select>
</span>
<span class="w-1/3 pl-43">
<div
class="flex items-center gap-2"
:class="{
'opacity-0': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">
<MdiDotsVertical
class="border-1 !text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
/>
<template #overlay>
<NcMenu>
<template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-500" />
<div>{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" />
<div>{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem>
</template>
<NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded />
Remove user
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</span>
</div>
</section>
>>>>>>> 994c6208a (feat: moved reset password to client side)
</div> </div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-7"> <div v-if="pagination.total > 10" class="flex items-center justify-center mt-7">
<a-pagination <a-pagination
@ -336,3 +458,9 @@ const copyPasswordResetUrl = async (user: User) => {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.tbody div:nth-child(10) {
border-bottom: none;
}
</style>

4
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -108,11 +108,11 @@ onMounted(async () => {
</div> </div>
</a-modal> </a-modal>
<div class="flex sm:flex-col lg:flex-row flex-wrap mt-4 gap-4"> <div class="flex flex-col md:flex-row flex-wrap mt-4 w-full gap-5 mb-10">
<a-card <a-card
v-for="(app, i) in apps" v-for="(app, i) in apps"
:key="i" :key="i"
class="sm:w-100 md:w-full lg:w-135" class="sm:w-138.2"
:class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`" :class="`relative flex overflow-x-hidden app-item-card !shadow-sm rounded-md w-full nc-app-store-card-${app.title}`"
> >
<div class="install-btn flex flex-row justify-end space-x-1"> <div class="install-btn flex flex-row justify-end space-x-1">

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

@ -0,0 +1,138 @@
<script setup lang="ts">
import type { RuleObject } from 'ant-design-vue/es/form'
import { validatePassword } from 'nocodb-sdk'
import { definePageMeta, iconMap, reactive, ref, useApi, useI18n } from '#imports'
definePageMeta({
requiresAuth: false,
})
const { api, isLoading, error } = useApi()
const { t } = useI18n()
const route = useRoute()
const navigator = useRouter()
const form = reactive({
password: '',
newPassword: '',
})
const formValidator = ref()
const formRules = {
password: [
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{
validator: (_: unknown, v: string) => {
return new Promise<boolean>((resolve, reject) => {
const { valid, hint } = validatePassword(v)
if (valid) return resolve(true)
reject(new Error(hint))
})
},
message: t('msg.error.signUpRules.passwdRequired'),
},
] as RuleObject[],
}
async function resetPassword() {
if (!formValidator.value.validate()) return
if (form.newPassword !== form.password) {
error.value = 'password does not match'
return
}
resetError()
try {
await api.auth.passwordReset(route.params.id as string, {
password: form.password,
})
navigator.push(`/#/sigin`)
} catch (e: any) {
await extractSdkResponseErrorMsg(e)
}
}
function resetError() {
if (error.value) error.value = null
}
</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
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>
<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>
</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>
</div>
</div>
</Transition>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<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="formRules.password">
<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>
</div>
</div>
</NuxtLayout>
</template>
<style lang="scss">
.signin {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
.password {
input {
@apply !border-none !m-0;
}
}
}
</style>

2
packages/nocodb/src/services/org-users.service.ts

@ -260,7 +260,7 @@ export class OrgUsersService {
return { return {
reset_password_token: token, reset_password_token: token,
reset_password_url: param.siteUrl + `/auth/password/reset/${token}`, reset_password_url: param.siteUrl + `/#/reset/${token}`,
}; };
} }

1
packages/nocodb/src/services/users/users.service.ts

@ -275,7 +275,6 @@ export class UsersService {
async passwordReset(param: { async passwordReset(param: {
body: PasswordResetReqType; body: PasswordResetReqType;
token: string; token: string;
// todo: exclude
req: any; req: any;
}): Promise<any> { }): Promise<any> {
validatePayload( validatePayload(

Loading…
Cancel
Save