Add Verifik RETHUS integration: backend module, settings auth flow, admin query button
- verifik.service.ts: email OTP auth, token storage/refresh, RETHUS query by professional - verifik.controller.ts: send-otp, confirm, refresh, rethus/professional/:id endpoints - settings/page.tsx: Verifik connection section (email→OTP→token) - professionals/[id]/page.tsx: Consultar RETHUS button with inline result display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
66bc47b6e1
commit
0a67f5d80a
@@ -14,6 +14,7 @@ import { StorageModule } from './storage/storage.module';
|
||||
import { NotificationsModule } from './notifications/notifications.module';
|
||||
import { SmsModule } from './sms/sms.module';
|
||||
import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||
import { VerifikModule } from './verifik/verifik.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,6 +33,7 @@ import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||
NotificationsModule,
|
||||
SmsModule,
|
||||
SuggestionsModule,
|
||||
VerifikModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Controller, Post, Get, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { VerifikService } from './verifik.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
@ApiTags('Verifik')
|
||||
@Controller('verifik')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class VerifikController {
|
||||
constructor(private verifik: VerifikService) {}
|
||||
|
||||
@Get('status')
|
||||
status() { return this.verifik.getStatus(); }
|
||||
|
||||
@Post('send-otp')
|
||||
sendOtp(@Body() body: { email: string }) {
|
||||
return this.verifik.sendOtp(body.email);
|
||||
}
|
||||
|
||||
@Post('confirm')
|
||||
confirm(@Body() body: { email: string; otp: string }) {
|
||||
return this.verifik.confirmOtp(body.email, body.otp);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
refresh() { return this.verifik.refreshToken(); }
|
||||
|
||||
// Query by professional ID — looks up cedula from DB
|
||||
@Get('rethus/professional/:id')
|
||||
rethusByProfessional(@Param('id') id: string) {
|
||||
return this.verifik.queryRethusByProfessional(id);
|
||||
}
|
||||
|
||||
// Direct query by document
|
||||
@Get('rethus')
|
||||
rethus(
|
||||
@Query('documentType') documentType: string,
|
||||
@Query('documentNumber') documentNumber: string,
|
||||
) {
|
||||
return this.verifik.queryRethus(documentType, documentNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { VerifikService } from './verifik.service';
|
||||
import { VerifikController } from './verifik.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
providers: [VerifikService],
|
||||
controllers: [VerifikController],
|
||||
exports: [VerifikService],
|
||||
})
|
||||
export class VerifikModule {}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const BASE = 'https://api.verifik.co';
|
||||
|
||||
@Injectable()
|
||||
export class VerifikService {
|
||||
private readonly logger = new Logger(VerifikService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
// ── token storage ──────────────────────────────────────────────────────────
|
||||
|
||||
private async getStoredToken(): Promise<string | null> {
|
||||
const s = await this.prisma.settings.findUnique({ where: { key: 'verifik_config' } });
|
||||
return (s?.value as any)?.token ?? null;
|
||||
}
|
||||
|
||||
private async saveToken(token: string) {
|
||||
await this.prisma.settings.upsert({
|
||||
where: { key: 'verifik_config' },
|
||||
create: { key: 'verifik_config', value: { token } },
|
||||
update: { value: { token } },
|
||||
});
|
||||
}
|
||||
|
||||
// ── auth flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
async sendOtp(email: string) {
|
||||
const res = await fetch(`${BASE}/v2/projects/email-login?email=${encodeURIComponent(email)}`, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error((body as any).message || `Error ${res.status}`);
|
||||
}
|
||||
return { message: 'OTP enviado al correo' };
|
||||
}
|
||||
|
||||
async confirmOtp(email: string, otp: string) {
|
||||
const res = await fetch(`${BASE}/v2/projects/email-login/confirm`, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otp }),
|
||||
});
|
||||
const body = await res.json().catch(() => ({})) as any;
|
||||
if (!res.ok) throw new Error(body.message || `Error ${res.status}`);
|
||||
const token: string = body.data?.accessToken ?? body.accessToken;
|
||||
if (!token) throw new Error('No se recibió token');
|
||||
await this.saveToken(token);
|
||||
return { message: 'Conectado correctamente' };
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
const token = await this.getStoredToken();
|
||||
if (!token) throw new Error('No hay token guardado');
|
||||
const res = await fetch(`${BASE}/v2/auth/session?origin=refresh&expiresIn=1`, {
|
||||
headers: { Accept: 'application/json', Authorization: token },
|
||||
});
|
||||
const body = await res.json().catch(() => ({})) as any;
|
||||
if (!res.ok) throw new Error(body.message || `Error ${res.status}`);
|
||||
const newToken: string = body.accessToken ?? body.data?.accessToken;
|
||||
if (!newToken) throw new Error('No se recibió token renovado');
|
||||
await this.saveToken(newToken);
|
||||
return { message: 'Token renovado' };
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
const token = await this.getStoredToken();
|
||||
if (!token) return { connected: false };
|
||||
// Quick validation — session endpoint with no origin param just validates
|
||||
const res = await fetch(`${BASE}/v2/auth/session`, {
|
||||
headers: { Accept: 'application/json', Authorization: token },
|
||||
});
|
||||
return { connected: res.ok };
|
||||
}
|
||||
|
||||
// ── RETHUS lookup ──────────────────────────────────────────────────────────
|
||||
|
||||
async queryRethusByProfessional(professionalId: string) {
|
||||
const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } });
|
||||
if (!prof) throw new Error('Profesional no encontrado');
|
||||
if (!prof.identification) throw new Error('El profesional no tiene número de cédula registrado');
|
||||
return this.queryRethus('CC', prof.identification);
|
||||
}
|
||||
|
||||
async queryRethus(documentType: string, documentNumber: string) {
|
||||
let token = await this.getStoredToken();
|
||||
if (!token) throw new Error('Verifik no está configurado. Conecta tu cuenta en Configuración.');
|
||||
|
||||
const call = async (t: string) =>
|
||||
fetch(`${BASE}/v2/co/cedula/rethus?documentType=${documentType}&documentNumber=${documentNumber}`, {
|
||||
headers: { Accept: 'application/json', Authorization: `Bearer ${t}` },
|
||||
});
|
||||
|
||||
let res = await call(token);
|
||||
|
||||
// Auto-refresh on 401
|
||||
if (res.status === 401) {
|
||||
this.logger.log('Token Verifik expirado, renovando…');
|
||||
await this.refreshToken();
|
||||
token = await this.getStoredToken();
|
||||
res = await call(token!);
|
||||
}
|
||||
|
||||
const body = await res.json().catch(() => ({})) as any;
|
||||
if (!res.ok) throw new Error(body.message || `Error Verifik ${res.status}`);
|
||||
return body.data ?? body;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user