Skip to main content

Overview

Portal Ciudadano Manta uses Supabase Auth for secure authentication with email/password, session management, and role-based access control.

Setup

Configure Supabase client and environment variables

User Flow

Registration, login, and session management

Security

PKCE flow, session persistence, and best practices

Role-Based Access

Citizen and administrator roles

Setup

Environment Variables

Configure Supabase credentials in your .env file:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
Never commit your .env file to version control. The anon key is safe for client-side use but should still be kept in environment variables.

Client Configuration

The Supabase client is pre-configured in src/lib/supabase.ts:
import { createClient } from "@supabase/supabase-js"
import type { Database } from "../types/database.types"
import { conditionalStorage } from "./storage"

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient<Database>(
  supabaseUrl || "https://placeholder.supabase.co",
  supabaseAnonKey || "placeholder-key",
  {
    auth: {
      persistSession: true,
      autoRefreshToken: true,
      detectSessionInUrl: true,
      storage: conditionalStorage, // Falls back to memory if localStorage unavailable
      flowType: "pkce", // PKCE flow for enhanced security
    },
    db: {
      schema: "public",
    },
    global: {
      headers: {
        "X-Client-Info": "[email protected]",
      },
    },
    realtime: {
      timeout: 10000,
    },
  }
)
Key Configuration Options:
  • persistSession: true - Sessions persist across page refreshes
  • autoRefreshToken: true - Automatically refreshes expired tokens
  • flowType: "pkce" - Uses PKCE flow for enhanced security in SPAs
  • storage: Custom conditional storage that falls back to memory

Authentication Flow

Initialize Authentication

Call this on app startup (typically in main.ts or root component):
import { useAuthStore } from '@/stores/auth.store'

const authStore = useAuthStore()

// Initialize and setup auth state listeners
await authStore.initAuth()
This will:
  1. Check for existing session in storage
  2. Load user profile if authenticated
  3. Setup auth state change listeners
  4. Handle token refresh automatically

User Registration

Citizen Registration

const authStore = useAuthStore()

const result = await authStore.register(
  '[email protected]',
  'SecurePassword123!',
  {
    nombres: 'Juan',
    apellidos: 'Pérez',
    cedula: '1234567890',
    parroquia: 'Manta',
    barrio: 'Centro',
    tipo: 'ciudadano'
  }
)

if (result.success) {
  console.log('Registration successful!')
  // User will receive verification email
} else {
  console.error('Registration failed:', result.error)
}
After registration, users receive a verification email. The email contains a link that redirects to /login?verified=true.

Administrator Registration

const result = await authStore.register(
  '[email protected]',
  'AdminPassword123!',
  {
    nombres: 'María',
    apellidos: 'González',
    cedula: '0987654321',
    tipo: 'administrador'
  }
)
Registration Flow:
  1. Create account in Supabase Auth
  2. Store user metadata (nombres, apellidos, etc.)
  3. Insert record in usuarios table (for ciudadanos)
  4. Insert record in administradores table (for administrators)
  5. Send verification email

User Login

const authStore = useAuthStore()

const result = await authStore.login(
  '[email protected]',
  'password123'
)

if (result.success) {
  console.log('Login successful!')
  console.log('User type:', authStore.usuario?.tipo)
  
  // Navigate based on user type
  if (authStore.isAdministrador()) {
    router.push('/admin')
  } else {
    router.push('/dashboard')
  }
} else {
  console.error('Login failed:', result.error)
}
Login Process:
  1. Authenticate with Supabase Auth
  2. Retrieve session and user object
  3. Fetch extended profile from usuarios or administradores table
  4. Store session in localStorage (or memory fallback)
  5. Setup realtime auth state listeners

Password Reset

const authStore = useAuthStore()

const result = await authStore.resetPassword('[email protected]')

if (result.success) {
  console.log('Reset email sent!')
} else {
  console.error('Reset failed:', result.error)
}
Users receive an email with a reset link that redirects to /reset-password with a token.

Logout

const authStore = useAuthStore()

const result = await authStore.logout()

if (result.success) {
  console.log('Logged out successfully')
  router.push('/login')
}
Logout Process:
  1. Clear local state immediately
  2. Call Supabase signOut() with 5-second timeout
  3. Clear session from storage
  4. Navigate to login page
Logout has a 5-second timeout to prevent hanging. Local state is cleared immediately regardless of timeout.

Security

PKCE Flow

The client uses PKCE (Proof Key for Code Exchange) flow for enhanced security:
{
  auth: {
    flowType: "pkce"
  }
}
PKCE protects against authorization code interception attacks and is recommended for SPAs.

Session Management

Sessions are automatically managed:
  • Persistence: Stored in localStorage (or memory fallback)
  • Auto-refresh: Tokens refresh automatically before expiration
  • Timeout: Realtime operations have 10-second timeout
  • Validation: Session checked on app init and route changes
// Check if user is authenticated
const isAuthed = await isAuthenticated()

// Get current user
const user = await getCurrentUser()

Auth State Listeners

