mirror of
https://github.com/ershisan99/flashcards-api.git
synced 2025-12-17 05:09:26 +00:00
auth in progress
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"dev": "nest start --watch",
|
"dev": "nest start --watch",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
|||||||
@@ -38,18 +38,17 @@ model User {
|
|||||||
grades Grade[]
|
grades Grade[]
|
||||||
generalChatMessages GeneralChatMessage[]
|
generalChatMessages GeneralChatMessage[]
|
||||||
verification Verification?
|
verification Verification?
|
||||||
AccessToken AccessToken[]
|
RevokedToken RevokedToken[]
|
||||||
RefreshToken RefreshToken[]
|
RefreshToken RefreshToken[]
|
||||||
|
|
||||||
@@fulltext([name, email])
|
@@fulltext([name, email])
|
||||||
}
|
}
|
||||||
|
|
||||||
model AccessToken {
|
model RevokedToken {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
token String @unique
|
token String @unique
|
||||||
expiresAt DateTime
|
revokedAt DateTime @default(now())
|
||||||
isRevoked Boolean @default(false)
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@ -58,7 +57,7 @@ model AccessToken {
|
|||||||
model RefreshToken {
|
model RefreshToken {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
token String @unique
|
token String @unique @db.VarChar(255)
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
isRevoked Boolean @default(false)
|
isRevoked Boolean @default(false)
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import * as process from 'process'
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
AuthModule,
|
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
AuthModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
|
||||||
MailerModule.forRoot({
|
MailerModule.forRoot({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export class JwtPayloadExtractorGuard extends AuthGuard('payloadExtractor') {
|
|||||||
canActivate(context: ExecutionContext) {
|
canActivate(context: ExecutionContext) {
|
||||||
// Add your custom authentication logic here
|
// Add your custom authentication logic here
|
||||||
// for example, call super.logIn(request) to establish a session.
|
// for example, call super.logIn(request) to establish a session.
|
||||||
|
console.log('JwtPayloadExtractorGuard')
|
||||||
return super.canActivate(context)
|
return super.canActivate(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import { Inject, Injectable } from '@nestjs/common'
|
|||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt'
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
import { AppSettings } from '../../settings/app-settings'
|
import { AppSettings } from '../../settings/app-settings'
|
||||||
|
import { AuthService } from '../../modules/auth/auth.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'payloadExtractor') {
|
export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'payloadExtractor') {
|
||||||
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
|
constructor(
|
||||||
|
@Inject(AppSettings.name) private readonly appSettings: AppSettings,
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
@@ -14,6 +18,7 @@ export class JwtPayloadExtractorStrategy extends PassportStrategy(Strategy, 'pay
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: any) {
|
async validate(payload: any) {
|
||||||
|
console.log(payload)
|
||||||
const userId = payload.userId
|
const userId = payload.userId
|
||||||
const name = payload.name
|
const name = payload.name
|
||||||
if (payload) return { userId, name }
|
if (payload) return { userId, name }
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export class AuthController {
|
|||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@Post('sign-in')
|
@Post('sign-in')
|
||||||
async login(@Request() req, @Res({ passthrough: true }) res) {
|
async login(@Request() req, @Res({ passthrough: true }) res) {
|
||||||
console.log(req)
|
|
||||||
const userData = req.user.data
|
const userData = req.user.data
|
||||||
res.cookie('refreshToken', userData.refreshToken, {
|
res.cookie('refreshToken', userData.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -96,10 +95,12 @@ export class AuthController {
|
|||||||
async refreshToken(@Request() req, @Response() res) {
|
async refreshToken(@Request() req, @Response() res) {
|
||||||
if (!req.cookie?.refreshToken) throw new UnauthorizedException()
|
if (!req.cookie?.refreshToken) throw new UnauthorizedException()
|
||||||
const userId = req.user.id
|
const userId = req.user.id
|
||||||
const newTokens = this.authService.createJwtTokensPair(userId, null)
|
const newTokens = await this.authService.createJwtTokensPair(userId)
|
||||||
res.cookie('refreshToken', newTokens.refreshToken, {
|
res.cookie('refreshToken', newTokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
|
path: '/refresh',
|
||||||
|
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
})
|
})
|
||||||
return { accessToken: newTokens.accessToken }
|
return { accessToken: newTokens.accessToken }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ import { LocalStrategy } from './strategies/local.strategy'
|
|||||||
imports: [UsersModule],
|
imports: [UsersModule],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, LocalStrategy],
|
providers: [AuthService, LocalStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,32 +1,104 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable, UnauthorizedException } from '@nestjs/common'
|
||||||
import { isAfter } from 'date-fns'
|
import { addDays, isAfter } from 'date-fns'
|
||||||
import * as jwt from 'jsonwebtoken'
|
import * as jwt from 'jsonwebtoken'
|
||||||
import * as bcrypt from 'bcrypt'
|
import * as bcrypt from 'bcrypt'
|
||||||
import { UsersRepository } from '../users/infrastructure/users.repository'
|
import { UsersRepository } from '../users/infrastructure/users.repository'
|
||||||
import * as process from 'process'
|
import * as process from 'process'
|
||||||
|
import { PrismaService } from '../../prisma.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(private usersRepository: UsersRepository) {}
|
constructor(private usersRepository: UsersRepository, private prisma: PrismaService) {}
|
||||||
|
|
||||||
createJwtTokensPair(userId: string, email: string | null) {
|
async revokeToken(token: string, userId: string): Promise<void> {
|
||||||
|
await this.prisma.revokedToken.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async isTokenRevoked(token: string): Promise<boolean> {
|
||||||
|
const revokedToken = await this.prisma.revokedToken.findUnique({
|
||||||
|
where: { token },
|
||||||
|
})
|
||||||
|
return !!revokedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically remove old revoked tokens
|
||||||
|
async removeExpiredTokens(): Promise<void> {
|
||||||
|
const hourAgo = new Date()
|
||||||
|
hourAgo.setHours(hourAgo.getHours() - 1)
|
||||||
|
await this.prisma.revokedToken.deleteMany({
|
||||||
|
where: { revokedAt: { lt: hourAgo } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async createJwtTokensPair(userId: string) {
|
||||||
const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY
|
const accessSecretKey = process.env.ACCESS_JWT_SECRET_KEY
|
||||||
const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY
|
const refreshSecretKey = process.env.REFRESH_JWT_SECRET_KEY
|
||||||
const payload: { userId: string; date: Date; email: string | null } = {
|
const payload: { userId: string; date: Date } = {
|
||||||
userId,
|
userId,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
email,
|
|
||||||
}
|
}
|
||||||
const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '1d' })
|
const accessToken = jwt.sign(payload, accessSecretKey, { expiresIn: '10m' })
|
||||||
const refreshToken = jwt.sign(payload, refreshSecretKey, {
|
const refreshToken = jwt.sign(payload, refreshSecretKey, {
|
||||||
expiresIn: '30d',
|
expiresIn: '30d',
|
||||||
})
|
})
|
||||||
|
console.log(refreshToken.length)
|
||||||
|
// Save refresh token in the database
|
||||||
|
await this.prisma.refreshToken.create({
|
||||||
|
data: {
|
||||||
|
userId: userId,
|
||||||
|
token: refreshToken,
|
||||||
|
expiresAt: addDays(new Date(), 30),
|
||||||
|
isRevoked: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkToken(accessToken: string, refreshToken: string) {
|
||||||
|
try {
|
||||||
|
await jwt.verify(accessToken, process.env.ACCESS_JWT_SECRET_KEY)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof jwt.TokenExpiredError) {
|
||||||
|
const dbRefreshToken = await this.prisma.refreshToken.findUnique({
|
||||||
|
where: { token: refreshToken },
|
||||||
|
})
|
||||||
|
const isTokenRevoked = await this.isTokenRevoked(accessToken)
|
||||||
|
if (isTokenRevoked) {
|
||||||
|
throw new UnauthorizedException()
|
||||||
|
}
|
||||||
|
if (dbRefreshToken && !dbRefreshToken.isRevoked && dbRefreshToken.expiresAt > new Date()) {
|
||||||
|
const newTokens = await this.createJwtTokensPair(dbRefreshToken.userId)
|
||||||
|
await this.prisma.refreshToken.update({
|
||||||
|
where: { id: dbRefreshToken.id },
|
||||||
|
data: { isRevoked: true },
|
||||||
|
})
|
||||||
|
return newTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(accessToken: string, refreshToken: string) {
|
||||||
|
// Revoke the access token
|
||||||
|
const decoded = jwt.verify(accessToken, process.env.ACCESS_JWT_SECRET_KEY)
|
||||||
|
await this.revokeToken(accessToken, decoded.userId)
|
||||||
|
await this.prisma.refreshToken.update({
|
||||||
|
where: { token: refreshToken },
|
||||||
|
data: { isRevoked: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async checkCredentials(email: string, password: string) {
|
async checkCredentials(email: string, password: string) {
|
||||||
const user = await this.usersRepository.findUserByEmail(email)
|
const user = await this.usersRepository.findUserByEmail(email)
|
||||||
if (!user /*|| !user.emailConfirmation.isConfirmed*/)
|
if (!user /*|| !user.emailConfirmation.isConfirmed*/)
|
||||||
@@ -49,7 +121,7 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tokensPair = this.createJwtTokensPair(user.id, user.email)
|
const tokensPair = await this.createJwtTokensPair(user.id)
|
||||||
return {
|
return {
|
||||||
resultCode: 0,
|
resultCode: 0,
|
||||||
data: tokensPair,
|
data: tokensPair,
|
||||||
|
|||||||
24
src/modules/auth/strategies/access-token.strategy.ts
Normal file
24
src/modules/auth/strategies/access-token.strategy.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common'
|
||||||
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
|
|
||||||
|
type JwtPayload = {
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
|
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
return { userId: payload.userId }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,42 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt'
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
import { AppSettings } from '../../../settings/app-settings'
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
|
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: true,
|
ignoreExpiration: false,
|
||||||
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
|
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: any) {
|
async validate(request: Request, payload: any) {
|
||||||
return { userId: payload.userId }
|
const accessToken = request.headers.authorization?.split(' ')[1]
|
||||||
|
const refreshToken = request.cookies.refreshToken // Extract refresh token from cookies
|
||||||
|
|
||||||
|
// If there's no refresh token, simply validate the user based on payload
|
||||||
|
if (!refreshToken) {
|
||||||
|
return { userId: payload.userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAccessToken = await this.authService.checkToken(accessToken, refreshToken)
|
||||||
|
|
||||||
|
// If new access token were issued, attach it to the response headers
|
||||||
|
if (newAccessToken) {
|
||||||
|
request.res.setHeader('Authorization', `Bearer ${newAccessToken.accessToken}`)
|
||||||
|
}
|
||||||
|
request.res.cookie('refreshToken', newAccessToken.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/auth/refresh-token',
|
||||||
|
})
|
||||||
|
return { userId: payload.userId }
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Invalid tokens')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validate(email: string, password: string): Promise<any> {
|
async validate(email: string, password: string): Promise<any> {
|
||||||
const user = await this.authService.checkCredentials(email, password)
|
const credentials = await this.authService.checkCredentials(email, password)
|
||||||
if (user.resultCode === 1) {
|
if (credentials.resultCode === 1) {
|
||||||
throw new UnauthorizedException()
|
throw new UnauthorizedException()
|
||||||
}
|
}
|
||||||
return user
|
return credentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/modules/auth/strategies/refresh-token.strategy.ts
Normal file
27
src/modules/auth/strategies/refresh-token.strategy.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common'
|
||||||
|
import { PassportStrategy } from '@nestjs/passport'
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
|
import { AppSettings } from '../../../settings/app-settings'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
type JwtPayload = {
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||||
|
constructor(@Inject(AppSettings.name) private readonly appSettings: AppSettings) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: appSettings.auth.ACCESS_JWT_SECRET_KEY,
|
||||||
|
passReqToCallback: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(req: Request, payload: any) {
|
||||||
|
const refreshToken = req.get('Authorization').replace('Bearer', '').trim()
|
||||||
|
return { ...payload, refreshToken }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,12 +152,10 @@ export class UsersRepository implements IUsersRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async revokeToken(id: string, token: string): Promise<User | null> {
|
async revokeToken(id: string, token: string): Promise<User | null> {
|
||||||
const revokedToken = await this.prisma.accessToken.update({
|
const revokedToken = await this.prisma.revokedToken.create({
|
||||||
where: {
|
|
||||||
token: token,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
isRevoked: true,
|
token: token,
|
||||||
|
userId: id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user