|
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'; |
7 | 4 | 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'; |
14 | 5 | 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'; |
18 | 6 | import { PermissionService } from './permission.service'; |
19 | | -import { SessionStoreService } from './session/session-store.service'; |
20 | 7 |
|
21 | 8 | @Injectable() |
22 | 9 | export class AuthService { |
23 | 10 | constructor( |
24 | | - private readonly prismaService: PrismaService, |
25 | | - private readonly userService: UserService, |
26 | 11 | 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 |
34 | 13 | ) {} |
35 | 14 |
|
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 | | - |
212 | 15 | async getUserInfo(user: IUserMeVo): Promise<IUserInfoVo> { |
213 | 16 | const res = pick(user, ['id', 'email', 'avatar', 'name']); |
214 | 17 | const accessTokenId = this.cls.get('accessTokenId'); |
|
0 commit comments