• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 10990844066

23 Sep 2024 08:50AM UTC coverage: 84.957%. First build
10990844066

Pull #937

github

web-flow
Merge 796f052ab into d22a1e085
Pull Request #937: fix: disabled delete collaborators last owner

5614 of 5917 branches covered (94.88%)

37 of 53 new or added lines in 4 files covered. (69.81%)

36931 of 43470 relevant lines covered (84.96%)

1164.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.8
/apps/nestjs-backend/src/features/auth/auth.service.ts
1
import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
4✔
2
import { generateUserId, getRandomString } from '@teable/core';
4✔
3
import { PrismaService } from '@teable/db-main-prisma';
4✔
4
import type { IChangePasswordRo, IUserInfoVo, IUserMeVo } from '@teable/openapi';
4✔
5
import * as bcrypt from 'bcrypt';
4✔
6
import { omit, pick } from 'lodash';
4✔
7
import { ClsService } from 'nestjs-cls';
4✔
8
import { CacheService } from '../../cache/cache.service';
4✔
9
import { AuthConfig, type IAuthConfig } from '../../configs/auth.config';
4✔
10
import { MailConfig, type IMailConfig } from '../../configs/mail.config';
4✔
11
import type { IClsStore } from '../../types/cls';
4✔
12
import { second } from '../../utils/second';
4✔
13
import { MailSenderService } from '../mail-sender/mail-sender.service';
4✔
14
import { UserService } from '../user/user.service';
4✔
15
import { PermissionService } from './permission.service';
4✔
16
import { SessionStoreService } from './session/session-store.service';
4✔
17