The auth store automatically listens for auth state changes:
supabase.auth.onAuthStateChange(async (event, session) => {
  switch (event) {
    case 'SIGNED_IN':
      // User signed in
      user.value = session?.user ?? null
      await fetchUsuario(session.user.id)
      break
      
    case 'SIGNED_OUT':
      // User signed out
      user.value = null
      usuario.value = null
      break
      
    case 'TOKEN_REFRESHED':
      // Token refreshed automatically
      user.value = session?.user ?? null
      break
      
    case 'USER_UPDATED':
      // User data updated
      user.value = session?.user ?? null
      await fetchUsuario(session.user.id)
      break
  }
})

Error Handling

Use the error handler helper:
import { handleSupabaseError } from '@/lib/supabase'

const { data, error } = await supabase
  .from('reportes')
  .select('*')

if (error) {
  const userMessage = handleSupabaseError(error)
  // Display userMessage to user
}

Role-Based Access

User Types

The platform supports two user types:
  1. Ciudadano (Citizen)
    • Submit reports
    • View news and surveys
    • Update profile
    • Stored in usuarios table
  2. Administrador (Administrator)
    • All citizen permissions
    • Manage reports (review, update status)
    • Create/edit/delete news
    • Create/edit/delete surveys
    • View statistics and analytics
    • Stored in administradores table

Checking User Role

const authStore = useAuthStore()

if (authStore.isAuthenticated()) {
  if (authStore.isCiudadano()) {
    console.log('User is a citizen')
  }
  
  if (authStore.isAdministrador()) {
    console.log('User is an administrator')
  }
  
  // Access user data
  console.log(authStore.usuario.nombres)
  console.log(authStore.usuario.tipo)
}

Route Protection

Protect routes in your router:
import { useAuthStore } from '@/stores/auth.store'

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  
  // Protected routes
  if (to.meta.requiresAuth && !authStore.isAuthenticated()) {
    return next('/login')
  }
  
  // Admin-only routes
  if (to.meta.requiresAdmin && !authStore.isAdministrador()) {
    return next('/dashboard')
  }
  
  next()
})

Database Schema

usuarios Table

CREATE TABLE usuarios (
  id UUID PRIMARY KEY REFERENCES auth.users(id),
  email TEXT NOT NULL,
  nombres TEXT NOT NULL,
  apellidos TEXT NOT NULL,
  cedula TEXT NOT NULL UNIQUE,
  parroquia TEXT,
  barrio TEXT,
  tipo TEXT NOT NULL CHECK (tipo IN ('ciudadano', 'administrador')),
  activo BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

administradores Table

CREATE TABLE administradores (
  id UUID PRIMARY KEY REFERENCES auth.users(id),
  email TEXT NOT NULL,
  nombres TEXT NOT NULL,
  apellidos TEXT NOT NULL,
  cedula TEXT NOT NULL UNIQUE,
  activo BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Advanced Usage

Profile Updates

const authStore = useAuthStore()

const result = await authStore.updateProfile({
  nombres: 'Juan Carlos',
  parroquia: 'Nueva Parroquia',
  barrio: 'Nuevo Barrio'
})

if (result.success) {
  console.log('Profile updated!')
  console.log(authStore.usuario) // Updated data
}

Fetch User Profile

Manually fetch user profile (usually done automatically):
const authStore = useAuthStore()

if (authStore.user) {
  const profile = await authStore.fetchUsuario(authStore.user.id)
  console.log(profile)
}
The function automatically:
  1. Checks usuarios table first
  2. Falls back to administradores table if not found
  3. Handles concurrent fetch requests with locking
  4. Has 10-second timeout for reliability

Session Validation

import { supabase } from '@/lib/supabase'

// Get current session
const { data: { session }, error } = await supabase.auth.getSession()

if (session) {
  console.log('Session valid until:', session.expires_at)
  console.log('User:', session.user)
} else {
  console.log('No active session')
}

// Get user (validates token)
const { data: { user }, error: userError } = await supabase.auth.getUser()

Best Practices

// In main.ts or App.vue
const authStore = useAuthStore()
await authStore.initAuth()
This ensures session restoration and proper auth state listeners.
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth.store'

const authStore = useAuthStore()
const isAdmin = computed(() => authStore.isAdministrador())

// In template
<div v-if="isAdmin">Admin only content</div>
const authStore = useAuthStore()

const handleLogin = async () => {
  const result = await authStore.login(email, password)
  
  if (authStore.loading) {
    // Show spinner
  }
  
  if (authStore.error) {
    // Display error
    console.error(authStore.error)
  }
}
Always protect authenticated and admin routes in your router configuration.
The logout function automatically clears all auth state. Ensure you also clear any cached user data in other stores.
For sensitive operations, validate the session is still valid:
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
  // Redirect to login
  await authStore.logout()
  router.push('/login')
}

Troubleshooting

Common Issues

Session not persisting:
  • Verify localStorage is available
  • Check browser privacy settings
  • Storage falls back to memory if localStorage is blocked
Token refresh failing:
  • Check Supabase project settings
  • Verify autoRefreshToken: true in config
  • Check network connectivity
User profile not loading:
  • Verify user exists in usuarios or administradores table
  • Check database permissions
  • Review console logs for error details
Login timeout:
  • Check Supabase service status
  • Verify network connection
  • Review timeout settings in config

API Reference

View complete API documentation for all composables and stores

Build docs developers (and LLMs) love