-
-
Notifications
You must be signed in to change notification settings - Fork 124
/
Copy pathauth.service.ts
234 lines (211 loc) · 6.63 KB
/
auth.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import {
BadRequestException,
Inject,
Injectable,
Logger,
LoggerService,
NotFoundException,
UnauthorizedException
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Cron, CronExpression } from '@nestjs/schedule'
import { UserAuthenticatedResponse } from '../auth.types'
import { IMailService, MAIL_SERVICE } from '@/mail/services/interface.service'
import { PrismaService } from '@/prisma/prisma.service'
import { AuthProvider } from '@prisma/client'
import { CacheService } from '@/cache/cache.service'
import { generateOtp } from '@/common/util'
import { createUser, getUserByEmailOrId } from '@/common/user'
import { UserWithWorkspace } from '@/user/user.types'
import { Response } from 'express'
@Injectable()
export class AuthService {
private readonly logger: LoggerService
constructor(
@Inject(MAIL_SERVICE) private mailService: IMailService,
private readonly prisma: PrismaService,
private jwt: JwtService,
private cache: CacheService
) {
this.logger = new Logger(AuthService.name)
}
/**
* Sends a login code to the given email address
* @throws {BadRequestException} If the email address is invalid
* @param email The email address to send the login code to
*/
async sendOtp(email: string): Promise<void> {
if (!email || !email.includes('@')) {
this.logger.error(`Invalid email address: ${email}`)
throw new BadRequestException('Please enter a valid email address')
}
const user = await this.createUserIfNotExists(email, AuthProvider.EMAIL_OTP)
const otp = await generateOtp(email, user.id, this.prisma)
await this.mailService.sendOtp(email, otp.code)
this.logger.log(`Login code sent to ${email}`)
}
/**
* resend a login code to the given email address after resend otp button is pressed
* @throws {BadRequestException} If the email address is invalid
* @param email The email address to resend the login code to
*/
async resendOtp(email: string): Promise<void> {
const user = await getUserByEmailOrId(email, this.prisma)
const otp = await generateOtp(email, user.id, this.prisma)
await this.mailService.sendOtp(email, otp.code)
}
/* istanbul ignore next */
/**
* Validates a login code sent to the given email address
* @throws {NotFoundException} If the user is not found
* @throws {UnauthorizedException} If the login code is invalid
* @param email The email address the login code was sent to
* @param otp The login code to validate
* @returns An object containing the user and a JWT token
*/
async validateOtp(
email: string,
otp: string
): Promise<UserAuthenticatedResponse> {
const user = await getUserByEmailOrId(email, this.prisma)
if (!user) {
this.logger.error(`User not found: ${email}`)
throw new NotFoundException('User not found')
}
const isOtpValid =
(await this.prisma.otp.findUnique({
where: {
userCode: {
code: otp,
userId: user.id
},
expiresAt: {
gt: new Date()
}
}
})) !== null
if (!isOtpValid) {
this.logger.error(`Invalid login code for ${email}: ${otp}`)
throw new UnauthorizedException('Invalid login code')
}
await this.prisma.otp.delete({
where: {
userCode: {
code: otp,
userId: user.id
}
}
})
this.cache.setUser(user) // Save user to cache
this.logger.log(`User logged in: ${email}`)
const token = await this.generateToken(user.id)
return {
...user,
token
}
}
/* istanbul ignore next */
/**
* Handles a login with an OAuth provider
* @param email The email of the user
* @param name The name of the user
* @param profilePictureUrl The profile picture URL of the user
* @param oauthProvider The OAuth provider used
* @returns An object containing the user and a JWT token
*/
async handleOAuthLogin(
email: string,
name: string,
profilePictureUrl: string,
oauthProvider: AuthProvider
): Promise<UserAuthenticatedResponse> {
// We need to create the user if it doesn't exist yet
const user = await this.createUserIfNotExists(
email,
oauthProvider,
name,
profilePictureUrl
)
const token = await this.generateToken(user.id)
return {
...user,
token
}
}
/* istanbul ignore next */
/**
* Cleans up expired OTPs every hour
* @throws {PrismaError} If there is an error deleting expired OTPs
*/
@Cron(CronExpression.EVERY_HOUR)
async cleanUpExpiredOtps() {
try {
const timeNow = new Date()
await this.prisma.otp.deleteMany({
where: {
expiresAt: {
lte: new Date(timeNow.getTime())
}
}
})
this.logger.log('Expired OTPs cleaned up successfully.')
} catch (error) {
this.logger.error(`Error cleaning up expired OTPs: ${error.message}`)
}
}
/**
* Creates a user if it doesn't exist yet. If the user has signed up with a
* different authentication provider, it throws an UnauthorizedException.
* @param email The email address of the user
* @param authProvider The AuthProvider used
* @param name The name of the user
* @param profilePictureUrl The profile picture URL of the user
* @returns The user
* @throws {UnauthorizedException} If the user has signed up with a different
* authentication provider
*/
private async createUserIfNotExists(
email: string,
authProvider: AuthProvider,
name?: string,
profilePictureUrl?: string
) {
let user: UserWithWorkspace | null
try {
user = await getUserByEmailOrId(email, this.prisma)
} catch (ignored) {}
// We need to create the user if it doesn't exist yet
if (!user) {
user = await createUser(
{
email,
name,
profilePictureUrl,
authProvider
},
this.prisma
)
}
// If the user has used OAuth to log in, we need to check if the OAuth provider
// used in the current login is different from the one stored in the database
if (user.authProvider !== authProvider) {
throw new UnauthorizedException(
'The user has signed up with a different authentication provider.'
)
}
return user
}
private async generateToken(id: string) {
return await this.jwt.signAsync({ id })
}
/**
* Clears the token cookie on logout
* @param res The response object
*/
async logout(res: Response): Promise<void> {
res.clearCookie('token', {
domain: process.env.DOMAIN ?? 'localhost'
})
this.logger.log('User logged out and token cookie cleared.')
}
}