Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
2d8fe625e8 Test 2026-03-03 17:19:21 +00:00
6 changed files with 357 additions and 155 deletions

View File

@ -1,4 +1,4 @@
require('dotenv').config();
module.exports = { module.exports = {
production: { production: {
@ -12,11 +12,12 @@ module.exports = {
seederStorage: 'sequelize', seederStorage: 'sequelize',
}, },
development: { development: {
username: 'postgres', username: process.env.DB_USER || 'postgres',
dialect: 'postgres', dialect: 'postgres',
password: '', password: process.env.DB_PASS || '',
database: 'db_renacer_certificados_saas', database: process.env.DB_NAME || 'db_renacer_certificados_saas',
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
logging: console.log, logging: console.log,
seederStorage: 'sequelize', seederStorage: 'sequelize',
}, },
@ -30,4 +31,4 @@ module.exports = {
logging: console.log, logging: console.log,
seederStorage: 'sequelize', seederStorage: 'sequelize',
} }
}; };

View File

@ -0,0 +1,45 @@
const { v4: uuid } = require('uuid');
module.exports = {
async up(queryInterface, Sequelize) {
const createdAt = new Date();
const updatedAt = new Date();
// Get the Public role ID
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1`
);
if (!roles || roles.length === 0) {
console.error("Public role not found");
return;
}
const publicRoleId = roles[0].id;
// Get permission IDs
const [permissions] = await queryInterface.sequelize.query(
`SELECT id, name FROM "permissions" WHERE name IN ('READ_CERTIFICATES', 'READ_USERS', 'READ_COMMUNITIES')`
);
const rolePermissions = permissions.map(p => ({
createdAt,
updatedAt,
roles_permissionsId: publicRoleId,
permissionId: p.id
}));
// Use ignoreDuplicates to avoid errors if already present
for (const rp of rolePermissions) {
await queryInterface.sequelize.query(
`INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
VALUES ('${rp.createdAt.toISOString()}', '${rp.updatedAt.toISOString()}', '${rp.roles_permissionsId}', '${rp.permissionId}')
ON CONFLICT DO NOTHING`
);
}
},
async down(queryInterface, Sequelize) {
// Revert logic
}
};

View File

@ -121,7 +121,7 @@ app.use('/api/organization_settings', passport.authenticate('jwt', {session: fal
app.use('/api/certificate_templates', passport.authenticate('jwt', {session: false}), certificate_templatesRoutes); app.use('/api/certificate_templates', passport.authenticate('jwt', {session: false}), certificate_templatesRoutes);
app.use('/api/certificates', passport.authenticate('jwt', {session: false}), certificatesRoutes); app.use('/api/certificates', certificatesRoutes);
app.use('/api/payments_log', passport.authenticate('jwt', {session: false}), payments_logRoutes); app.use('/api/payments_log', passport.authenticate('jwt', {session: false}), payments_logRoutes);

View File

@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20' export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
export const appTitle = 'created by Flatlogic generator!' export const appTitle = 'Corporación Renacer - DDHH Mundiales'
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle}${appTitle}` export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle}${appTitle}`

View File

@ -1,166 +1,157 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import BaseButton from '../components/BaseButton'; import { useRouter } from 'next/router';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks'; import { mdiShieldCheck, mdiAccountGroup, mdiCertificate, mdiMagnify } from '@mdi/js';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; import BaseIcon from '../components/BaseIcon';
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function LandingPage() {
const router = useRouter();
const [searchCode, setSearchCode] = useState('');
export default function Starter() { const handleSearch = (e: React.FormEvent) => {
const [illustrationImage, setIllustrationImage] = useState({ e.preventDefault();
src: undefined, if (searchCode.trim()) {
photographer: undefined, router.push(`/verify?code=${searchCode.trim()}`);
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Renacer Certificados SaaS'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
} }
}; };
return ( return (
<div <div className="min-h-screen bg-white text-gray-900">
style={ <Head>
contentPosition === 'background' <title>{getPageTitle('Inicio')}</title>
? { <meta name="description" content="Corporación Renacer DDHH Mundiales - Plataforma de Certificación de Líderes Sociales" />
backgroundImage: `${ </Head>
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head>
<title>{getPageTitle('Starter Page')}</title>
</Head>
<SectionFullScreen bg='violet'> {/* Navigation */}
<div <nav className="flex items-center justify-between p-6 lg:px-8 border-b" aria-label="Global">
className={`flex ${ <div className="flex lg:flex-1">
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row' <Link href="/" className="-m-1.5 p-1.5 flex items-center gap-2">
} min-h-screen w-full`} <div className="bg-blue-900 p-2 rounded-lg text-white">
> <BaseIcon path={mdiShieldCheck} size={24} />
{contentType === 'image' && contentPosition !== 'background' </div>
? imageBlock(illustrationImage) <span className="text-xl font-bold tracking-tight text-blue-900 uppercase">Renacer DDHH</span>
: null} </Link>
{contentType === 'video' && contentPosition !== 'background' </div>
? videoBlock(illustrationVideo) <div className="flex gap-x-6">
: null} <Link href="/login" className="text-sm font-semibold leading-6 text-gray-900">
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'> Acceso Administrativo
<CardBox className='w-full md:w-3/5 lg:w-2/3'> </Link>
<CardBoxComponentTitle title="Welcome to your Renacer Certificados SaaS app!"/> <Link href="/register" className="text-sm font-semibold leading-6 text-blue-900 border border-blue-900 px-4 py-1 rounded-md hover:bg-blue-50">
Registrarse como Líder
<div className="space-y-3"> </Link>
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p> </div>
<p className='text-center text-gray-500'>For guides and documentation please check </nav>
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
{/* Hero Section */}
<div className="relative isolate px-6 pt-14 lg:px-8 bg-gradient-to-b from-blue-50 to-white">
<div className="mx-auto max-w-2xl py-24 sm:py-32">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Empoderando Líderes Sociales en Defensa de los Derechos Humanos
</h1>
<p className="mt-6 text-lg leading-8 text-gray-600">
Validamos y certificamos la labor de quienes transforman comunidades.
Garantizamos transparencia, verificabilidad y credibilidad internacional.
</p>
{/* Verification Box */}
<div className="mt-10 flex flex-col items-center justify-center gap-y-4">
<form onSubmit={handleSearch} className="w-full max-w-md flex items-center bg-white rounded-full shadow-lg border p-1 focus-within:ring-2 focus-within:ring-blue-500 transition-all">
<div className="pl-4 text-gray-400">
<BaseIcon path={mdiMagnify} size={24} />
</div>
<input
type="text"
placeholder="Ingrese código de certificado..."
className="flex-grow px-4 py-3 outline-none rounded-full text-gray-700"
value={searchCode}
onChange={(e) => setSearchCode(e.target.value)}
/>
<button type="submit" className="bg-blue-900 text-white px-6 py-3 rounded-full font-bold hover:bg-blue-800 transition-colors">
Verificar
</button>
</form>
<p className="text-sm text-gray-500">¿Tienes un código QR? Escanéalo para verificación instantánea.</p>
</div>
</div>
</div>
</div> </div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons> {/* Features */}
</CardBox> <div className="py-24 sm:py-32 bg-white">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl lg:text-center">
<h2 className="text-base font-semibold leading-7 text-blue-900 uppercase tracking-widest">Nuestra Misión</h2>
<p className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Credibilidad Digital para el Impacto Social
</p>
</div>
<div className="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-none">
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
<div className="flex flex-col items-center text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
<BaseIcon path={mdiCertificate} size={32} />
</div>
<dt className="text-xl font-bold leading-7 text-gray-900">Certificación Oficial</dt>
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
<p>Emitimos certificados digitales con validez anual, respaldando la formación y trayectoria de líderes en DDHH.</p>
</dd>
</div>
<div className="flex flex-col items-center text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
<BaseIcon path={mdiShieldCheck} size={32} />
</div>
<dt className="text-xl font-bold leading-7 text-gray-900">Verificabilidad Inmediata</dt>
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
<p>Cualquier entidad puede validar la autenticidad de un certificado mediante QR o código único en tiempo real.</p>
</dd>
</div>
<div className="flex flex-col items-center text-center">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
<BaseIcon path={mdiAccountGroup} size={32} />
</div>
<dt className="text-xl font-bold leading-7 text-gray-900">Comunidad de Líderes</dt>
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
<p>Más que un papel, es el acceso a una red global comprometida con la paz y la justicia social en Colombia y el mundo.</p>
</dd>
</div>
</dl>
</div>
</div>
</div>
{/* Footer */}
<footer className="bg-gray-900 text-white py-12">
<div className="mx-auto max-w-7xl px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-8">
<div className="flex flex-col gap-4">
<span className="text-2xl font-bold text-white tracking-widest uppercase">Renacer DDHH</span>
<p className="text-gray-400 text-sm max-w-xs">Corporación Renacer Derechos Humanos Mundiales. NIT: 9018960153. Sede Principal: Colombia.</p>
</div>
<div className="flex gap-x-12">
<div className="flex flex-col gap-2">
<h4 className="font-bold text-amber-400">Institucional</h4>
<Link href="#" className="text-sm text-gray-400 hover:text-white">Sobre Nosotros</Link>
<Link href="#" className="text-sm text-gray-400 hover:text-white">Misión y Visión</Link>
</div>
<div className="flex flex-col gap-2">
<h4 className="font-bold text-amber-400">Legal</h4>
<Link href="/terms-of-use" className="text-sm text-gray-400 hover:text-white">Términos</Link>
<Link href="/privacy-policy" className="text-sm text-gray-400 hover:text-white">Privacidad</Link>
</div>
</div>
</div>
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
© 2026 Corporación Renacer DDHH Mundiales. Todos los derechos reservados.
</div>
</footer>
</div> </div>
</div> );
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
);
} }
Starter.getLayout = function getLayout(page: ReactElement) { LandingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -0,0 +1,165 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import axios from 'axios';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
import { mdiShieldCheck, mdiAlertCircle, mdiCheckDecagram, mdiClockOutline, mdiCloseOctagon } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import Link from 'next/link';
export default function VerificationPage() {
const router = useRouter();
const { code } = router.query;
const [loading, setLoading] = useState(true);
const [certificate, setCertificate] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (code) {
fetchCertificate(code as string);
} else if (router.isReady && !code) {
setLoading(false);
setError('No se proporcionó un código de verificación.');
}
}, [code, router.isReady]);
const fetchCertificate = async (searchCode: string) => {
try {
setLoading(true);
setError(null);
// Search by code
const response = await axios.get(`/certificates?certificate_code=${searchCode}`);
const data = response.data;
if (data.rows && data.rows.length > 0) {
setCertificate(data.rows[0]);
} else {
setError('Certificado no encontrado. Verifique el código e intente nuevamente.');
}
} catch (err) {
console.error(err);
setError('Error al conectar con el servidor de validación.');
} finally {
setLoading(false);
}
};
const getStatusInfo = (status: string) => {
switch (status) {
case 'ACTIVO':
return { color: 'text-green-600', bg: 'bg-green-100', icon: mdiCheckDecagram, label: 'VÁLIDO / ACTIVO' };
case 'VENCIDO':
return { color: 'text-amber-600', bg: 'bg-amber-100', icon: mdiClockOutline, label: 'CADUCADO' };
case 'REVOCADO':
return { color: 'text-red-600', bg: 'bg-red-100', icon: mdiCloseOctagon, label: 'REVOCADO / ANULADO' };
default:
return { color: 'text-gray-600', bg: 'bg-gray-100', icon: mdiAlertCircle, label: 'DESCONOCIDO' };
}
};
return (
<div className="min-h-screen bg-slate-50 flex flex-col">
<Head>
<title>{getPageTitle('Verificación de Certificado')}</title>
</Head>
<nav className="flex items-center justify-between p-6 lg:px-8 border-b bg-white shadow-sm">
<Link href="/" className="-m-1.5 p-1.5 flex items-center gap-2">
<div className="bg-blue-900 p-2 rounded-lg text-white">
<BaseIcon path={mdiShieldCheck} size={24} />
</div>
<span className="text-xl font-bold tracking-tight text-blue-900 uppercase">Renacer DDHH</span>
</Link>
</nav>
<main className="flex-grow flex items-center justify-center p-6 text-gray-900">
<div className="w-full max-w-2xl">
{loading ? (
<div className="text-center py-12">
<div className="animate-spin inline-block w-8 h-8 border-4 border-blue-900 border-t-transparent rounded-full mb-4"></div>
<p className="text-gray-600">Validando autenticidad en el registro oficial...</p>
</div>
) : error ? (
<div className="bg-white p-8 rounded-2xl shadow-xl border border-red-100 text-center">
<div className="text-red-500 mb-4 flex justify-center">
<BaseIcon path={mdiAlertCircle} size={64} />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Validación Fallida</h2>
<p className="text-gray-600 mb-8">{error}</p>
<Link href="/" className="bg-blue-900 text-white px-8 py-3 rounded-full font-bold hover:bg-blue-800 transition-colors">
Volver al Inicio
</Link>
</div>
) : certificate && (
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden border border-blue-100">
<div className={`p-8 ${getStatusInfo(certificate.status).bg} flex flex-col items-center text-center`}>
<div className={`${getStatusInfo(certificate.status).color} mb-4`}>
<BaseIcon path={getStatusInfo(certificate.status).icon} size={80} />
</div>
<h2 className={`text-3xl font-black ${getStatusInfo(certificate.status).color}`}>
{getStatusInfo(certificate.status).label}
</h2>
<p className="text-gray-700 mt-2 font-medium tracking-widest uppercase text-xs">Sello de Verificación Digital</p>
</div>
<div className="p-8 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Líder Certificado</h4>
<p className="text-xl font-bold text-gray-900">
{certificate.leader?.firstName} {certificate.leader?.lastName}
</p>
</div>
<div>
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Tipo de Credencial</h4>
<p className="text-xl font-bold text-gray-900">{certificate.title}</p>
</div>
<div>
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Código de Registro</h4>
<p className="text-lg font-mono font-bold text-blue-900 bg-blue-50 px-2 py-1 rounded inline-block uppercase">
{certificate.certificate_code}
</p>
</div>
<div>
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Fecha de Emisión</h4>
<p className="text-lg font-bold text-gray-900">
{new Date(certificate.issue_date).toLocaleDateString()}
</p>
</div>
</div>
<div className="border-t pt-6">
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2">Descripción de Aval</h4>
<p className="text-gray-700 leading-relaxed italic">
&quot;{certificate.description}&quot;
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg flex items-center gap-4 text-sm text-gray-500">
<BaseIcon path={mdiShieldCheck} size={24} className="text-blue-900" />
<p>Esta certificación ha sido emitida por la Corporación Renacer DDHH Mundiales y su integridad está garantizada mediante firma digital HMAC-SHA256.</p>
</div>
</div>
<div className="p-6 bg-gray-50 border-t flex justify-center">
<Link href="/" className="text-blue-900 font-bold hover:underline">
Nueva Consulta de Verificación
</Link>
</div>
</div>
)}
</div>
</main>
<footer className="bg-gray-900 text-white py-8 text-center text-sm">
<p>© 2026 Corporación Renacer DDHH Mundiales. NIT: 9018960153.</p>
</footer>
</div>
);
}
VerificationPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};