Compare commits
No commits in common. "bbbfb889ff73c8d136a5458387c44d303709de14" and "4cad7e2f17e86922fe3b25bbb2b9413b014c5790" have entirely different histories.
bbbfb889ff
...
4cad7e2f17
2
.gitignore
vendored
2
.gitignore
vendored
@ -54,5 +54,3 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
/prisma/generated
|
|
||||||
|
|||||||
@ -30,11 +30,6 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
||||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,9 +17,7 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
"prisma:generate": "prisma generate",
|
|
||||||
"prisma:studio": "prisma studio"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.3",
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
@ -30,12 +28,7 @@
|
|||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/microservices": "^11.1.12",
|
"@nestjs/microservices": "^11.1.12",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
|
||||||
"@prisma/client": "^7.3.0",
|
|
||||||
"@teacinema/contracts": "^1.0.0",
|
"@teacinema/contracts": "^1.0.0",
|
||||||
"dotenv-expand": "^12.0.3",
|
|
||||||
"ioredis": "^5.9.2",
|
|
||||||
"prisma": "^7.3.0",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import 'dotenv-expand/config'
|
|
||||||
import { defineConfig } from 'prisma/config'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
schema: 'prisma/schema.prisma',
|
|
||||||
migrations: {
|
|
||||||
path: 'prisma/migrations'
|
|
||||||
},
|
|
||||||
datasource: {
|
|
||||||
url: process.env['POSTGRES_URI']
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client"
|
|
||||||
output = "./generated"
|
|
||||||
moduleFormat = "cjs"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
}
|
|
||||||
|
|
||||||
model Account {
|
|
||||||
id String @id @default(nanoid())
|
|
||||||
|
|
||||||
phone String? @unique
|
|
||||||
email String? @unique
|
|
||||||
|
|
||||||
isPhoneVerified Boolean @default(false) @map("is_phone_verified")
|
|
||||||
isEmailVerified Boolean @default(false) @map("is_email_verified")
|
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
|
||||||
|
|
||||||
@@map("accounts")
|
|
||||||
}
|
|
||||||
@ -1,18 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { ConfigModule } from '@nestjs/config'
|
|
||||||
|
|
||||||
import { PrismaModule } from './infra/prisma/prisma.module'
|
|
||||||
import { RedisModule } from './infra/redis/redis.module'
|
|
||||||
import { AuthModule } from './modules/auth/auth.module'
|
import { AuthModule } from './modules/auth/auth.module'
|
||||||
import { OtpModule } from './modules/otp/otp.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [AuthModule]
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
|
||||||
PrismaModule,
|
|
||||||
RedisModule,
|
|
||||||
AuthModule,
|
|
||||||
OtpModule
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { PrismaService } from './prisma.service'
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
providers: [PrismaService],
|
|
||||||
exports: [PrismaService]
|
|
||||||
})
|
|
||||||
export class PrismaModule {}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
OnModuleDestroy,
|
|
||||||
OnModuleInit
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { ConfigService } from '@nestjs/config'
|
|
||||||
import { PrismaPg } from '@prisma/adapter-pg'
|
|
||||||
import { PrismaClient } from '@prisma/generated/client'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaService
|
|
||||||
extends PrismaClient
|
|
||||||
implements OnModuleInit, OnModuleDestroy
|
|
||||||
{
|
|
||||||
private readonly logger = new Logger(PrismaService.name)
|
|
||||||
|
|
||||||
public constructor(private readonly configService: ConfigService) {
|
|
||||||
super({
|
|
||||||
adapter: new PrismaPg({
|
|
||||||
user: configService.getOrThrow<string>('POSTGRES_USERNAME'),
|
|
||||||
password: configService.getOrThrow<string>('POSTGRES_PASSWORD'),
|
|
||||||
host: configService.getOrThrow<string>('POSTGRES_HOST'),
|
|
||||||
port: configService.getOrThrow<number>('POSTGRES_PORT'),
|
|
||||||
database: configService.getOrThrow<string>('POSTGRES_DATABASE_NAME')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onModuleInit() {
|
|
||||||
const start = Date.now()
|
|
||||||
|
|
||||||
this.logger.log('Prisma connecting...')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$connect()
|
|
||||||
|
|
||||||
const ms = Date.now() - start
|
|
||||||
|
|
||||||
this.logger.log(`Prisma connected in ${ms}ms`)
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error('Prisma connection error ', e)
|
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onModuleDestroy() {
|
|
||||||
this.logger.log('Prisma disconnecting...')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$disconnect()
|
|
||||||
|
|
||||||
this.logger.log('Prisma disconnected')
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error('Prisma disconnection error ', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { RedisService } from './redis.service'
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
providers: [RedisService],
|
|
||||||
exports: [RedisService]
|
|
||||||
})
|
|
||||||
export class RedisModule {}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
OnModuleDestroy,
|
|
||||||
OnModuleInit
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { ConfigService } from '@nestjs/config'
|
|
||||||
import Redis from 'ioredis'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RedisService
|
|
||||||
extends Redis
|
|
||||||
implements OnModuleInit, OnModuleDestroy
|
|
||||||
{
|
|
||||||
private readonly logger = new Logger(RedisService.name)
|
|
||||||
|
|
||||||
public constructor(private readonly configService: ConfigService) {
|
|
||||||
super({
|
|
||||||
username: configService.getOrThrow<string>('REDIS_USERNAME'),
|
|
||||||
host: configService.getOrThrow<string>('REDIS_HOST'),
|
|
||||||
port: configService.getOrThrow<number>('REDIS_PORT'),
|
|
||||||
password: configService.getOrThrow<string>('REDIS_PASSWORD'),
|
|
||||||
maxRetriesPerRequest: 5,
|
|
||||||
enableOfflineQueue: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public onModuleInit() {
|
|
||||||
const start = Date.now()
|
|
||||||
|
|
||||||
this.on('connect', () => {
|
|
||||||
this.logger.log('Redis connecting...')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.on('ready', () => {
|
|
||||||
this.logger.log(`Redis connected in ${Date.now() - start}ms`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.on('error', e => {
|
|
||||||
this.logger.error('Redis connection error', {
|
|
||||||
error: e.message || e
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.on('close', () => {
|
|
||||||
this.logger.warn('Redis connection closed')
|
|
||||||
})
|
|
||||||
|
|
||||||
this.on('reconnecting', () => {
|
|
||||||
this.logger.log('Redis reconnecting')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onModuleDestroy() {
|
|
||||||
this.logger.log('Redis disconnecting...')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.quit()
|
|
||||||
this.logger.log('Redis disconnected')
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.error('Redis disconnecting error', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,9 +2,7 @@ import { Controller } from '@nestjs/common'
|
|||||||
import { GrpcMethod } from '@nestjs/microservices'
|
import { GrpcMethod } from '@nestjs/microservices'
|
||||||
import type {
|
import type {
|
||||||
SendOtpRequest,
|
SendOtpRequest,
|
||||||
SendOtpResponse,
|
SendOtpResponse
|
||||||
VerifyOtpRequest,
|
|
||||||
VerifyOtpResponse
|
|
||||||
} from '@teacinema/contracts/gen/auth'
|
} from '@teacinema/contracts/gen/auth'
|
||||||
|
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service'
|
||||||
@ -14,12 +12,10 @@ export class AuthController {
|
|||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'SendOtp')
|
@GrpcMethod('AuthService', 'SendOtp')
|
||||||
public sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
public sendOtp(data: SendOtpRequest): SendOtpResponse {
|
||||||
return this.authService.sendOtp(data)
|
console.log('Incoming OTP request', data)
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('AuthService', 'VerifyOtp')
|
|
||||||
public VerifyOtp(data: VerifyOtpRequest): Promise<VerifyOtpResponse> {
|
|
||||||
return this.authService.verifyOtp(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
import { OtpService } from '@/modules/otp/otp.service'
|
|
||||||
|
|
||||||
import { AuthController } from './auth.controller'
|
import { AuthController } from './auth.controller'
|
||||||
import { AuthRepository } from './auth.repository'
|
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, AuthRepository, OtpService]
|
providers: [AuthService]
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
|
||||||
import { Account } from '@prisma/generated/client'
|
|
||||||
import {
|
|
||||||
AccountCreateInput,
|
|
||||||
AccountUpdateInput
|
|
||||||
} from '@prisma/generated/models/Account'
|
|
||||||
|
|
||||||
import { PrismaService } from '@/infra/prisma/prisma.service'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthRepository {
|
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
|
||||||
|
|
||||||
public findByPhone(phone: NonNullable<Account['phone']>) {
|
|
||||||
return this.prismaService.account.findUnique({
|
|
||||||
where: { phone }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public findByEmail(email: NonNullable<Account['email']>) {
|
|
||||||
return this.prismaService.account.findUnique({
|
|
||||||
where: { email }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public createAccount(data: AccountCreateInput) {
|
|
||||||
return this.prismaService.account.create({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateAccount(id: Account['id'], data: AccountUpdateInput) {
|
|
||||||
return this.prismaService.account.update({
|
|
||||||
where: { id },
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { RpcException } from '@nestjs/microservices'
|
|
||||||
import { Account } from '@prisma/generated/client'
|
|
||||||
import {
|
|
||||||
SendOtpRequest,
|
|
||||||
SendOtpResponse,
|
|
||||||
VerifyOtpRequest,
|
|
||||||
VerifyOtpResponse
|
|
||||||
} from '@teacinema/contracts/gen/auth'
|
|
||||||
|
|
||||||
import { OtpService } from '@/modules/otp/otp.service'
|
|
||||||
|
|
||||||
import { AuthRepository } from './auth.repository'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {}
|
||||||
public constructor(
|
|
||||||
private readonly authRepository: AuthRepository,
|
|
||||||
private readonly otpService: OtpService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
|
|
||||||
const { identifier, type } = data
|
|
||||||
|
|
||||||
let account: Account | null = null
|
|
||||||
const isPhoneType = type === 'phone'
|
|
||||||
const isEmailType = type === 'email'
|
|
||||||
|
|
||||||
if (isPhoneType) {
|
|
||||||
account = await this.authRepository.findByPhone(identifier)
|
|
||||||
} else {
|
|
||||||
account = await this.authRepository.findByEmail(identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
account = await this.authRepository.createAccount({
|
|
||||||
email: isEmailType ? identifier : undefined,
|
|
||||||
phone: isPhoneType ? identifier : undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = await this.otpService.send(
|
|
||||||
identifier,
|
|
||||||
type as 'email' | 'phone'
|
|
||||||
)
|
|
||||||
console.debug('CODE', code)
|
|
||||||
|
|
||||||
return { ok: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verifyOtp(data: VerifyOtpRequest): Promise<VerifyOtpResponse> {
|
|
||||||
const { identifier, code, type } = data
|
|
||||||
|
|
||||||
await this.otpService.verify(identifier, code, type as 'email' | 'phone')
|
|
||||||
|
|
||||||
const isPhoneType = type === 'phone'
|
|
||||||
const isEmailType = type === 'email'
|
|
||||||
|
|
||||||
const account = isPhoneType
|
|
||||||
? await this.authRepository.findByPhone(identifier)
|
|
||||||
: await this.authRepository.findByEmail(identifier)
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
throw new RpcException('Account not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPhoneType && !account.isPhoneVerified) {
|
|
||||||
await this.authRepository.updateAccount(account.id, {
|
|
||||||
isPhoneVerified: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmailType && !account.isEmailVerified) {
|
|
||||||
await this.authRepository.updateAccount(account.id, {
|
|
||||||
isEmailVerified: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { accessToken: '123456', refreshToken: '123456' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common'
|
|
||||||
|
|
||||||
import { OtpService } from './otp.service'
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [OtpService]
|
|
||||||
})
|
|
||||||
export class OtpModule {}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
|
||||||
import { RpcException } from '@nestjs/microservices'
|
|
||||||
import { createHash } from 'node:crypto'
|
|
||||||
|
|
||||||
import { RedisService } from '@/infra/redis/redis.service'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OtpService {
|
|
||||||
public constructor(private readonly redisService: RedisService) {}
|
|
||||||
|
|
||||||
public async send(indentifier: string, type: 'email' | 'phone') {
|
|
||||||
const { code, hash } = this.generateCode()
|
|
||||||
|
|
||||||
await this.redisService.set(
|
|
||||||
`otp:${type}:${indentifier}`,
|
|
||||||
hash,
|
|
||||||
'EX',
|
|
||||||
60 * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verify(
|
|
||||||
indentifier: string,
|
|
||||||
code: string,
|
|
||||||
type: 'email' | 'phone'
|
|
||||||
) {
|
|
||||||
const stroredHash = await this.redisService.get(
|
|
||||||
`otp:${type}:${indentifier}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!stroredHash) {
|
|
||||||
throw new RpcException('Invalid or expired code')
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = createHash('sha256').update(code).digest('hex')
|
|
||||||
if (stroredHash !== hash) {
|
|
||||||
throw new RpcException('Invalid or expired code')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.redisService.del(`otp:${type}:${indentifier}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateCode() {
|
|
||||||
// todo посмотреть готовые бибилотеки для генерации friendly otp
|
|
||||||
const code = Math.floor(100_000 + Math.random() * 900_000)
|
|
||||||
const hash = createHash('sha256').update(String(code)).digest('hex')
|
|
||||||
|
|
||||||
return { code, hash }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,11 +20,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false,
|
"noFallthroughCasesInSwitch": false
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"],
|
|
||||||
"@prisma/generated": ["./prisma/generated"],
|
|
||||||
"@prisma/generated/*": ["./prisma/generated/*"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user