TrenzaCR

trenza-supabase-auth

0
0
# Install this skill:
npx skills add TrenzaCR/trenzaos-config --skill "trenza-supabase-auth"

Install specific skill from multi-skill repository

# Description

>

# SKILL.md


name: trenza-supabase-auth
description: >
Autenticación y gestión de sesiones con Supabase Auth.
Trigger: Al implementar login, logout, registro, recuperación de contraseña, o sesiones.
license: MIT
metadata:
author: trenza
version: "1.0"


TrenzaOS Supabase Auth Skills

Purpose

Este skill enforce las mejores prácticas de autenticación y gestión de sesiones.

Core Rules

1. Autenticación - Flujo Completo

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

// 1. Inicio de sesión
async function signIn(email: string, password: string) {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  })

  if (error) {
    return { status: 'error', error_code: 'AUTH_INVALID_CREDENTIALS' }
  }

  // 2. Obtener tenant del usuario
  const tenantId = await getTenantForUser(data.user.id)

  if (!tenantId) {
    // Usuario sin tenant asignado
    await supabase.auth.signOut()
    return { status: 'error', error_code: 'NO_TENANT_ASSIGNED' }
  }

  // 3. Crear cookie de tenant
  await setTenantCookie(tenantId)

  return { 
    status: 'success', 
    data: { 
      session: data.session,
      tenantId 
    } 
  }
}

// 2. Cierre de sesión
async function signOut() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  await deleteTenantCookie()
}

2. Middleware de Autenticación

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get: (key) => request.cookies.get(key)?.value,
        set: (key, value, options) => {
          request.cookies.set({ key, value, ...options })
        },
        remove: (key, options) => {
          request.cookies.set({ key, value: '', ...options })
        },
      },
    }
  )

  // Refrescar sesión
  const { data: { session } } = await supabase.auth.getSession()

  // Rutas protegidas
  if (!session && isProtectedRoute(request.nextUrl.pathname)) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

3. Obtención de Usuario Actual

// lib/auth/user.ts
import { createClient } from '@/lib/supabase/server'

export async function getCurrentUser() {
  const supabase = await createClient()

  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    return null
  }

  // Obtener datos del usuario desde la tabla users
  const { data: userData } = await supabase
    .from('users')
    .select('*, tenants!inner(*)')
    .eq('auth_id', user.id)
    .single()

  return userData
}

export async function getCurrentTenantId(): Promise<string> {
  const cookieStore = await cookies()
  const tenantId = cookieStore.get('tenant_id')?.value

  if (!tenantId) {
    throw new Error('TENANT_NOT_FOUND')
  }

  return tenantId
}

4. Roles y Permisos

-- Tabla de roles
CREATE TABLE user_roles (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        TEXT UNIQUE NOT NULL,  -- owner, admin, manager, member, viewer
  permissions JSONB DEFAULT '[]'     -- ['read:invoices', 'write:invoices', 'delete:invoices']
);

-- Asignar rol a usuario
ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'member';
ALTER TABLE users ADD COLUMN permissions JSONB DEFAULT '[]';
// Verificación de permisos
function hasPermission(user: User, permission: string): boolean {
  const rolePermissions = getRolePermissions(user.role)
  return rolePermissions.includes(permission)
}

// En Server Action
async function createInvoice(formData: FormData) {
  const user = await getCurrentUser()

  if (!hasPermission(user, 'write:invoices')) {
    return { status: 'error', error_code: 'FORBIDDEN' }
  }

  // Continuar...
}

5. JWT y Claims

// Configurar JWT con tenant_id
// El tenant_id debe estar en el JWT para RLS

// hooks/use-auth.ts
export async function refreshSession() {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.getSession()

  if (data.session) {
    // Asegurar que tenant_id está en el JWT
    const tenantId = await getTenantForUser(data.session.user.id)

    // Crear cookie con tenant
    await setTenantCookie(tenantId)
  }
}

6. Rate Limiting en Login

// lib/rate-limit.ts
const loginRateLimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '15m')  // 5 intentos cada 15 min
})

export async function rateLimitLogin(ip: string): Promise<boolean> {
  const { success } = await loginRateLimit.limit(ip)
  return success
}

// En Server Action de login
export async function login(prevState, formData: FormData) {
  const ip = headers().get('x-forwarded-for') || 'unknown'

  if (!(await rateLimitLogin(ip))) {
    return { 
      status: 'error', 
      error_code: 'RATE_LIMITED',
      message: 'Demasiados intentos. Intenta en 15 minutos.' 
    }
  }

  // Continuar con login...
}

7. Recuperación de Contraseña

// Solicitar reset
async function requestPasswordReset(email: string) {
  const supabase = await createClient()

  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/reset-password`
  })

  // Siempre devolver éxito para evitar enumeration
  return { 
    status: 'success',
    message: 'Si el email existe, recibirás un enlace de recuperación.' 
  }
}

// Reset de contraseña
async function resetPassword(newPassword: string) {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.updateUser({
    password: newPassword
  })

  if (error) {
    return { status: 'error', error_code: 'RESET_FAILED' }
  }

  return { status: 'success' }
}

8. Verificación de Email

// Forzar verificación de email antes de dar acceso
async function signUp(email: string, password: string) {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`
    }
  })

  if (error) {
    return { status: 'error', error_code: 'SIGNUP_FAILED' }
  }

  // Usuario creado pero necesita verificar email
  return { 
    status: 'success',
    data: { needsVerification: !data.session }
  }
}

Security Checklist

  • [ ] ¿Usas Supabase Auth (no credenciales propias)?
  • [ ] ¿Tienes rate limiting en login?
  • [ ] ¿El tenant_id está en el JWT?
  • [ ] ¿Manejas correctamente el refresh de sesión?
  • [ ] ¿Previenes enumeration de emails en login/signup?
  • [ ] ¿Las rutas sensibles tienen middleware de auth?

References

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.