fix idp on started
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Controller, UseFilters } from '@nestjs/common';
|
||||
import { BadRequestException, Controller, UseFilters } from '@nestjs/common';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { GrpcExceptionFilter } from '../infra/grpc-exception.filter';
|
||||
import { AdminService } from './admin.service';
|
||||
@@ -20,6 +20,7 @@ import { FamilyService } from './family.service';
|
||||
import { MediaService } from './media.service';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { ChatService } from './chat.service';
|
||||
import { MessagingService } from '../infra/messaging.service';
|
||||
|
||||
@Controller()
|
||||
@UseFilters(new GrpcExceptionFilter())
|
||||
@@ -41,7 +42,8 @@ export class AuthGrpcController {
|
||||
private readonly family: FamilyService,
|
||||
private readonly media: MediaService,
|
||||
private readonly notifications: NotificationsService,
|
||||
private readonly chat: ChatService
|
||||
private readonly chat: ChatService,
|
||||
private readonly messaging: MessagingService
|
||||
) {}
|
||||
|
||||
@GrpcMethod('AuthService', 'Register')
|
||||
@@ -437,6 +439,15 @@ export class AuthGrpcController {
|
||||
return this.settings.deleteSocialProvider(command.providerName);
|
||||
}
|
||||
|
||||
@GrpcMethod('SettingsService', 'TestMessagingDelivery')
|
||||
async testMessagingDelivery(command: { channel: string; target: string }) {
|
||||
const channel = command.channel === 'sms' ? 'sms' : 'email';
|
||||
if (!command.target?.trim()) {
|
||||
throw new BadRequestException('Укажите email или номер телефона для тестовой отправки');
|
||||
}
|
||||
return this.messaging.sendTestDelivery(channel, command.target.trim());
|
||||
}
|
||||
|
||||
@GrpcMethod('ProfileService', 'GetProfile')
|
||||
getProfile(command: { userId: string }) {
|
||||
return this.profile.getProfile(command.userId);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { normalizeLdapUserFilter, resolveLdapBindIdentity, resolveLdapPort, vali
|
||||
import { AuthTokens, LoginCommand, PublicUser, RegisterCommand } from './dto';
|
||||
import { AccessService } from './access.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { MessagingService } from '../infra/messaging.service';
|
||||
|
||||
const FIRST_ADMIN_LOCK = 'locks:first-super-admin';
|
||||
|
||||
@@ -23,7 +24,8 @@ export class AuthService {
|
||||
private readonly config: ConfigService,
|
||||
private readonly access: AccessService,
|
||||
private readonly settings: SettingsService,
|
||||
private readonly ldapClient: LdapClientService
|
||||
private readonly ldapClient: LdapClientService,
|
||||
private readonly messaging: MessagingService
|
||||
) {}
|
||||
|
||||
async register(command: RegisterCommand): Promise<PublicUser> {
|
||||
@@ -131,19 +133,27 @@ export class AuthService {
|
||||
async sendOtp(recipient: string, channel?: string) {
|
||||
const existingUser = await this.findUserByRecipient(recipient);
|
||||
const target = this.resolveOtpTarget(recipient, channel, existingUser);
|
||||
const deliveryChannel = target.includes('@') ? 'email' : 'sms';
|
||||
const expiryMinutes = await this.settings.getNumber('OTP_EXPIRY_MINUTES', 10);
|
||||
const code = String(randomInt(100000, 999999));
|
||||
const expiresAt = new Date(Date.now() + 5 * 60_000);
|
||||
const expiresAt = new Date(Date.now() + expiryMinutes * 60_000);
|
||||
await this.prisma.authCode.create({
|
||||
data: {
|
||||
userId: existingUser?.id,
|
||||
target,
|
||||
channel: target.includes('@') ? 'email' : 'sms',
|
||||
channel: deliveryChannel,
|
||||
purpose: 'passwordless-login',
|
||||
codeHash: await bcrypt.hash(code, 10),
|
||||
expiresAt
|
||||
}
|
||||
});
|
||||
console.log('[OTP MOCK] Code for', target, 'is:', code);
|
||||
await this.messaging.sendVerificationCode({
|
||||
channel: deliveryChannel as 'email' | 'sms',
|
||||
target,
|
||||
code,
|
||||
expiryMinutes
|
||||
});
|
||||
await this.redis.client.del(this.otpAttemptKey(target));
|
||||
return { sent: true, expiresAt: expiresAt.toISOString(), maskedTarget: this.maskTarget(target) };
|
||||
}
|
||||
|
||||
@@ -160,8 +170,20 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!authCode) throw new UnauthorizedException('Код входа не найден или истек');
|
||||
|
||||
const attemptKey = this.otpAttemptKey(authCode.target);
|
||||
const maxAttempts = await this.settings.getNumber('OTP_MAX_ATTEMPTS', 5);
|
||||
const attempts = Number((await this.redis.client.get(attemptKey)) ?? 0);
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new UnauthorizedException('Превышено число попыток ввода кода');
|
||||
}
|
||||
|
||||
const matches = await bcrypt.compare(command.code, authCode.codeHash);
|
||||
if (!matches) throw new UnauthorizedException('Неверный код входа');
|
||||
if (!matches) {
|
||||
await this.redis.client.set(attemptKey, String(attempts + 1), 'EX', 15 * 60);
|
||||
throw new UnauthorizedException('Неверный код входа');
|
||||
}
|
||||
await this.redis.client.del(attemptKey);
|
||||
await this.prisma.authCode.update({ where: { id: authCode.id }, data: { usedAt: new Date() } });
|
||||
|
||||
const user = await this.findOrCreatePasswordlessUser(command.recipient);
|
||||
@@ -183,6 +205,10 @@ export class AuthService {
|
||||
return map[channel] ?? recipient;
|
||||
}
|
||||
|
||||
private otpAttemptKey(target: string) {
|
||||
return `otp:attempts:passwordless-login:${target.toLowerCase()}`;
|
||||
}
|
||||
|
||||
private maskTarget(value: string) {
|
||||
if (value.includes('@')) {
|
||||
const [name, domain] = value.split('@');
|
||||
|
||||
@@ -3,12 +3,16 @@ import * as bcrypt from 'bcryptjs';
|
||||
import { randomInt } from 'node:crypto';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { MessagingService } from '../infra/messaging.service';
|
||||
import { RedisService } from '../infra/redis.service';
|
||||
|
||||
@Injectable()
|
||||
export class OtpService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly settings: SettingsService
|
||||
private readonly settings: SettingsService,
|
||||
private readonly messaging: MessagingService,
|
||||
private readonly redis: RedisService
|
||||
) {}
|
||||
|
||||
async send(command: { target: string; channel: string; purpose: string; userId?: string }) {
|
||||
@@ -25,7 +29,14 @@ export class OtpService {
|
||||
expiresAt
|
||||
}
|
||||
});
|
||||
console.log(`OTP ${command.channel} для ${command.target}: ${code}`);
|
||||
const channel = command.channel === 'sms' ? 'sms' : 'email';
|
||||
await this.messaging.sendVerificationCode({
|
||||
channel,
|
||||
target: command.target,
|
||||
code,
|
||||
expiryMinutes
|
||||
});
|
||||
await this.redis.client.del(this.attemptKey(command.target, command.purpose));
|
||||
return { sent: true, expiresAt: expiresAt.toISOString() };
|
||||
}
|
||||
|
||||
@@ -41,9 +52,25 @@ export class OtpService {
|
||||
});
|
||||
|
||||
if (!authCode) throw new BadRequestException('Код подтверждения не найден или истек');
|
||||
|
||||
const attemptKey = this.attemptKey(command.target, command.purpose);
|
||||
const maxAttempts = await this.settings.getNumber('OTP_MAX_ATTEMPTS', 5);
|
||||
const attempts = Number((await this.redis.client.get(attemptKey)) ?? 0);
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new UnauthorizedException('Превышено число попыток ввода кода');
|
||||
}
|
||||
|
||||
const matches = await bcrypt.compare(command.code, authCode.codeHash);
|
||||
if (!matches) throw new UnauthorizedException('Неверный код подтверждения');
|
||||
if (!matches) {
|
||||
await this.redis.client.set(attemptKey, String(attempts + 1), 'EX', 15 * 60);
|
||||
throw new UnauthorizedException('Неверный код подтверждения');
|
||||
}
|
||||
await this.redis.client.del(attemptKey);
|
||||
await this.prisma.authCode.update({ where: { id: authCode.id }, data: { usedAt: new Date() } });
|
||||
return { verified: true };
|
||||
}
|
||||
|
||||
private attemptKey(target: string, purpose: string) {
|
||||
return `otp:attempts:${purpose}:${target.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,25 @@ export const DEFAULT_SYSTEM_SETTINGS = [
|
||||
},
|
||||
{ key: 'LDAP_USERNAME_ATTR', value: 'sAMAccountName', description: 'Атрибут LDAP с логином пользователя' },
|
||||
{ key: 'LDAP_EMAIL_ATTR', value: 'mail', description: 'Атрибут LDAP с почтой пользователя' },
|
||||
{ key: 'LDAP_DISPLAY_NAME_ATTR', value: 'displayName', description: 'Атрибут LDAP с отображаемым именем' }
|
||||
{ key: 'LDAP_DISPLAY_NAME_ATTR', value: 'displayName', description: 'Атрибут LDAP с отображаемым именем' },
|
||||
{ key: 'EMAIL_ENABLED', value: 'false', description: 'Отправлять OTP-коды на email через настроенный SMTP' },
|
||||
{ key: 'EMAIL_PROVIDER', value: 'smtp', description: 'Провайдер email: smtp или console (только лог)' },
|
||||
{ key: 'EMAIL_SMTP_HOST', value: '', description: 'SMTP-сервер, например smtp.yandex.ru или smtp.gmail.com' },
|
||||
{ key: 'EMAIL_SMTP_PORT', value: '587', description: 'Порт SMTP (587 — STARTTLS, 465 — SSL)' },
|
||||
{ key: 'EMAIL_SMTP_SECURE', value: 'false', description: 'SSL/TLS при подключении (true для порта 465)' },
|
||||
{ key: 'EMAIL_SMTP_USER', value: '', description: 'Логин SMTP (обычно совпадает с адресом отправителя)' },
|
||||
{ key: 'EMAIL_SMTP_PASSWORD', value: '', description: 'Пароль или app-password SMTP' },
|
||||
{ key: 'EMAIL_FROM_ADDRESS', value: '', description: 'Адрес отправителя, например noreply@example.com' },
|
||||
{ key: 'EMAIL_FROM_NAME', value: '', description: 'Имя отправителя в письме (пусто = название проекта)' },
|
||||
{ key: 'SMS_ENABLED', value: 'false', description: 'Отправлять OTP-коды по SMS через настроенного провайдера' },
|
||||
{ key: 'SMS_PROVIDER', value: 'smsru', description: 'SMS-провайдер: smsru, smsc или console (только лог)' },
|
||||
{ key: 'SMS_API_KEY', value: '', description: 'API-ключ sms.ru (api_id)' },
|
||||
{ key: 'SMS_LOGIN', value: '', description: 'Логин smsc.ru' },
|
||||
{ key: 'SMS_PASSWORD', value: '', description: 'Пароль smsc.ru' },
|
||||
{ key: 'SMS_SENDER', value: '', description: 'Имя отправителя SMS (если поддерживается провайдером)' }
|
||||
] as const;
|
||||
|
||||
const SECRET_SETTING_KEYS = new Set(['LDAP_BIND_PASSWORD']);
|
||||
const SECRET_SETTING_KEYS = new Set(['LDAP_BIND_PASSWORD', 'EMAIL_SMTP_PASSWORD', 'SMS_API_KEY', 'SMS_PASSWORD']);
|
||||
|
||||
export const PUBLIC_SETTING_KEYS = [
|
||||
'PROJECT_NAME',
|
||||
|
||||
Reference in New Issue
Block a user