4✔
18
@Injectable()
4✔
19
export class AuthService {
4✔
20
  constructor(
181✔
21
    private readonly prismaService: PrismaService,
181✔
22
    private readonly userService: UserService,
181✔
23
    private readonly cls: ClsService<IClsStore>,
181✔
24
    private readonly sessionStoreService: SessionStoreService,
181✔
25
    private readonly mailSenderService: MailSenderService,
181✔
26
    private readonly cacheService: CacheService,
181✔
27
    private readonly permissionService: PermissionService,
181✔
28
    @AuthConfig() private readonly authConfig: IAuthConfig,
181✔
29
    @MailConfig() private readonly mailConfig: IMailConfig
181✔
30
  ) {}
181✔
31

181✔
32
  private async encodePassword(password: string) {
181✔
33
    const salt = await bcrypt.genSalt(10);
15✔
34
    const hashPassword = await bcrypt.hash(password, salt);
15✔
35
    return { salt, hashPassword };
15✔
36
  }
15✔
37

181✔
38
  private async comparePassword(
181✔
39
    password: string,
158✔
40
    hashPassword: string | null,
158✔
41
    salt: string | null
158✔
42
  ) {
158✔
43
    const _hashPassword = await bcrypt.hash(password || '', salt || '');
158✔
44
    return _hashPassword === hashPassword;
158✔
45
  }
158✔
46

181✔
47
  private async getUserByIdOrThrow(userId: string) {
181✔
48
    const user = await this.userService.getUserById(userId);
×
49
    if (!user) {
×
50
      throw new BadRequestException('User not found');
×
51
    }
×
52
    return user;
×
53
  }
×
54

181✔
55
  async validateUserByEmail(email: string, pass: string) {
181✔
56
    const user = await this.userService.getUserByEmail(email);
158✔
57
    if (!user || (user.accounts.length === 0 && user.password == null)) {
158✔
58
      throw new BadRequestException(`${email} not registered`);
×
59
    }
×
60

158✔
61
    if (!user.password) {
158✔
62
      throw new BadRequestException('Password is not set');
×
63
    }
×
64

158✔
65
    const { password, salt, ...result } = user;
158✔
66
    return (await this.comparePassword(pass, password, salt)) ? { ...result, password } : null;
158✔
67
  }
158✔
68

181✔
69
  async signup(email: string, password: string, defaultSpaceName?: string) {
181✔
70
    const user = await this.userService.getUserByEmail(email);
82✔
71
    if (user && (user.password !== null || user.accounts.length > 0)) {
82✔
72
      throw new HttpException(`User ${email} is already registered`, HttpStatus.BAD_REQUEST);
67✔
73
    }
67✔
74
    const { salt, hashPassword } = await this.encodePassword(password);
15✔
75
    return await this.prismaService.$tx(async () => {
15✔
76
      if (user) {
15✔
77
        return await this.prismaService.user.update({
×
78
          where: { id: user.id, deletedTime: null },
×
79
          data: {
×
80
            salt,
×
81
            password: hashPassword,
×
82
            lastSignTime: new Date().toISOString(),
×
83
          },
×
84
        });
×
85
      }
×
86
      return await this.userService.createUserWithSettingCheck(
15✔
87
        {
15✔
88
          id: generateUserId(),
15✔
89
          name: email.split('@')[0],
15✔
90
          email,
15✔
91
          salt,
15✔
92
          password: hashPassword,
15✔
93
          lastSignTime: new Date().toISOString(),
15✔
94
        },
15✔
95
        undefined,
15✔
96
        defaultSpaceName
15✔
97
      );
15✔
98
    });
15✔
99
  }
15✔
100

181✔
101
  async signout(req: Express.Request) {
181✔
102
    await new Promise<void>((resolve, reject) => {
×
103
      req.session.destroy(function (err) {
×
104
        // cannot access session here
×
105
        if (err) {
×
106
          reject(err);
×
107
          return;
×
108
        }
×
109
        resolve();
×
110
      });
×
111
    });
×
112
  }
×
113

181✔
114
  async changePassword({ password, newPassword }: IChangePasswordRo) {
181✔
115
    const userId = this.cls.get('user.id');
×
116
    const user = await this.getUserByIdOrThrow(userId);
×
117

×
118
    const { password: currentHashPassword, salt } = user;
×
119
    if (!(await this.comparePassword(password, currentHashPassword, salt))) {
×
120
      throw new BadRequestException('Password is incorrect');
×
121
    }
×
122
    const { salt: newSalt, hashPassword: newHashPassword } = await this.encodePassword(newPassword);
×
123
    await this.prismaService.txClient().user.update({
×
124
      where: { id: userId, deletedTime: null },
×
125
      data: {
×
126
        password: newHashPassword,
×
127
        salt: newSalt,
×
128
      },
×
129
    });
×
130
    // clear session
×
131
    await this.sessionStoreService.clearByUserId(userId);
×
132
  }
×
133

181✔
134
  async sendResetPasswordEmail(email: string) {
181✔
135
    const user = await this.userService.getUserByEmail(email);
×
136
    if (!user || (user.accounts.length === 0 && user.password == null)) {
×
137
      throw new BadRequestException(`${email} not registered`);
×
138
    }
×
139

×
140
    const resetPasswordCode = getRandomString(30);
×
141

×
142
    const url = `${this.mailConfig.origin}/auth/reset-password?code=${resetPasswordCode}`;
×
143
    const resetPasswordEmailOptions = this.mailSenderService.resetPasswordEmailOptions({
×
144
      name: user.name,
×
145
      email: user.email,
×
146
      resetPasswordUrl: url,
×
147
    });
×
148
    await this.mailSenderService.sendMail({
×
149
      to: user.email,
×
150
      ...resetPasswordEmailOptions,
×
151
    });
×
152
    await this.cacheService.set(
×
153
      `reset-password-email:${resetPasswordCode}`,
×
154
      { userId: user.id },
×
155
      second(this.authConfig.resetPasswordEmailExpiresIn)
×
156
    );
×
157
  }
×
158

181✔
159
  async resetPassword(code: string, newPassword: string) {
181✔
160
    const resetPasswordEmail = await this.cacheService.get(`reset-password-email:${code}`);
×
161
    if (!resetPasswordEmail) {
×
162
      throw new BadRequestException('Token is invalid');
×
163
    }
×
164
    const { userId } = resetPasswordEmail;
×
165
    const { salt, hashPassword } = await this.encodePassword(newPassword);
×
166
    await this.prismaService.txClient().user.update({
×
167
      where: { id: userId, deletedTime: null },
×
168
      data: {
×
169
        password: hashPassword,
×
170
        salt,
×
171
      },
×
172
    });
×
173
    await this.cacheService.del(`reset-password-email:${code}`);
×
NEW
174
    // clear session
×
NEW
175
    await this.sessionStoreService.clearByUserId(userId);
×
176
  }
×
177

181✔
178
  async addPassword(newPassword: string) {
181✔
179
    const userId = this.cls.get('user.id');
×
180
    const user = await this.getUserByIdOrThrow(userId);
×
181

×
182
    if (user.password) {
×
183
      throw new BadRequestException('Password is already set');
×
184
    }
×
185
    const { salt, hashPassword } = await this.encodePassword(newPassword);
×
186
    await this.prismaService.txClient().user.update({
×
187
      where: { id: userId, deletedTime: null, password: null },
×
188
      data: {
×
189
        password: hashPassword,
×
190
        salt,
×
191
      },
×
192
    });
×
NEW
193
    // clear session
×
NEW
194
    await this.sessionStoreService.clearByUserId(userId);
×
195
  }
×
196

181✔
197
  async getUserInfo(user: IUserMeVo): Promise<IUserInfoVo> {
181✔
198
    const res = pick(user, ['id', 'email', 'avatar', 'name']);
4✔
199
    const accessTokenId = this.cls.get('accessTokenId');
4✔
200
    if (!accessTokenId) {
4✔
201
      return res;
×
202
    }
×
203
    const { scopes } = await this.permissionService.getAccessToken(accessTokenId);
4✔
204
    if (!scopes.includes('user|email_read')) {
4✔
205
      return omit(res, 'email');
2✔
206
    }
2✔
207
    return res;
2✔
208
  }
2✔
209
}
181✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc