Skip to content

Commit 6067b25

Browse files
authored
feat: setting to force oauth2/oidc login & refactor (#1131)
* feat: setting to force oauth2/oidc login & refactor * feat: setting to force oauth2/oidc login
1 parent 168129f commit 6067b25

File tree

16 files changed

+417
-338
lines changed

16 files changed

+417
-338
lines changed

apps/nestjs-backend/src/configs/env.validation.schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ export const envValidationSchema = Joi.object({
5656
'The `BACKEND_GITHUB_CLIENT_SECRET` is required when `SOCIAL_AUTH_PROVIDERS` includes `github`',
5757
}),
5858
}),
59+
60+
PASSWORD_LOGIN_DISABLED: Joi.string().equal('true').optional(),
5961
});
Lines changed: 8 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,25 @@
1-
import { Body, Controller, Get, HttpCode, Patch, Post, Req, Res, UseGuards } from '@nestjs/common';
1+
import { Controller, Get, HttpCode, Post, Req, Res } from '@nestjs/common';
22
import type { IUserMeVo } from '@teable/openapi';
3-
import {
4-
IAddPasswordRo,
5-
IChangePasswordRo,
6-
IResetPasswordRo,
7-
ISendResetPasswordEmailRo,
8-
ISignup,
9-
addPasswordRoSchema,
10-
changePasswordRoSchema,
11-
resetPasswordRoSchema,
12-
sendResetPasswordEmailRoSchema,
13-
signupSchema,
14-
} from '@teable/openapi';
15-
import { Response, Request } from 'express';
3+
import { Response } from 'express';
164
import { AUTH_SESSION_COOKIE_NAME } from '../../const';
17-
import { ZodValidationPipe } from '../../zod.validation.pipe';
185
import { AuthService } from './auth.service';
19-
import { Public } from './decorators/public.decorator';
206
import { TokenAccess } from './decorators/token.decorator';
21-
import { LocalAuthGuard } from './guard/local-auth.guard';
22-
import { pickUserMe } from './utils';
7+
import { SessionService } from './session/session.service';
238

