update oauth

This commit is contained in:
lendry
2026-06-25 14:40:05 +03:00
parent 1796008a28
commit 6c63343fc7
19 changed files with 623 additions and 200 deletions

View File

@@ -20,6 +20,7 @@ import { ProfileService } from './domain/profile.service';
import { DocumentsService } from './domain/documents.service';
import { AddressesService } from './domain/addresses.service';
import { OAuthCoreService } from './domain/oauth-core.service';
import { OAuthIssuerService } from './domain/oauth-issuer.service';
import { OtpService } from './domain/otp.service';
import { AdvancedAuthService } from './domain/advanced-auth.service';
import { FamilyService } from './domain/family.service';
@@ -59,6 +60,7 @@ import { MaintenanceSchedulerService } from './domain/maintenance-scheduler.serv
DocumentsService,
AddressesService,
OAuthCoreService,
OAuthIssuerService,
OtpService,
AdvancedAuthService,
TotpService,

View File

@@ -1,16 +1,17 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { randomBytes } from 'node:crypto';
import { PrismaService } from '../infra/prisma.service';
import { OAuthIssuerService } from './oauth-issuer.service';
@Injectable()
export class OAuthCoreService {
constructor(
private readonly prisma: PrismaService,
private readonly jwt: JwtService,
private readonly config: ConfigService
private readonly config: ConfigService,
private readonly oauthIssuer: OAuthIssuerService
) {}
async authorize(command: { userId: string; clientId: string; redirectUri: string; scope: string; state?: string }) {
@@ -70,9 +71,10 @@ export class OAuthCoreService {
}
private async issueOAuthTokens(userId: string, clientId: string, scopes: string[]) {
const accessToken = await this.jwt.signAsync({ sub: userId, aud: clientId, scopes, typ: 'access_token' }, { secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'), expiresIn: '15m', issuer: 'id.lendry.ru' });
const refreshToken = await this.jwt.signAsync({ sub: userId, aud: clientId, scopes, typ: 'refresh_token' }, { secret: this.config.getOrThrow<string>('JWT_REFRESH_SECRET'), expiresIn: '30d', issuer: 'id.lendry.ru' });
const idToken = await this.jwt.signAsync({ sub: userId, aud: clientId }, { secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'), expiresIn: '15m', issuer: 'id.lendry.ru' });
const issuer = await this.oauthIssuer.resolveIssuer();
const accessToken = await this.jwt.signAsync({ sub: userId, aud: clientId, scopes, typ: 'access_token' }, { secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'), expiresIn: '15m', issuer });
const refreshToken = await this.jwt.signAsync({ sub: userId, aud: clientId, scopes, typ: 'refresh_token' }, { secret: this.config.getOrThrow<string>('JWT_REFRESH_SECRET'), expiresIn: '30d', issuer });
const idToken = await this.jwt.signAsync({ sub: userId, aud: clientId }, { secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'), expiresIn: '15m', issuer });
return { accessToken, tokenType: 'Bearer', expiresIn: 900, refreshToken, idToken };
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SettingsService } from './settings.service';
@Injectable()
export class OAuthIssuerService {
constructor(
private readonly settings: SettingsService,
private readonly config: ConfigService
) {}
async resolveIssuer(): Promise<string> {
const fromDb = (await this.settings.getValue('PUBLIC_API_URL', '')).trim();
if (fromDb) {
return fromDb.replace(/\/+$/, '');
}
const fromEnv = this.config.get<string>('PUBLIC_API_URL')?.trim();
if (fromEnv) {
return fromEnv.replace(/\/+$/, '');
}
const domain = (await this.settings.getValue('PROJECT_DOMAIN', 'id.lendry.ru')).trim();
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain.replace(/\/+$/, '');
}
return `https://${domain.replace(/^\/+/, '')}`;
}
}

View File

@@ -4,7 +4,12 @@ import { PrismaService } from '../infra/prisma.service';
export const DEFAULT_SYSTEM_SETTINGS = [
{ key: 'PROJECT_NAME', value: 'MVK ID', description: 'Название проекта в интерфейсе и заголовке страницы' },
{ key: 'PROJECT_TAGLINE', value: 'Единый аккаунт для сервисов Lendry', description: 'Краткий слоган под названием проекта' },
{ key: 'PROJECT_DOMAIN', value: 'id.lendry.ru', description: 'Основной домен IdP' },
{ key: 'PROJECT_DOMAIN', value: 'id.lendry.ru', description: 'Основной домен IdP (без пути)' },
{
key: 'PUBLIC_API_URL',
value: 'http://localhost:3000',
description: 'Публичный базовый URL API для OAuth и OpenID Connect (issuer). Пример: https://sso.example.ru/idp-api'
},
{ key: 'PIN_LOCK_TIMEOUT_MINUTES', value: '15', description: 'Через сколько минут неактивности сессия блокируется PIN-кодом' },
{ key: 'PIN_DELETE_GRACE_MINUTES', value: '1440', description: 'Задержка перед окончательным удалением PIN-кода после запроса (минуты)' },
{ key: 'PIN_REQUIRE_ON_DELETE', value: 'true', description: 'Требовать текущий PIN-код при запросе удаления защиты' },
@@ -91,6 +96,7 @@ export const PUBLIC_SETTING_KEYS = [
'PROJECT_NAME',
'PROJECT_TAGLINE',
'PROJECT_DOMAIN',
'PUBLIC_API_URL',
'REGISTRATION_ENABLED',
'MAINTENANCE_MODE',
'LDAP_ENABLED',
@@ -109,5 +115,18 @@ export class SystemSettingsSeedService implements OnModuleInit {
update: { description: setting.description, isSecret: SECRET_SETTING_KEYS.has(setting.key) }
});
}
const envPublicApiUrl = process.env.PUBLIC_API_URL?.trim();
if (envPublicApiUrl) {
await this.prisma.systemSetting.upsert({
where: { key: 'PUBLIC_API_URL' },
create: {
key: 'PUBLIC_API_URL',
value: envPublicApiUrl,
description: 'Публичный базовый URL API для OAuth и OpenID Connect (issuer). Пример: https://sso.example.ru/idp-api'
},
update: { value: envPublicApiUrl }
});
}
}
}