feat: add email/phone change
This commit is contained in:
parent
bf60bf5901
commit
6cb3d0d6f3
@ -17,6 +17,8 @@ model Account {
|
|||||||
isPhoneVerified Boolean @default(false) @map("is_phone_verified")
|
isPhoneVerified Boolean @default(false) @map("is_phone_verified")
|
||||||
isEmailVerified Boolean @default(false) @map("is_email_verified")
|
isEmailVerified Boolean @default(false) @map("is_email_verified")
|
||||||
|
|
||||||
|
pendingContactChanges PendingContactChange[]
|
||||||
|
|
||||||
role Role @default(USER)
|
role Role @default(USER)
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
@ -25,6 +27,24 @@ model Account {
|
|||||||
@@map("accounts")
|
@@map("accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PendingContactChange {
|
||||||
|
id String @id @default(nanoid())
|
||||||
|
|
||||||
|
type String
|
||||||
|
value String
|
||||||
|
codeHash String @map("code_hash")
|
||||||
|
expiresAt DateTime @map("expires_at")
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||||||
|
accountId String @map("account_id")
|
||||||
|
|
||||||
|
@@unique([accountId, type])
|
||||||
|
@@map("pending_contact_changes")
|
||||||
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
ADMIN
|
ADMIN
|
||||||
USER
|
USER
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { Controller } from '@nestjs/common'
|
import { Controller } from '@nestjs/common'
|
||||||
import { GrpcMethod } from '@nestjs/microservices'
|
import { GrpcMethod } from '@nestjs/microservices'
|
||||||
import type {
|
import type {
|
||||||
|
ConfirmEmailChangeRequest,
|
||||||
|
ConfirmEmailChangeResponse,
|
||||||
|
ConfirmPhoneChangeRequest,
|
||||||
|
ConfirmPhoneChangeResponse,
|
||||||
GetAccountRequest,
|
GetAccountRequest,
|
||||||
GetAccountResponse
|
GetAccountResponse,
|
||||||
|
InitEmailChangeRequest,
|
||||||
|
InitEmailChangeResponse,
|
||||||
|
InitPhoneChangeRequest,
|
||||||
|
InitPhoneChangeResponse
|
||||||
} from '@teacinema/contracts/gen/account'
|
} from '@teacinema/contracts/gen/account'
|
||||||
|
|
||||||
import { AccountService } from './account.service'
|
import { AccountService } from './account.service'
|
||||||
@ -17,4 +25,32 @@ export class AccountController {
|
|||||||
): Promise<GetAccountResponse> {
|
): Promise<GetAccountResponse> {
|
||||||
return this.accountService.getAccount(data)
|
return this.accountService.getAccount(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('AccountService', 'InitEmailChange')
|
||||||
|
public async initEmailChange(
|
||||||
|
data: InitEmailChangeRequest
|
||||||
|
): Promise<InitEmailChangeResponse> {
|
||||||
|
return this.accountService.initEmailChange(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('AccountService', 'ConfirmEmailChange')
|
||||||
|
public async confirmEmailChange(
|
||||||
|
data: ConfirmEmailChangeRequest
|
||||||
|
): Promise<ConfirmEmailChangeResponse> {
|
||||||
|
return this.accountService.confirmEmailChange(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('AccountService', 'InitPhoneChange')
|
||||||
|
public async initPhoneChange(
|
||||||
|
data: InitPhoneChangeRequest
|
||||||
|
): Promise<InitPhoneChangeResponse> {
|
||||||
|
return this.accountService.initPhoneChange(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('AccountService', 'ConfirmPhoneChange')
|
||||||
|
public async confirmPhoneChange(
|
||||||
|
data: ConfirmPhoneChangeRequest
|
||||||
|
): Promise<ConfirmPhoneChangeResponse> {
|
||||||
|
return this.accountService.confirmPhoneChange(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
import { AccountRepository } from '@/modules/account/account.repository'
|
import { AccountRepository } from '@/modules/account/account.repository'
|
||||||
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
import { UserRepository } from '@/shared/utils/repositories'
|
||||||
|
|
||||||
import { AccountController } from './account.controller'
|
import { AccountController } from './account.controller'
|
||||||
import { AccountService } from './account.service'
|
import { AccountService } from './account.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AccountController],
|
controllers: [AccountController],
|
||||||
providers: [AccountService, AccountRepository]
|
providers: [AccountService, AccountRepository, UserRepository, OtpService]
|
||||||
})
|
})
|
||||||
export class AccountModule {}
|
export class AccountModule {}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { Account } from '@prisma/generated/client'
|
||||||
|
|
||||||
import { PrismaService } from '@/infra/prisma/prisma.service'
|
import { PrismaService } from '@/infra/prisma/prisma.service'
|
||||||
|
|
||||||
@ -9,4 +10,38 @@ export class AccountRepository {
|
|||||||
public findById(id: string) {
|
public findById(id: string) {
|
||||||
return this.prismaService.account.findUnique({ where: { id } })
|
return this.prismaService.account.findUnique({ where: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findContactChange(accountId: Account['id'], type: 'email' | 'phone') {
|
||||||
|
return this.prismaService.pendingContactChange.findUnique({
|
||||||
|
where: { accountId_type: { accountId, type } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public upsertPendingChange(data: {
|
||||||
|
accountId: Account['id']
|
||||||
|
type: 'email' | 'phone'
|
||||||
|
value: string
|
||||||
|
codeHash: string
|
||||||
|
expiresAt: Date
|
||||||
|
}) {
|
||||||
|
return this.prismaService.pendingContactChange.upsert({
|
||||||
|
where: {
|
||||||
|
accountId_type: {
|
||||||
|
accountId: data.accountId,
|
||||||
|
type: data.type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: data,
|
||||||
|
update: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public deletePendingChange(
|
||||||
|
accountId: Account['id'],
|
||||||
|
type: 'email' | 'phone'
|
||||||
|
) {
|
||||||
|
return this.prismaService.pendingContactChange.delete({
|
||||||
|
where: { accountId_type: { accountId, type } }
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,27 @@ import { Injectable } from '@nestjs/common'
|
|||||||
import { RpcException } from '@nestjs/microservices'
|
import { RpcException } from '@nestjs/microservices'
|
||||||
import { Role } from '@prisma/generated/enums'
|
import { Role } from '@prisma/generated/enums'
|
||||||
import { convertEnum, RpcStatus } from '@teacinema/common'
|
import { convertEnum, RpcStatus } from '@teacinema/common'
|
||||||
import type {
|
import {
|
||||||
|
ConfirmEmailChangeRequest,
|
||||||
|
ConfirmPhoneChangeRequest,
|
||||||
GetAccountRequest,
|
GetAccountRequest,
|
||||||
GetAccountResponse
|
GetAccountResponse,
|
||||||
|
InitEmailChangeRequest,
|
||||||
|
InitPhoneChangeRequest
|
||||||
} from '@teacinema/contracts/gen/account'
|
} from '@teacinema/contracts/gen/account'
|
||||||
|
|
||||||
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
import { UserRepository } from '@/shared/utils/repositories'
|
||||||
|
|
||||||
import { AccountRepository } from './account.repository'
|
import { AccountRepository } from './account.repository'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
public constructor(private readonly accountRepository: AccountRepository) {}
|
public constructor(
|
||||||
|
private readonly accountRepository: AccountRepository,
|
||||||
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly otpService: OtpService
|
||||||
|
) {}
|
||||||
|
|
||||||
public async getAccount(
|
public async getAccount(
|
||||||
data: GetAccountRequest
|
data: GetAccountRequest
|
||||||
@ -36,4 +47,132 @@ export class AccountService {
|
|||||||
role: convertEnum(Role, account.role)
|
role: convertEnum(Role, account.role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async initEmailChange(data: InitEmailChangeRequest) {
|
||||||
|
const { email, userId } = data
|
||||||
|
|
||||||
|
const existing = await this.userRepository.findByEmail(email)
|
||||||
|
if (existing) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.ALREADY_EXISTS,
|
||||||
|
details: 'Email already in use'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, hash } = await this.otpService.send(email, 'email')
|
||||||
|
console.log('CODE', code)
|
||||||
|
await this.accountRepository.upsertPendingChange({
|
||||||
|
accountId: userId,
|
||||||
|
type: 'email',
|
||||||
|
value: email,
|
||||||
|
codeHash: hash,
|
||||||
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async confirmEmailChange(data: ConfirmEmailChangeRequest) {
|
||||||
|
const { code, userId, email } = data
|
||||||
|
|
||||||
|
const pending = await this.accountRepository.findContactChange(
|
||||||
|
userId,
|
||||||
|
'email'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!pending) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
details: 'No pending request'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.value !== email) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.INVALID_ARGUMENTS,
|
||||||
|
details: 'Email mismatch'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.expiresAt < new Date()) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
details: 'Code expired'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const newEmail = pending.value
|
||||||
|
|
||||||
|
await this.otpService.verify(newEmail, code, 'email')
|
||||||
|
await this.userRepository.updateAccount(userId, {
|
||||||
|
email: newEmail,
|
||||||
|
isEmailVerified: true
|
||||||
|
})
|
||||||
|
await this.accountRepository.deletePendingChange(userId, 'email')
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initPhoneChange(data: InitPhoneChangeRequest) {
|
||||||
|
const { phone, userId } = data
|
||||||
|
|
||||||
|
const existing = await this.userRepository.findByPhone(phone)
|
||||||
|
if (existing) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.ALREADY_EXISTS,
|
||||||
|
details: 'Phone already in use'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, hash } = await this.otpService.send(phone, 'phone')
|
||||||
|
console.log('CODE', code)
|
||||||
|
await this.accountRepository.upsertPendingChange({
|
||||||
|
accountId: userId,
|
||||||
|
type: 'phone',
|
||||||
|
value: phone,
|
||||||
|
codeHash: hash,
|
||||||
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async confirmPhoneChange(data: ConfirmPhoneChangeRequest) {
|
||||||
|
const { code, userId, phone } = data
|
||||||
|
|
||||||
|
const pending = await this.accountRepository.findContactChange(
|
||||||
|
userId,
|
||||||
|
'phone'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!pending) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
details: 'No pending request'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.value !== phone) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.INVALID_ARGUMENTS,
|
||||||
|
details: 'Phone mismatch'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.expiresAt < new Date()) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
details: 'Code expired'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const newPhone = pending.value
|
||||||
|
|
||||||
|
await this.otpService.verify(newPhone, code, 'phone')
|
||||||
|
await this.userRepository.updateAccount(userId, {
|
||||||
|
phone: newPhone,
|
||||||
|
isPhoneVerified: true
|
||||||
|
})
|
||||||
|
await this.accountRepository.deletePendingChange(userId, 'phone')
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { PassportModule } from '@teacinema/passport'
|
|||||||
|
|
||||||
import { getPassportConfig } from '@/config'
|
import { getPassportConfig } from '@/config'
|
||||||
import { OtpService } from '@/modules/otp/otp.service'
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
import { UserRepository } from '@/shared/utils/repositories'
|
||||||
|
|
||||||
import { AuthController } from './auth.controller'
|
import { AuthController } from './auth.controller'
|
||||||
import { AuthRepository } from './auth.repository'
|
import { AuthRepository } from './auth.repository'
|
||||||
@ -17,6 +18,6 @@ import { AuthService } from './auth.service'
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, AuthRepository, OtpService]
|
providers: [AuthService, AuthRepository, UserRepository, OtpService]
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
import { Account } from '@prisma/generated/client'
|
import { AccountCreateInput } from '@prisma/generated/models/Account'
|
||||||
import {
|
|
||||||
AccountCreateInput,
|
|
||||||
AccountUpdateInput
|
|
||||||
} from '@prisma/generated/models/Account'
|
|
||||||
|
|
||||||
import { PrismaService } from '@/infra/prisma/prisma.service'
|
import { PrismaService } from '@/infra/prisma/prisma.service'
|
||||||
|
|
||||||
@ -11,26 +7,7 @@ import { PrismaService } from '@/infra/prisma/prisma.service'
|
|||||||
export class AuthRepository {
|
export class AuthRepository {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
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) {
|
public createAccount(data: AccountCreateInput) {
|
||||||
return this.prismaService.account.create({ data })
|
return this.prismaService.account.create({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateAccount(id: Account['id'], data: AccountUpdateInput) {
|
|
||||||
return this.prismaService.account.update({
|
|
||||||
where: { id },
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { PassportService } from '@teacinema/passport'
|
|||||||
|
|
||||||
import { AllConfigs } from '@/config'
|
import { AllConfigs } from '@/config'
|
||||||
import { OtpService } from '@/modules/otp/otp.service'
|
import { OtpService } from '@/modules/otp/otp.service'
|
||||||
|
import { UserRepository } from '@/shared/utils/repositories'
|
||||||
|
|
||||||
import { AuthRepository } from './auth.repository'
|
import { AuthRepository } from './auth.repository'
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export class AuthService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly configService: ConfigService<AllConfigs>,
|
private readonly configService: ConfigService<AllConfigs>,
|
||||||
private readonly authRepository: AuthRepository,
|
private readonly authRepository: AuthRepository,
|
||||||
|
private readonly userRepository: UserRepository,
|
||||||
private readonly otpService: OtpService,
|
private readonly otpService: OtpService,
|
||||||
private readonly passportService: PassportService
|
private readonly passportService: PassportService
|
||||||
) {
|
) {
|
||||||
@ -45,9 +47,9 @@ export class AuthService {
|
|||||||
const isEmailType = type === 'email'
|
const isEmailType = type === 'email'
|
||||||
|
|
||||||
if (isPhoneType) {
|
if (isPhoneType) {
|
||||||
account = await this.authRepository.findByPhone(identifier)
|
account = await this.userRepository.findByPhone(identifier)
|
||||||
} else {
|
} else {
|
||||||
account = await this.authRepository.findByEmail(identifier)
|
account = await this.userRepository.findByEmail(identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@ -75,8 +77,8 @@ export class AuthService {
|
|||||||
const isEmailType = type === 'email'
|
const isEmailType = type === 'email'
|
||||||
|
|
||||||
const account = isPhoneType
|
const account = isPhoneType
|
||||||
? await this.authRepository.findByPhone(identifier)
|
? await this.userRepository.findByPhone(identifier)
|
||||||
: await this.authRepository.findByEmail(identifier)
|
: await this.userRepository.findByEmail(identifier)
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
@ -86,13 +88,13 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPhoneType && !account.isPhoneVerified) {
|
if (isPhoneType && !account.isPhoneVerified) {
|
||||||
await this.authRepository.updateAccount(account.id, {
|
await this.userRepository.updateAccount(account.id, {
|
||||||
isPhoneVerified: true
|
isPhoneVerified: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmailType && !account.isEmailVerified) {
|
if (isEmailType && !account.isEmailVerified) {
|
||||||
await this.authRepository.updateAccount(account.id, {
|
await this.userRepository.updateAccount(account.id, {
|
||||||
isEmailVerified: true
|
isEmailVerified: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export class OtpService {
|
|||||||
60 * 5
|
60 * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
return code
|
return { code, hash }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async verify(
|
public async verify(
|
||||||
|
|||||||
1
src/shared/utils/repositories/index.ts
Normal file
1
src/shared/utils/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './user.repository'
|
||||||
29
src/shared/utils/repositories/user.repository.ts
Normal file
29
src/shared/utils/repositories/user.repository.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { Account } from '@prisma/generated/client'
|
||||||
|
import { AccountUpdateInput } from '@prisma/generated/models/Account'
|
||||||
|
|
||||||
|
import { PrismaService } from '@/infra/prisma/prisma.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository {
|
||||||
|
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 updateAccount(id: Account['id'], data: AccountUpdateInput) {
|
||||||
|
return this.prismaService.account.update({
|
||||||
|
where: { id },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1383,9 +1383,9 @@
|
|||||||
integrity sha512-q3DURJbSk3k8MNWFIYaSM4LEcBgPbWa+HJmBz/nzYT4kuYitJVSXxpZ97kr0Ea+81AZwAU3JhQKlkK0SBWUi0A==
|
integrity sha512-q3DURJbSk3k8MNWFIYaSM4LEcBgPbWa+HJmBz/nzYT4kuYitJVSXxpZ97kr0Ea+81AZwAU3JhQKlkK0SBWUi0A==
|
||||||
|
|
||||||
"@teacinema/contracts@^1.0.0":
|
"@teacinema/contracts@^1.0.0":
|
||||||
version "1.0.6"
|
version "1.0.7"
|
||||||
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.6/contracts-1.0.6.tgz#7aa97d4c3081b791908c90d2a12d771b5fb79bd5"
|
resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.7/contracts-1.0.7.tgz#2c75a3118e4126d30f5c8193c4518bac3f1812ff"
|
||||||
integrity sha512-2IU2S9hWtKeMpn4zRS9pJAaSryIODrRjS1q9QezQOXF6554vtQlMVr0fLVTGhNmXlbRfuje0f6HggApqPDFMaA==
|
integrity sha512-lE14PO/yBphYCu0BGFGyGAyvJ74v+ACz34MXA3fFo/PwThPVpudBReSG6oRyUACRybkN882GKXFU6LrJXvGbrQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nestjs/microservices" "^11.1.12"
|
"@nestjs/microservices" "^11.1.12"
|
||||||
protoc "33.4.0"
|
protoc "33.4.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user