Backend: - CRUD completo para countries, regions y cities - GET /settings/policy/:key — página HTML pública para políticas - GET/PATCH /settings/policies/:key — admin endpoints protegidos Admin: - /locations: árbol interactivo País → Región → Ciudad con add/edit/delete - /settings: editor de Política de Privacidad y Términos con enlace público copiable - Sidebar: Ciudades → Ubicaciones Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
import { Controller, Get, Patch, Body, UseGuards, Param, Res } from '@nestjs/common';
|
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { Response } from 'express';
|
|
import { SettingsService } from './settings.service';
|
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|
|
|
@ApiTags('Settings')
|
|
@Controller('settings')
|
|
export class SettingsController {
|
|
constructor(private settings: SettingsService) {}
|
|
|
|
@Get()
|
|
getGlobal() { return this.settings.getGlobal(); }
|
|
|
|
@Patch()
|
|
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
|
updateGlobal(@Body() body: Record<string, any>) {
|
|
return this.settings.updateGlobal(body);
|
|
}
|
|
|
|
// Policies admin (protected)
|
|
@Get('policies')
|
|
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
|
getPolicies() { return this.settings.getPolicies(); }
|
|
|
|
@Patch('policies/:key')
|
|
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
|
updatePolicy(@Param('key') key: 'privacy' | 'terms', @Body() body: { content: string }) {
|
|
return this.settings.updatePolicy(key, body.content);
|
|
}
|
|
|
|
// Public policy pages
|
|
@Get('policy/:key')
|
|
async getPublicPolicy(@Param('key') key: string, @Res() res: Response) {
|
|
const content = await this.settings.getPolicy(key);
|
|
const titles: Record<string, string> = {
|
|
privacy: 'Politica de Privacidad',
|
|
terms: 'Terminos y Condiciones',
|
|
};
|
|
const title = titles[key] || 'Politica';
|
|
|
|
if (!content) {
|
|
return res.status(404).send(`<html><body><h1>${title}</h1><p>No disponible aun.</p></body></html>`);
|
|
}
|
|
|
|
const html = `<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title} - ProsApp</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1e293b; }
|
|
header { background: linear-gradient(135deg, #42A4EF, #1565C0); padding: 24px 0; text-align: center; }
|
|
header img { height: 40px; margin-bottom: 8px; }
|
|
header h1 { color: white; font-size: 1.5rem; font-weight: 700; }
|
|
.container { max-width: 800px; margin: 32px auto; padding: 0 16px 64px; }
|
|
.card { background: white; border-radius: 12px; padding: 40px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
.content { white-space: pre-wrap; line-height: 1.8; font-size: 0.95rem; color: #374151; }
|
|
.content h1, .content h2, .content h3 { color: #1e293b; margin: 1.5em 0 0.5em; font-weight: 600; }
|
|
.content p { margin-bottom: 1em; }
|
|
footer { text-align: center; padding: 24px; color: #94a3b8; font-size: 0.8rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<img src="https://prosapp.co/img/logo_prosapp.png" alt="ProsApp" onerror="this.style.display='none'">
|
|
<h1>${title}</h1>
|
|
</header>
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="content">${content.replace(/</g, '<').replace(/>/g, '>')}</div>
|
|
</div>
|
|
</div>
|
|
<footer>© ${new Date().getFullYear()} ProsApp. Todos los derechos reservados.</footer>
|
|
</body>
|
|
</html>`;
|
|
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
return res.send(html);
|
|
}
|
|
}
|