You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
6.5 KiB
243 lines
6.5 KiB
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
|
|
import { UsersService } from '../users/users.service'
|
|
import { JwtService } from '@nestjs/jwt'
|
|
import * as bcrypt from 'bcryptjs'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import { InjectModel } from '@nestjs/mongoose'
|
|
import { ApiKey, ApiKeyDocument } from './schemas/api-key.schema'
|
|
import { Model } from 'mongoose'
|
|
import { User } from '../users/schemas/user.schema'
|
|
import axios from 'axios'
|
|
import {
|
|
PasswordReset,
|
|
PasswordResetDocument,
|
|
} from './schemas/password-reset.schema'
|
|
import { MailService } from 'src/mail/mail.service'
|
|
import { RequestResetPasswordInputDTO, ResetPasswordInputDTO } from './auth.dto'
|
|
import { AccessLog } from './schemas/access-log.schema'
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
private usersService: UsersService,
|
|
private jwtService: JwtService,
|
|
@InjectModel(ApiKey.name) private apiKeyModel: Model<ApiKeyDocument>,
|
|
@InjectModel(PasswordReset.name)
|
|
private passwordResetModel: Model<PasswordResetDocument>,
|
|
@InjectModel(AccessLog.name) private accessLogModel: Model<AccessLog>,
|
|
private readonly mailService: MailService,
|
|
) {}
|
|
|
|
async login(userData: any) {
|
|
const user = await this.usersService.findOne({ email: userData.email })
|
|
if (!user) {
|
|
throw new HttpException(
|
|
{ error: 'User not found' },
|
|
HttpStatus.UNAUTHORIZED,
|
|
)
|
|
}
|
|
|
|
if (!(await bcrypt.compare(userData.password, user.password))) {
|
|
throw new HttpException(
|
|
{ error: 'Invalid credentials' },
|
|
HttpStatus.UNAUTHORIZED,
|
|
)
|
|
}
|
|
|
|
user.lastLoginAt = new Date()
|
|
await user.save()
|
|
|
|
const payload = { email: user.email, sub: user._id }
|
|
return {
|
|
accessToken: this.jwtService.sign(payload),
|
|
user,
|
|
}
|
|
}
|
|
|
|
async loginWithGoogle(idToken: string) {
|
|
const response = await axios.get(
|
|
`https://oauth2.googleapis.com/tokeninfo?id_token=${idToken}`,
|
|
)
|
|
|
|
const { sub: googleId, name, email, picture } = response.data
|
|
let user = await this.usersService.findOne({ email })
|
|
|
|
if (!user) {
|
|
user = await this.usersService.create({
|
|
name,
|
|
email,
|
|
googleId,
|
|
avatar: picture,
|
|
lastLoginAt: new Date(),
|
|
})
|
|
} else {
|
|
user.googleId = googleId
|
|
|
|
if (!user.name) {
|
|
user.name = name
|
|
}
|
|
if (!user.avatar) {
|
|
user.avatar = picture
|
|
}
|
|
user.lastLoginAt = new Date()
|
|
await user.save()
|
|
}
|
|
|
|
const payload = { email: user.email, sub: user._id }
|
|
return {
|
|
accessToken: this.jwtService.sign(payload),
|
|
user,
|
|
}
|
|
}
|
|
|
|
async register(userData: any) {
|
|
const hashedPassword = await bcrypt.hash(userData.password, 10)
|
|
const user = await this.usersService.create({
|
|
...userData,
|
|
password: hashedPassword,
|
|
lastLoginAt: new Date(),
|
|
})
|
|
|
|
const payload = { email: user.email, sub: user._id }
|
|
|
|
return {
|
|
accessToken: this.jwtService.sign(payload),
|
|
user,
|
|
}
|
|
}
|
|
|
|
async requestResetPassword({ email }: RequestResetPasswordInputDTO) {
|
|
const user = await this.usersService.findOne({ email })
|
|
if (!user) {
|
|
throw new HttpException({ error: 'User not found' }, HttpStatus.NOT_FOUND)
|
|
}
|
|
|
|
const otp = Math.floor(100000 + Math.random() * 900000).toString()
|
|
const expiresAt = new Date(Date.now() + 20 * 60 * 1000)
|
|
|
|
const hashedOtp = await bcrypt.hash(otp, 10)
|
|
const passwordReset = new this.passwordResetModel({
|
|
user: user._id,
|
|
otp: hashedOtp,
|
|
expiresAt,
|
|
})
|
|
passwordReset.save()
|
|
|
|
await this.mailService.sendEmailFromTemplate({
|
|
to: user.email,
|
|
subject: 'Password Reset',
|
|
template: 'password-reset-request',
|
|
context: { name: user.name, otp },
|
|
})
|
|
|
|
return { message: 'Password reset email sent' }
|
|
}
|
|
|
|
async resetPassword({ email, otp, newPassword }: ResetPasswordInputDTO) {
|
|
const user = await this.usersService.findOne({ email })
|
|
if (!user) {
|
|
throw new HttpException({ error: 'User not found' }, HttpStatus.NOT_FOUND)
|
|
}
|
|
const passwordReset = await this.passwordResetModel.findOne(
|
|
{
|
|
user: user._id,
|
|
expiresAt: { $gt: new Date() },
|
|
},
|
|
null,
|
|
{ sort: { createdAt: -1 } },
|
|
)
|
|
|
|
if (!passwordReset || !(await bcrypt.compare(otp, passwordReset.otp))) {
|
|
throw new HttpException({ error: 'Invalid OTP' }, HttpStatus.BAD_REQUEST)
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(newPassword, 10)
|
|
user.password = hashedPassword
|
|
await user.save()
|
|
|
|
this.mailService.sendEmailFromTemplate({
|
|
to: user.email,
|
|
subject: 'Password Reset',
|
|
template: 'password-reset-success',
|
|
context: { name: user.name },
|
|
})
|
|
|
|
return { message: 'Password reset successfully' }
|
|
}
|
|
|
|
async generateApiKey(currentUser: User) {
|
|
const apiKey = uuidv4()
|
|
const hashedApiKey = await bcrypt.hash(apiKey, 10)
|
|
|
|
const newApiKey = new this.apiKeyModel({
|
|
apiKey: apiKey.substr(0, 17) + '*'.repeat(18),
|
|
hashedApiKey,
|
|
user: currentUser._id,
|
|
})
|
|
|
|
await newApiKey.save()
|
|
|
|
return { apiKey, message: 'Save this key, it wont be shown again ;)' }
|
|
}
|
|
|
|
async getUserApiKeys(currentUser: User) {
|
|
return this.apiKeyModel.find({ user: currentUser._id })
|
|
}
|
|
|
|
async findApiKey(params) {
|
|
return this.apiKeyModel.findOne(params)
|
|
}
|
|
|
|
async findApiKeyById(apiKeyId: string) {
|
|
return this.apiKeyModel.findById(apiKeyId)
|
|
}
|
|
|
|
async deleteApiKey(apiKeyId: string) {
|
|
const apiKey = await this.apiKeyModel.findOne({ _id: apiKeyId })
|
|
if (!apiKey) {
|
|
throw new HttpException(
|
|
{
|
|
error: 'Api key not found',
|
|
},
|
|
HttpStatus.NOT_FOUND,
|
|
)
|
|
}
|
|
|
|
// await this.apiKeyModel.deleteOne({ _id: apiKeyId })
|
|
}
|
|
|
|
async trackAccessLog({ request }) {
|
|
const { apiKey, user, method, url, ip, headers } = request
|
|
const userAgent = headers['user-agent']
|
|
|
|
if (request.apiKey) {
|
|
this.apiKeyModel
|
|
.findByIdAndUpdate(
|
|
apiKey._id,
|
|
{ $inc: { usageCount: 1 }, lastUsedAt: new Date() },
|
|
{ new: true },
|
|
)
|
|
.exec()
|
|
.catch((e) => {
|
|
console.log('Failed to update api key usage count')
|
|
console.log(e)
|
|
})
|
|
}
|
|
|
|
this.accessLogModel
|
|
.create({
|
|
apiKey,
|
|
user,
|
|
method,
|
|
url: url.split('?')[0],
|
|
ip:
|
|
request.headers['x-forwarded-for'] ||
|
|
request.connection.remoteAddress ||
|
|
ip,
|
|
userAgent,
|
|
})
|
|
.catch((e) => {
|
|
console.log('Failed to track access log')
|
|
console.log(e)
|
|
})
|
|
}
|
|
}
|