Documentation Index Fetch the complete documentation index at: https://mintlify.com/joaoelian204/Portal-Ciudadano-Manta-web/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Portal Ciudadano Manta implements a robust authentication system using Supabase Auth with role-based access control for citizens and administrators. The system includes email verification, password recovery, session management, and security features like rate limiting.
User Types
The system supports two distinct user roles:
Ciudadano Regular citizens who can submit reports, participate in surveys, and view news
Administrador Administrative users who manage content, review reports, and access the admin panel
Authentication Store
The authentication logic is centralized in auth.store.ts using Pinia for state management.
State Interface
import type { User } from "@supabase/supabase-js" ;
import type { Database } from "../types/database.types" ;
type Usuario = Database [ "public" ][ "Tables" ][ "usuarios" ][ "Row" ];
interface AuthState {
user : User | null ; // Supabase auth user
usuario : Usuario | null ; // Extended user profile
loading : boolean ; // Loading state
error : string | null ; // Error messages
fetchingUser : boolean ; // Prevents concurrent fetches
}
User Profile Structure
interface Usuario {
id : string ; // UUID from Supabase Auth
email : string ;
nombres : string ;
apellidos : string ;
cedula : string ; // 10-digit Ecuadorian ID
parroquia : string ; // Parish (district)
barrio : string ; // Neighborhood
tipo : "ciudadano" | "administrador" ;
activo : boolean ;
created_at : string ;
updated_at : string ;
}
Core Authentication Functions
User Registration
Registration creates both a Supabase Auth user and a profile record in the appropriate table.
const register = async (
email : string ,
password : string ,
userData : {
nombres : string ;
apellidos : string ;
cedula : string ;
parroquia ?: string ;
barrio ?: string ;
tipo : "ciudadano" | "administrador" ;
}
) => {
loading . value = true ;
error . value = null ;
try {
// Create Supabase Auth user
const { data : authData , error : authError } = await supabase . auth . signUp ({
email ,
password ,
options: {
data: {
nombres: userData . nombres ,
apellidos: userData . apellidos ,
cedula: userData . cedula ,
parroquia: userData . parroquia ,
barrio: userData . barrio ,
tipo: userData . tipo ,
},
emailRedirectTo: ` ${ window . location . origin } /login?verified=true` ,
},
});
if ( authError ) throw authError ;
if ( ! authData . user ) throw new Error ( "Error al crear usuario" );
user . value = authData . user ;
// Create profile in appropriate table
if ( userData . tipo === "ciudadano" ) {
await supabase . from ( "usuarios" ). insert ({
id: authData . user . id ,
email ,
nombres: userData . nombres ,
apellidos: userData . apellidos ,
cedula: userData . cedula ,
parroquia: userData . parroquia ?? "" ,
barrio: userData . barrio ?? "" ,
tipo: "ciudadano" ,
activo: true ,
});
} else if ( userData . tipo === "administrador" ) {
await supabase . from ( "administradores" ). insert ({
id: authData . user . id ,
email ,
nombres: userData . nombres ,
apellidos: userData . apellidos ,
cedula: userData . cedula ,
activo: true ,
});
}
return { success: true };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
User Login
Login authenticates the user and fetches their complete profile.
const login = async ( email : string , password : string ) => {
loading . value = true ;
error . value = null ;
try {
const { data , error : loginError } = await supabase . auth . signInWithPassword ({
email ,
password ,
});
if ( loginError ) throw loginError ;
if ( ! data . user ) throw new Error ( "Error al iniciar sesión" );
user . value = data . user ;
// Fetch user profile from usuarios table
const usuarioData = await fetchUsuario ( data . user . id );
if ( ! usuarioData ) {
// Check if user is an administrator
const { data : admin } = await supabase
. from ( "administradores" )
. select ( "id, nombres" )
. eq ( "id" , data . user . id )
. single ();
if ( admin ) {
usuario . value = {
id: admin . id ,
email: data . user . email ?? "" ,
nombres: admin . nombres ,
apellidos: "" ,
cedula: "" ,
parroquia: "" ,
barrio: "" ,
tipo: "administrador" ,
activo: true ,
created_at: "" ,
updated_at: "" ,
};
}
}
return { success: true };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Session Management
The system automatically manages sessions and listens for auth state changes.
const initAuth = async () => {
loading . value = true ;
try {
// Get current session
const { data : { session } } = await supabase . auth . getSession ();
if ( session ?. user ) {
user . value = session . user ;
await fetchUsuario ( session . user . id );
}
// Listen for auth state changes
supabase . auth . onAuthStateChange ( async ( event , session ) => {
console . log ( "🔐 Auth state change:" , event );
if ( event === "SIGNED_OUT" ) {
user . value = null ;
usuario . value = null ;
} else if ( event === "TOKEN_REFRESHED" ) {
console . log ( "🔄 Token refrescado exitosamente" );
user . value = session ?. user ?? null ;
} else if ( event === "SIGNED_IN" ) {
if ( ! usuario . value || user . value ?. id !== session ?. user ?. id ) {
user . value = session ?. user ?? null ;
if ( session ?. user ) {
await fetchUsuario ( session . user . id );
}
}
}
});
} catch ( err : any ) {
error . value = err . message ;
} finally {
loading . value = false ;
}
};
Security Features
Rate Limiting
The login form implements progressive rate limiting to prevent brute force attacks:
Track Failed Attempts
The system tracks failed login attempts using localStorage and sessionStorage. const INTENTOS_MAXIMOS = 3 ;
const intentosFallidos = ref ( 0 );
const bloqueosAcumulados = ref ( 0 );
const estaBloqueado = ref ( false );
Progressive Lockout
Each failed attempt increases the lockout duration:
First lockout: 5 minutes
Second lockout: 10 minutes
Third lockout: 15 minutes, and so on
const bloquearFormulario = () => {
bloqueosAcumulados . value ++ ;
const tiempoBloqueoMinutos = bloqueosAcumulados . value * 5 ;
tiempoRestanteBloqueo . value = tiempoBloqueoMinutos * 60 ;
estaBloqueado . value = true ;
guardarDatosBloqueo ();
iniciarContadorBloqueo ();
};
Browser Fingerprinting
The system generates a unique browser identifier to track attempts across sessions. const generarIdentificadorNavegador = () => {
const canvas = document . createElement ( 'canvas' );
const ctx = canvas . getContext ( '2d' );
// ... canvas fingerprinting logic ...
const fingerprint = [
navigator . userAgent ,
navigator . language ,
new Date (). getTimezoneOffset (),
screen . width + 'x' + screen . height ,
screen . colorDepth ,
canvas . toDataURL ()
]. join ( '|' );
// Generate hash
let hash = 0 ;
for ( let i = 0 ; i < fingerprint . length ; i ++ ) {
const char = fingerprint . charCodeAt ( i );
hash = (( hash << 5 ) - hash ) + char ;
hash = hash & hash ;
}
return 'browser_' + Math . abs ( hash ). toString ( 36 );
};
Password Recovery
Users can reset their password via email:
const resetPassword = async ( email : string ) => {
loading . value = true ;
error . value = null ;
try {
const { error : resetError } = await supabase . auth . resetPasswordForEmail (
email ,
{
redirectTo: ` ${ window . location . origin } /reset-password` ,
}
);
if ( resetError ) throw resetError ;
return { success: true };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Email Recovery
Users who forget their email can retrieve it using their Ecuadorian ID (cédula):
const searchUserByCedula = async ( cedula : string ) => {
// Validate cédula format (10 digits)
if ( cedula . length !== 10 || ! validateCedula ( cedula )) {
throw new Error ( "Cédula inválida" );
}
// Use RPC function for security
const { data , error } = await supabase . rpc ( "buscar_email_por_cedula" , {
cedula_input: cedula ,
});
if ( error ) throw error ;
const usuario = data && data . length > 0 ? data [ 0 ] : null ;
return usuario ;
};
Cedula Validation
The system validates Ecuadorian national IDs using the official algorithm:
const validarCedulaEcuatoriana = ( cedula : string ) : boolean => {
if ( cedula . length !== 10 ) return false ;
const digitoRegion = Number ( cedula . substring ( 0 , 2 ));
if ( digitoRegion < 1 || digitoRegion > 24 ) return false ;
const ultimoDigito = Number ( cedula . substring ( 9 , 10 ));
// Calculate checksum for even positions
const pares =
Number ( cedula . substring ( 1 , 2 )) +
Number ( cedula . substring ( 3 , 4 )) +
Number ( cedula . substring ( 5 , 6 )) +
Number ( cedula . substring ( 7 , 8 ));
// Calculate checksum for odd positions (with special multiplication)
let impares = 0 ;
for ( let i = 0 ; i < 9 ; i += 2 ) {
let num = Number ( cedula . substring ( i , i + 1 )) * 2 ;
if ( num > 9 ) num -= 9 ;
impares += num ;
}
const sumaTotal = pares + impares ;
const primerDigitoSuma = String ( sumaTotal ). substring ( 0 , 1 );
const decena = ( Number ( primerDigitoSuma ) + 1 ) * 10 ;
let digitoValidador = decena - sumaTotal ;
if ( digitoValidador === 10 ) digitoValidador = 0 ;
return digitoValidador === ultimoDigito ;
};
Role-Based Access Control
The system provides helper functions to check user roles:
// Getters
const isAuthenticated = () => !! user . value ;
const isCiudadano = () => usuario . value ?. tipo === "ciudadano" ;
const isAdministrador = () => usuario . value ?. tipo === "administrador" ;
Usage in Components
< script setup lang = "ts" >
import { useAuthStore } from "@/stores/auth.store" ;
const authStore = useAuthStore ();
</ script >
< template >
< div v-if = " authStore . isAuthenticated () " >
< div v-if = " authStore . isAdministrador () " >
<!-- Admin-only content -->
</ div >
< div v-else-if = " authStore . isCiudadano () " >
<!-- Citizen-only content -->
</ div >
</ div >
</ template >
Best Practices
Supabase automatically handles session persistence. The rememberMe parameter is maintained for compatibility but sessions persist by default through localStorage.
Always handle authentication errors gracefully and provide user-friendly messages. The system includes translations for common error scenarios.
Supabase automatically refreshes tokens. The auth state listener handles TOKEN_REFRESHED events to update the UI without refetching user data.
Concurrent Fetch Prevention
The fetchingUser flag prevents multiple concurrent user profile fetches, improving performance and preventing race conditions.
Supabase Auth Docs Official Supabase authentication documentation
Vue Router Guards Protecting routes with navigation guards