249
@Controller('api/auth')
2510
export class AuthController {
26-
constructor(private readonly authService: AuthService) {}
27-
28-
@Public()
29-
@UseGuards(LocalAuthGuard)
30-
@HttpCode(200)
31-
@Post('signin')
32-
async signin(@Req() req: Express.Request): Promise<IUserMeVo> {
33-
return req.user as IUserMeVo;
34-
}
11+
constructor(
12+
private readonly authService: AuthService,
13+
private readonly sessionService: SessionService
14+
) {}
3515

3616
@Post('signout')
3717
@HttpCode(200)
3818
async signout(@Req() req: Express.Request, @Res({ passthrough: true }) res: Response) {
39-
await this.authService.signout(req);
19+
await this.sessionService.signout(req);
4020
res.clearCookie(AUTH_SESSION_COOKIE_NAME);
4121
}
4222

43-
@Public()
44-
@Post('signup')
45-
async signup(
46-
@Body(new ZodValidationPipe(signupSchema)) body: ISignup,
47-
@Res({ passthrough: true }) res: Response,
48-
@Req() req: Express.Request
49-
): Promise<IUserMeVo> {
50-
const user = pickUserMe(
51-
await this.authService.signup(body.email, body.password, body.defaultSpaceName, body.refMeta)
52-
);
53-
// set cookie, passport login
54-
await new Promise<void>((resolve, reject) => {
55-
req.login(user, (err) => (err ? reject(err) : resolve()));
56-
});
57-
return user;
58-
}
59-
6023
@Get('/user/me')
6124
async me(@Req() request: Express.Request) {
6225
return request.user;
@@ -67,42 +30,4 @@ export class AuthController {
6730
async user(@Req() request: Express.Request) {
6831
return this.authService.getUserInfo(request.user as IUserMeVo);
6932
}
70-
71-
@Patch('/change-password')
72-
async changePassword(
73-
@Body(new ZodValidationPipe(changePasswordRoSchema)) changePasswordRo: IChangePasswordRo,
74-
@Req() req: Request,
75-
@Res({ passthrough: true }) res: Response
76-
) {
77-
await this.authService.changePassword(changePasswordRo);
78-
await this.authService.signout(req);
79-
res.clearCookie(AUTH_SESSION_COOKIE_NAME);
80-
}
81-
82-
@Post('/send-reset-password-email')
83-
@Public()
84-
async sendResetPasswordEmail(
85-
@Body(new ZodValidationPipe(sendResetPasswordEmailRoSchema)) body: ISendResetPasswordEmailRo
86-
) {
87-
return this.authService.sendResetPasswordEmail(body.email);
88-
}
89-
90-
@Post('/reset-password')
91-
@Public()
92-
async resetPassword(
93-
@Res({ passthrough: true }) res: Response,
94-
@Body(new ZodValidationPipe(resetPasswordRoSchema)) body: IResetPasswordRo
95-
) {
96-
await this.authService.resetPassword(body.code, body.password);
97-
res.clearCookie(AUTH_SESSION_COOKIE_NAME);
98-
}
99-
100-
@Post('/add-password')
101-
async addPassword(
102-
@Res({ passthrough: true }) res: Response,
103-
@Body(new ZodValidationPipe(addPasswordRoSchema)) body: IAddPasswordRo
104-
) {
105-
await this.authService.addPassword(body.password);
106-
res.clearCookie(AUTH_SESSION_COOKIE_NAME);
107-
}
10833
}

apps/nestjs-backend/src/features/auth/auth.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { Module } from '@nestjs/common';
2+
import { ConditionalModule } from '@nestjs/config';
23
import { PassportModule } from '@nestjs/passport';
34
import { AccessTokenModule } from '../access-token/access-token.module';
45
import { UserModule } from '../user/user.module';
56
import { AuthController } from './auth.controller';
67
import { AuthService } from './auth.service';
78
import { AuthGuard } from './guard/auth.guard';
9+
import { LocalAuthModule } from './local-auth/local-auth.module';
810
import { PermissionModule } from './permission.module';
911
import { SessionStoreService } from './session/session-store.service';
1012
import { SessionModule } from './session/session.module';
1113
import { SessionSerializer } from './session/session.serializer';
1214
import { SocialModule } from './social/social.module';
1315
import { AccessTokenStrategy } from './strategies/access-token.strategy';
14-
import { LocalStrategy } from './strategies/local.strategy';
1516
import { SessionStrategy } from './strategies/session.strategy';
1617

1718
@Module({
@@ -20,12 +21,14 @@ import { SessionStrategy } from './strategies/session.strategy';
2021
PassportModule.register({ session: true }),
2122
SessionModule,
2223
AccessTokenModule,
24+
ConditionalModule.registerWhen(LocalAuthModule, (env) => {
25+
return Boolean(env.PASSWORD_LOGIN_DISABLED !== 'true');
26+
}),
2327
SocialModule,
2428
PermissionModule,
2529
],
2630
providers: [
2731
AuthService,
28-
LocalStrategy,
2932
SessionStrategy,
3033
AuthGuard,
3134
SessionSerializer,

apps/nestjs-backend/src/features/auth/auth.service.ts

Lines changed: 4 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,17 @@
1-
import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
2-
import { generateUserId, getRandomString } from '@teable/core';
3-
import { PrismaService } from '@teable/db-main-prisma';
4-
import type { IChangePasswordRo, IRefMeta, IUserInfoVo, IUserMeVo } from '@teable/openapi';
5-
import * as bcrypt from 'bcrypt';
6-
import { isEmpty, omit, pick } from 'lodash';
1+
import { Injectable } from '@nestjs/common';
2+
import type { IUserInfoVo, IUserMeVo } from '@teable/openapi';
3+
import { omit, pick } from 'lodash';
74
import { ClsService } from 'nestjs-cls';
8-
import { CacheService } from '../../cache/cache.service';
9-
import { AuthConfig, type IAuthConfig } from '../../configs/auth.config';
10-
import { MailConfig, type IMailConfig } from '../../configs/mail.config';
11-
import { EventEmitterService } from '../../event-emitter/event-emitter.service';
12-
import { Events } from '../../event-emitter/events';
13-
import { UserSignUpEvent } from '../../event-emitter/events/user/user.event';
145
import type { IClsStore } from '../../types/cls';
15-
import { second } from '../../utils/second';
16-
import { MailSenderService } from '../mail-sender/mail-sender.service';
17-
import { UserService } from '../user/user.service';
186
import { PermissionService } from './permission.service';
19-
import { SessionStoreService } from './session/session-store.service';
207

218
@Injectable()
229
export class AuthService {
2310
constructor(
24-
private readonly prismaService: PrismaService,
25-
private readonly userService: UserService,
2611
private readonly cls: ClsService<IClsStore>,
27-
private readonly sessionStoreService: SessionStoreService,
28-
private readonly mailSenderService: MailSenderService,
29-
private readonly cacheService: CacheService,
30-
private readonly permissionService: PermissionService,
31-
private readonly eventEmitterService: EventEmitterService,
32-
@AuthConfig() private readonly authConfig: IAuthConfig,
33-
@MailConfig() private readonly mailConfig: IMailConfig
12+
private readonly permissionService: PermissionService
3413
) {}
3514

36-
private async encodePassword(password: string) {
37-
const salt = await bcrypt.genSalt(10);
38-
const hashPassword = await bcrypt.hash(password, salt);
39-
return { salt, hashPassword };
40-
}
41-
42-
private async comparePassword(
43-
password: string,
44-
hashPassword: string | null,
45-
salt: string | null
46-
) {
47-
const _hashPassword = await bcrypt.hash(password || '', salt || '');
48-
return _hashPassword === hashPassword;
49-
}
50-
51-
private async getUserByIdOrThrow(userId: string) {
52-
const user = await this.userService.getUserById(userId);
53-
if (!user) {
54-
throw new BadRequestException('User not found');
55-
}
56-
return user;
57-
}
58-
59-
async validateUserByEmail(email: string, pass: string) {
60-
const user = await this.userService.getUserByEmail(email);
61-
if (!user || (user.accounts.length === 0 && user.password == null)) {
62-
throw new BadRequestException(`${email} not registered`);
63-
}
64-
65-
if (!user.password) {
66-
throw new BadRequestException('Password is not set');
67-
}
68-
69-
if (user.isSystem) {
70-
throw new BadRequestException('User is system user');
71-
}
72-
73-
const { password, salt, ...result } = user;
74-
return (await this.comparePassword(pass, password, salt)) ? { ...result, password } : null;
75-
}
76-
77-
async signup(email: string, password: string, defaultSpaceName?: string, refMeta?: IRefMeta) {
78-
const user = await this.userService.getUserByEmail(email);
79-
if (user && (user.password !== null || user.accounts.length > 0)) {
80-
throw new HttpException(`User ${email} is already registered`, HttpStatus.BAD_REQUEST);
81-
}
82-
if (user && user.isSystem) {
83-
throw new HttpException(`User ${email} is system user`, HttpStatus.BAD_REQUEST);
84-
}
85-
const { salt, hashPassword } = await this.encodePassword(password);
86-
const res = await this.prismaService.$tx(async () => {
87-
if (user) {
88-
return await this.prismaService.user.update({
89-
where: { id: user.id, deletedTime: null },
90-
data: {
91-
salt,
92-
password: hashPassword,
93-
lastSignTime: new Date().toISOString(),
94-
refMeta: refMeta ? JSON.stringify(refMeta) : undefined,
95-
},
96-
});
97-
}
98-
return await this.userService.createUserWithSettingCheck(
99-
{
100-
id: generateUserId(),
101-
name: email.split('@')[0],
102-
email,
103-
salt,
104-
password: hashPassword,
105-
lastSignTime: new Date().toISOString(),
106-
refMeta: isEmpty(refMeta) ? undefined : JSON.stringify(refMeta),
107-
},
108-
undefined,
109-
defaultSpaceName
110-
);
111-
});
112-
this.eventEmitterService.emitAsync(Events.USER_SIGNUP, new UserSignUpEvent(res.id));
113-
return res;
114-
}
115-
116-
async signout(req: Express.Request) {
117-
await new Promise<void>((resolve, reject) => {
118-
req.session.destroy(function (err) {
119-
// cannot access session here
120-
if (err) {
121-
reject(err);
122-
return;
123-
}
124-
resolve();
125-
});
126-
});
127-
}
128-
129-
async changePassword({ password, newPassword }: IChangePasswordRo) {
130-
const userId = this.cls.get('user.id');
131-
const user = await this.getUserByIdOrThrow(userId);
132-
133-
const { password: currentHashPassword, salt } = user;
134-
if (!(await this.comparePassword(password, currentHashPassword, salt))) {
135-
throw new BadRequestException('Password is incorrect');
136-
}
137-
const { salt: newSalt, hashPassword: newHashPassword } = await this.encodePassword(newPassword);
138-
await this.prismaService.txClient().user.update({
139-
where: { id: userId, deletedTime: null },
140-
data: {
141-
password: newHashPassword,
142-
salt: newSalt,
143-
},
144-
});
145-
// clear session
146-
await this.sessionStoreService.clearByUserId(userId);
147-
}
148-
149-
async sendResetPasswordEmail(email: string) {
150-
const user = await this.userService.getUserByEmail(email);
151-
if (!user || (user.accounts.length === 0 && user.password == null)) {
152-
throw new BadRequestException(`${email} not registered`);
153-
}
154-
155-
const resetPasswordCode = getRandomString(30);
156-
157-
const url = `${this.mailConfig.origin}/auth/reset-password?code=${resetPasswordCode}`;
158-
const resetPasswordEmailOptions = this.mailSenderService.resetPasswordEmailOptions({
159-
name: user.name,
160-
email: user.email,
161-
resetPasswordUrl: url,
162-
});
163-
await this.mailSenderService.sendMail({
164-
to: user.email,
165-
...resetPasswordEmailOptions,
166-
});
167-
await this.cacheService.set(
168-
`reset-password-email:${resetPasswordCode}`,
169-
{ userId: user.id },
170-
second(this.authConfig.resetPasswordEmailExpiresIn)
171-
);
172-
}
173-
174-
async resetPassword(code: string, newPassword: string) {
175-
const resetPasswordEmail = await this.cacheService.get(`reset-password-email:${code}`);
176-
if (!resetPasswordEmail) {
177-
throw new BadRequestException('Token is invalid');
178-
}
179-
const { userId } = resetPasswordEmail;
180-
const { salt, hashPassword } = await this.encodePassword(newPassword);
181-
await this.prismaService.txClient().user.update({
182-
where: { id: userId, deletedTime: null },
183-
data: {
184-
password: hashPassword,
185-
salt,
186-
},
187-
});
188-
await this.cacheService.del(`reset-password-email:${code}`);
189-
// clear session
190-
await this.sessionStoreService.clearByUserId(userId);
191-
}
192-
193-
async addPassword(newPassword: string) {
194-
const userId = this.cls.get('user.id');
195-
const user = await this.getUserByIdOrThrow(userId);
196-
197-
if (user.password) {
198-
throw new BadRequestException('Password is already set');
199-
}
200-
const { salt, hashPassword } = await this.encodePassword(newPassword);
201-
await this.prismaService.txClient().user.update({
202-
where: { id: userId, deletedTime: null, password: null },
203-
data: {
204-
password: hashPassword,
205-
salt,
206-
},
207-
});
208-
// clear session
209-
await this.sessionStoreService.clearByUserId(userId);
210-
}
211-
21215
async getUserInfo(user: IUserMeVo): Promise<IUserInfoVo> {
21316
const res = pick(user, ['id', 'email', 'avatar', 'name']);
21417
const accessTokenId = this.cls.get('accessTokenId');

0 commit comments

Comments
 (0)