diff --git a/packages/nocodb/src/app.module.ts b/packages/nocodb/src/app.module.ts index 55298ff011..6fb5683874 100644 --- a/packages/nocodb/src/app.module.ts +++ b/packages/nocodb/src/app.module.ts @@ -24,11 +24,13 @@ import { ExtractIdsMiddleware } from '~/middlewares/extract-ids/extract-ids.midd import { HookHandlerService } from '~/services/hook-handler.service'; import { BasicStrategy } from '~/strategies/basic.strategy/basic.strategy'; import { UsersModule } from '~/modules/users/users.module'; +import { AuthModule } from '~/modules/auth/auth.module'; export const ceModuleConfig = { imports: [ GlobalModule, UsersModule, + AuthModule, ...(process.env['PLAYWRIGHT_TEST'] === 'true' ? [TestModule] : []), MetasModule, DatasModule, diff --git a/packages/nocodb/src/controllers/auth.controller.spec.ts b/packages/nocodb/src/controllers/auth.controller.spec.ts index cff81811a8..575bcb49fb 100644 --- a/packages/nocodb/src/controllers/auth.controller.spec.ts +++ b/packages/nocodb/src/controllers/auth.controller.spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { AuthService } from '../services/auth.service'; -import { AuthController } from './auth.controller'; +import { AuthController } from './auth/auth.controller'; import type { TestingModule } from '@nestjs/testing'; describe('AuthController', () => { diff --git a/packages/nocodb/src/controllers/auth.controller.ts b/packages/nocodb/src/controllers/auth.controller.ts deleted file mode 100644 index 8dd2b74ece..0000000000 --- a/packages/nocodb/src/controllers/auth.controller.ts +++ /dev/null @@ -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, - ) {} - - @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); - } -} diff --git a/packages/nocodb/src/controllers/auth/auth.controller.ts b/packages/nocodb/src/controllers/auth/auth.controller.ts new file mode 100644 index 0000000000..5a1b869856 --- /dev/null +++ b/packages/nocodb/src/controllers/auth/auth.controller.ts @@ -0,0 +1,266 @@ +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'; + +export class CreateUserDto { + readonly username: string; + readonly email: string; + readonly password: string; +} + +@Controller() +export class AuthController { + constructor( + protected readonly usersService: UsersService, + protected readonly appHooksService: AppHooksService, + protected readonly config: ConfigService, + ) {} + + @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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); + } +} diff --git a/packages/nocodb/src/controllers/users/ui/auth/emailVerify.ts b/packages/nocodb/src/controllers/auth/ui/auth/emailVerify.ts similarity index 100% rename from packages/nocodb/src/controllers/users/ui/auth/emailVerify.ts rename to packages/nocodb/src/controllers/auth/ui/auth/emailVerify.ts diff --git a/packages/nocodb/src/controllers/users/ui/auth/resetPassword.ts b/packages/nocodb/src/controllers/auth/ui/auth/resetPassword.ts similarity index 100% rename from packages/nocodb/src/controllers/users/ui/auth/resetPassword.ts rename to packages/nocodb/src/controllers/auth/ui/auth/resetPassword.ts diff --git a/packages/nocodb/src/controllers/users/ui/emailTemplates/forgotPassword.ts b/packages/nocodb/src/controllers/auth/ui/emailTemplates/forgotPassword.ts similarity index 100% rename from packages/nocodb/src/controllers/users/ui/emailTemplates/forgotPassword.ts rename to packages/nocodb/src/controllers/auth/ui/emailTemplates/forgotPassword.ts diff --git a/packages/nocodb/src/controllers/users/ui/emailTemplates/invite.ts b/packages/nocodb/src/controllers/auth/ui/emailTemplates/invite.ts similarity index 100% rename from packages/nocodb/src/controllers/users/ui/emailTemplates/invite.ts rename to packages/nocodb/src/controllers/auth/ui/emailTemplates/invite.ts diff --git a/packages/nocodb/src/controllers/users/ui/emailTemplates/verify.ts b/packages/nocodb/src/controllers/auth/ui/emailTemplates/verify.ts similarity index 100% rename from packages/nocodb/src/controllers/users/ui/emailTemplates/verify.ts rename to packages/nocodb/src/controllers/auth/ui/emailTemplates/verify.ts diff --git a/packages/nocodb/src/controllers/users/users.controller.ts b/packages/nocodb/src/controllers/users/users.controller.ts index b02a462f29..71a5918b93 100644 --- a/packages/nocodb/src/controllers/users/users.controller.ts +++ b/packages/nocodb/src/controllers/users/users.controller.ts @@ -9,17 +9,11 @@ import { Response, UseGuards, } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; -import * as ejs from 'ejs'; + import { ConfigService } from '@nestjs/config'; -import { extractRolesObj } from 'nocodb-sdk'; import type { AppConfig } from '~/interface/config'; -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'; + import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; -import { randomTokenString, setTokenCookie } from '~/services/users/helpers'; import { UsersService } from '~/services/users/users.service'; @Controller() @@ -29,230 +23,4 @@ export class UsersController { protected readonly appHooksService: AppHooksService, protected readonly config: ConfigService, ) {} - - @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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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); - } } diff --git a/packages/nocodb/src/modules/auth/auth.module.ts b/packages/nocodb/src/modules/auth/auth.module.ts new file mode 100644 index 0000000000..878c22abaf --- /dev/null +++ b/packages/nocodb/src/modules/auth/auth.module.ts @@ -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 {} diff --git a/packages/nocodb/src/modules/users/users.module.ts b/packages/nocodb/src/modules/users/users.module.ts index 2213008a01..8fc5677adb 100644 --- a/packages/nocodb/src/modules/users/users.module.ts +++ b/packages/nocodb/src/modules/users/users.module.ts @@ -1,6 +1,5 @@ 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 { UsersController } from '~/controllers/users/users.controller'; @@ -15,7 +14,7 @@ import { MetasModule } from '~/modules/metas/metas.module'; controllers: [ ...(process.env.NC_WORKER_CONTAINER !== 'true' ? [UsersController] : []), ], - providers: [UsersService, GoogleStrategyProvider], + providers: [UsersService], exports: [UsersService], }) export class UsersModule {} diff --git a/packages/nocodb/src/services/auth.service.ts b/packages/nocodb/src/services/auth.service.ts index e1e4682a36..3ebfd0130f 100644 --- a/packages/nocodb/src/services/auth.service.ts +++ b/packages/nocodb/src/services/auth.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { v4 as uuidv4 } from 'uuid'; -import type { CreateUserDto } from '~/controllers/auth.controller'; +import type { CreateUserDto } from 'src/controllers/auth/auth.controller'; import Noco from '~/Noco'; import { genJwt } from '~/services/users/helpers'; import { UsersService } from '~/services/users/users.service'; diff --git a/packages/nocodb/src/services/users/users.service.ts b/packages/nocodb/src/services/users/users.service.ts index 6f30734178..3e614703c9 100644 --- a/packages/nocodb/src/services/users/users.service.ts +++ b/packages/nocodb/src/services/users/users.service.ts @@ -209,7 +209,7 @@ export class UsersService { }); try { const template = ( - await import('~/controllers/users/ui/emailTemplates/forgotPassword') + await import('~/controllers/auth/ui/emailTemplates/forgotPassword') ).default; await NcPluginMgrv2.emailAdapter().then((adapter) => adapter.mailSend({ @@ -451,7 +451,7 @@ export class UsersService { try { const template = ( - await import('~/controllers/users/ui/emailTemplates/verify') + await import('~/controllers/auth/ui/emailTemplates/verify') ).default; await ( await NcPluginMgrv2.emailAdapter()