mirror of https://github.com/nocodb/nocodb
36 changed files with 753 additions and 350 deletions
@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup> |
||||
const { user } = useGlobal() |
||||
|
||||
const isErrored = ref(false) |
||||
const isTitleUpdating = ref(false) |
||||
const form = ref({ |
||||
title: '', |
||||
email: '', |
||||
}) |
||||
|
||||
const { updateUserProfile } = useUsers() |
||||
const formValidator = ref() |
||||
|
||||
const formRules = { |
||||
title: [ |
||||
{ required: true, message: 'Name required' }, |
||||
{ min: 2, message: 'Name must be at least 2 characters long' }, |
||||
{ max: 60, message: 'Name must be at most 60 characters long' }, |
||||
], |
||||
} |
||||
|
||||
const onSubmit = async () => { |
||||
const valid = await formValidator.value.validate() |
||||
|
||||
if (!valid) return |
||||
|
||||
if (isTitleUpdating.value) return |
||||
|
||||
isTitleUpdating.value = true |
||||
isErrored.value = false |
||||
|
||||
try { |
||||
await updateUserProfile({ attrs: { display_name: form.value.title } }) |
||||
} catch (e: any) { |
||||
console.error(e) |
||||
} finally { |
||||
isTitleUpdating.value = false |
||||
} |
||||
} |
||||
|
||||
const email = computed(() => user.value?.email) |
||||
|
||||
watch( |
||||
() => user.value?.display_name, |
||||
() => { |
||||
if (!user.value?.display_name) return |
||||
|
||||
form.value.title = user.value.display_name |
||||
form.value.email = user.value.email |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
}, |
||||
) |
||||
|
||||
const onValidate = async (_: any, valid: boolean) => { |
||||
isErrored.value = !valid |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-col items-center"> |
||||
<div class="flex flex-col w-150"> |
||||
<div class="flex font-medium text-xl">Profile</div> |
||||
<div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2"> |
||||
<div class="flex font-medium text-base">Account details</div> |
||||
<div class="flex text-gray-500">Control your appearance.</div> |
||||
<div class="flex flex-row mt-4"> |
||||
<div class="flex h-20 mt-1.5"> |
||||
<GeneralUserIcon size="xlarge" /> |
||||
</div> |
||||
<div class="flex w-10"></div> |
||||
<a-form |
||||
ref="formValidator" |
||||
layout="vertical" |
||||
no-style |
||||
:model="form" |
||||
class="flex flex-col w-full" |
||||
@finish="onSubmit" |
||||
@validate="onValidate" |
||||
> |
||||
<div class="text-gray-800 mb-1.5">Name</div> |
||||
<a-form-item name="title" :rules="formRules.title"> |
||||
<a-input |
||||
v-model:value="form.title" |
||||
class="w-full !rounded-md !py-1.5" |
||||
placeholder="Name" |
||||
data-testid="nc-account-settings-rename-input" |
||||
/> |
||||
</a-form-item> |
||||
<div class="text-gray-800 mb-1.5">Account Email ID</div> |
||||
<a-input |
||||
v-model:value="email" |
||||
class="w-full !rounded-md !py-1.5" |
||||
placeholder="Email" |
||||
disabled |
||||
data-testid="nc-account-settings-email-input" |
||||
/> |
||||
<div class="flex flex-row w-full justify-end mt-8"> |
||||
<NcButton |
||||
type="primary" |
||||
html-type="submit" |
||||
:disabled="isErrored || (form.title && form.title === user?.display_name)" |
||||
:loading="isTitleUpdating" |
||||
data-testid="nc-account-settings-save" |
||||
@click="onSubmit" |
||||
> |
||||
<template #loading> Saving </template> |
||||
Save |
||||
</NcButton> |
||||
</div> |
||||
</a-form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,57 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia' |
||||
|
||||
export const useUsers = defineStore('userStore', () => { |
||||
const { api } = useApi() |
||||
const { user } = useGlobal() |
||||
|
||||
const updateUserProfile = async ({ |
||||
attrs, |
||||
}: { |
||||
attrs: { |
||||
display_name?: string |
||||
} |
||||
}) => { |
||||
if (!user.value) throw new Error('User is not defined') |
||||
|
||||
await api.userProfile.update(attrs) |
||||
|
||||
user.value = { |
||||
...user.value, |
||||
...attrs, |
||||
} |
||||
} |
||||
|
||||
const loadCurrentUser = async () => { |
||||
const res = await api.auth.me() |
||||
|
||||
user.value = { |
||||
...user.value, |
||||
...res, |
||||
roles: res.roles, |
||||
project_roles: res.project_roles, |
||||
workspace_roles: res.workspace_roles, |
||||
} |
||||
} |
||||
|
||||
watch( |
||||
() => user.value?.id, |
||||
(newId, oldId) => { |
||||
if (!newId) return |
||||
if (newId === oldId) return |
||||
|
||||
loadCurrentUser() |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
}, |
||||
) |
||||
|
||||
return { |
||||
loadCurrentUser, |
||||
updateUserProfile, |
||||
} |
||||
}) |
||||
|
||||
if (import.meta.hot) { |
||||
import.meta.hot.accept(acceptHMRUpdate(useUsers as any, import.meta.hot)) |
||||
} |
@ -1,46 +0,0 @@
|
||||
import { |
||||
Body, |
||||
Controller, |
||||
HttpCode, |
||||
Post, |
||||
Request, |
||||
UseGuards, |
||||
} from '@nestjs/common'; |
||||
import { AuthGuard } from '@nestjs/passport'; |
||||
import { ConfigService } from '@nestjs/config'; |
||||
import type { AppConfig } from '~/interface/config'; |
||||
import { AuthService } from '~/services/auth.service'; |
||||
import { NcError } from '~/helpers/catchError'; |
||||
|
||||
export class CreateUserDto { |
||||
readonly username: string; |
||||
readonly email: string; |
||||
readonly password: string; |
||||
} |
||||
|
||||
@Controller() |
||||
export class AuthController { |
||||
constructor( |
||||
private readonly authService: AuthService, |
||||
private readonly config: ConfigService<AppConfig>, |
||||
) {} |
||||
|
||||
@UseGuards(AuthGuard('local')) |
||||
@Post('/api/v1/auth/user/signin') |
||||
@HttpCode(200) |
||||
async signin(@Request() req) { |
||||
if (this.config.get('auth', { infer: true }).disableEmailAuth) { |
||||
NcError.forbidden('Email authentication is disabled'); |
||||
} |
||||
return await this.authService.login(req.user); |
||||
} |
||||
|
||||
@Post('/api/v1/auth/user/signup') |
||||
@HttpCode(200) |
||||
async signup(@Body() createUserDto: CreateUserDto) { |
||||
if (this.config.get('auth', { infer: true }).disableEmailAuth) { |
||||
NcError.forbidden('Email authentication is disabled'); |
||||
} |
||||
return await this.authService.signup(createUserDto); |
||||
} |
||||
} |
@ -0,0 +1,260 @@
|
||||
import { |
||||
Body, |
||||
Controller, |
||||
Get, |
||||
HttpCode, |
||||
Param, |
||||
Post, |
||||
Request, |
||||
Response, |
||||
UseGuards, |
||||
} from '@nestjs/common'; |
||||
import { AuthGuard } from '@nestjs/passport'; |
||||
import { ConfigService } from '@nestjs/config'; |
||||
import { extractRolesObj } from 'nocodb-sdk'; |
||||
import * as ejs from 'ejs'; |
||||
import type { AppConfig } from '~/interface/config'; |
||||
|
||||
import { UsersService } from '~/services/users/users.service'; |
||||
import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; |
||||
import { randomTokenString, setTokenCookie } from '~/services/users/helpers'; |
||||
|
||||
import { GlobalGuard } from '~/guards/global/global.guard'; |
||||
import { NcError } from '~/helpers/catchError'; |
||||
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; |
||||
import { User } from '~/models'; |
||||
|
||||
@Controller() |
||||
export class AuthController { |
||||
constructor( |
||||
protected readonly usersService: UsersService, |
||||
protected readonly appHooksService: AppHooksService, |
||||
protected readonly config: ConfigService<AppConfig>, |
||||
) {} |
||||
|
||||
@Post([ |
||||
'/auth/user/signup', |
||||
'/api/v1/db/auth/user/signup', |
||||
'/api/v1/auth/user/signup', |
||||
]) |
||||
@HttpCode(200) |
||||
async signup(@Request() req: any, @Response() res: any): Promise<any> { |
||||
if (this.config.get('auth', { infer: true }).disableEmailAuth) { |
||||
NcError.forbidden('Email authentication is disabled'); |
||||
} |
||||
res.json( |
||||
await this.usersService.signup({ |
||||
body: req.body, |
||||
req, |
||||
res, |
||||
}), |
||||
); |
||||
} |
||||
|
||||
@Post([ |
||||
'/auth/token/refresh', |
||||
'/api/v1/db/auth/token/refresh', |
||||
'/api/v1/auth/token/refresh', |
||||
]) |
||||
@HttpCode(200) |
||||
async refreshToken(@Request() req: any, @Response() res: any): Promise<any> { |
||||
res.json( |
||||
await this.usersService.refreshToken({ |
||||
body: req.body, |
||||
req, |
||||
res, |
||||
}), |
||||
); |
||||
} |
||||
|
||||
@Post([ |
||||
'/auth/user/signin', |
||||
'/api/v1/db/auth/user/signin', |
||||
'/api/v1/auth/user/signin', |
||||
]) |
||||
@UseGuards(AuthGuard('local')) |
||||
@HttpCode(200) |
||||
async signin(@Request() req, @Response() res) { |
||||
if (this.config.get('auth', { infer: true }).disableEmailAuth) { |
||||
NcError.forbidden('Email authentication is disabled'); |
||||
} |
||||
await this.setRefreshToken({ req, res }); |
||||
res.json(await this.usersService.login(req.user)); |
||||
} |
||||
|
||||
@UseGuards(GlobalGuard) |
||||
@Post('/api/v1/auth/user/signout') |
||||
@HttpCode(200) |
||||
async signOut(@Request() req, @Response() res): Promise<any> { |
||||
if (!(req as any).isAuthenticated()) { |
||||
NcError.forbidden('Not allowed'); |
||||
} |
||||
res.json( |
||||
await this.usersService.signOut({ |
||||
req, |
||||
res, |
||||
}), |
||||
); |
||||
} |
||||
|
||||
@Post(`/auth/google/genTokenByCode`) |
||||
@HttpCode(200) |
||||
@UseGuards(AuthGuard('google')) |
||||
async googleSignin(@Request() req, @Response() res) { |
||||
await this.setRefreshToken({ req, res }); |
||||
res.json(await this.usersService.login(req.user)); |
||||
} |
||||
|
||||
@Get('/auth/google') |
||||
@UseGuards(AuthGuard('google')) |
||||
googleAuthenticate() { |
||||
// google strategy will take care the request
|
||||
} |
||||
|
||||
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) |
||||
@UseGuards(GlobalGuard) |
||||
async me(@Request() req) { |
||||
const user = { |
||||
...req.user, |
||||
roles: extractRolesObj(req.user.roles), |
||||
workspace_roles: extractRolesObj(req.user.workspace_roles), |
||||
project_roles: extractRolesObj(req.user.project_roles), |
||||
}; |
||||
return user; |
||||
} |
||||
|
||||
@Post([ |
||||
'/user/password/change', |
||||
'/api/v1/db/auth/password/change', |
||||
'/api/v1/auth/password/change', |
||||
]) |
||||
@UseGuards(GlobalGuard) |
||||
@Acl('passwordChange', { |
||||
scope: 'org', |
||||
}) |
||||
@HttpCode(200) |
||||
async passwordChange(@Request() req: any): Promise<any> { |
||||
if (!(req as any).isAuthenticated()) { |
||||
NcError.forbidden('Not allowed'); |
||||
} |
||||
|
||||
await this.usersService.passwordChange({ |
||||
user: req['user'], |
||||
req, |
||||
body: req.body, |
||||
}); |
||||
|
||||
return { msg: 'Password has been updated successfully' }; |
||||
} |
||||
|
||||
@Post([ |
||||
'/auth/password/forgot', |
||||
'/api/v1/db/auth/password/forgot', |
||||
'/api/v1/auth/password/forgot', |
||||
]) |
||||
@HttpCode(200) |
||||
async passwordForgot(@Request() req: any): Promise<any> { |
||||
await this.usersService.passwordForgot({ |
||||
siteUrl: (req as any).ncSiteUrl, |
||||
body: req.body, |
||||
req, |
||||
}); |
||||
|
||||
return { msg: 'Please check your email to reset the password' }; |
||||
} |
||||
|
||||
@Post([ |
||||
'/auth/token/validate/:tokenId', |
||||
'/api/v1/db/auth/token/validate/:tokenId', |
||||
'/api/v1/auth/token/validate/:tokenId', |
||||
]) |
||||
@HttpCode(200) |
||||
async tokenValidate(@Param('tokenId') tokenId: string): Promise<any> { |
||||
await this.usersService.tokenValidate({ |
||||
token: tokenId, |
||||
}); |
||||
return { msg: 'Token has been validated successfully' }; |
||||
} |
||||
|
||||
@Post([ |
||||
'/auth/password/reset/:tokenId', |
||||
'/api/v1/db/auth/password/reset/:tokenId', |
||||
'/api/v1/auth/password/reset/:tokenId', |
||||
]) |
||||
@HttpCode(200) |
||||
async passwordReset( |
||||
@Request() req: any, |
||||
@Param('tokenId') tokenId: string, |
||||
@Body() body: any, |
||||
): Promise<any> { |
||||
await this.usersService.passwordReset({ |
||||
token: tokenId, |
||||
body: body, |
||||
req, |
||||
}); |
||||
|
||||
return { msg: 'Password has been reset successfully' }; |
||||
} |
||||
|
||||
@Post([ |
||||
'/api/v1/db/auth/email/validate/:tokenId', |
||||
'/api/v1/auth/email/validate/:tokenId', |
||||
]) |
||||
@HttpCode(200) |
||||
async emailVerification( |
||||
@Request() req: any, |
||||
@Param('tokenId') tokenId: string, |
||||
): Promise<any> { |
||||
await this.usersService.emailVerification({ |
||||
token: tokenId, |
||||
req, |
||||
}); |
||||
|
||||
return { msg: 'Email has been verified successfully' }; |
||||
} |
||||
|
||||
@Get([ |
||||
'/api/v1/db/auth/password/reset/:tokenId', |
||||
'/auth/password/reset/:tokenId', |
||||
]) |
||||
async renderPasswordReset( |
||||
@Request() req: any, |
||||
@Response() res: any, |
||||
@Param('tokenId') tokenId: string, |
||||
): Promise<any> { |
||||
try { |
||||
res.send( |
||||
ejs.render((await import('./ui/auth/resetPassword')).default, { |
||||
ncPublicUrl: process.env.NC_PUBLIC_URL || '', |
||||
token: JSON.stringify(tokenId), |
||||
baseUrl: `/`, |
||||
}), |
||||
); |
||||
} catch (e) { |
||||
return res.status(400).json({ msg: e.message }); |
||||
} |
||||
} |
||||
|
||||
async setRefreshToken({ res, req }) { |
||||
const userId = req.user?.id; |
||||
|
||||
if (!userId) return; |
||||
|
||||
const user = await User.get(userId); |
||||
|
||||
if (!user) return; |
||||
|
||||
const refreshToken = randomTokenString(); |
||||
|
||||
if (!user['token_version']) { |
||||
user['token_version'] = randomTokenString(); |
||||
} |
||||
|
||||
await User.update(user.id, { |
||||
refresh_token: refreshToken, |
||||
email: user.email, |
||||
token_version: user['token_version'], |
||||
}); |
||||
setTokenCookie(res, refreshToken); |
||||
} |
||||
} |
@ -0,0 +1,24 @@
|
||||
import type { Knex } from 'knex'; |
||||
import { MetaTable } from '~/utils/globals'; |
||||
|
||||
const up = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.USERS, (table) => { |
||||
table.string('display_name'); |
||||
table.string('user_name'); |
||||
table.dropColumn('firstname'); |
||||
table.dropColumn('lastname'); |
||||
table.dropColumn('username'); |
||||
}); |
||||
}; |
||||
|
||||
const down = async (knex: Knex) => { |
||||
await knex.schema.alterTable(MetaTable.USERS, (table) => { |
||||
table.dropColumn('display_name'); |
||||
table.dropColumn('user_name'); |
||||
table.string('firstname'); |
||||
table.string('lastname'); |
||||
table.string('username'); |
||||
}); |
||||
}; |
||||
|
||||
export { up, down }; |
@ -0,0 +1,21 @@
|
||||
import { forwardRef, Module } from '@nestjs/common'; |
||||
import { PassportModule } from '@nestjs/passport'; |
||||
import { GoogleStrategyProvider } from '~/strategies/google.strategy/google.strategy'; |
||||
import { GlobalModule } from '~/modules/global/global.module'; |
||||
import { UsersService } from '~/services/users/users.service'; |
||||
import { AuthController } from '~/controllers/auth/auth.controller'; |
||||
import { MetasModule } from '~/modules/metas/metas.module'; |
||||
|
||||
@Module({ |
||||
imports: [ |
||||
forwardRef(() => GlobalModule), |
||||
PassportModule, |
||||
forwardRef(() => MetasModule), |
||||
], |
||||
controllers: [ |
||||
...(process.env.NC_WORKER_CONTAINER !== 'true' ? [AuthController] : []), |
||||
], |
||||
providers: [UsersService, GoogleStrategyProvider], |
||||
exports: [UsersService], |
||||
}) |
||||
export class AuthModule {} |
Loading…
Reference in new issue