Skip to main content

Overview

APTIV Scrap Control uses JWT (JSON Web Tokens) for stateless authentication with bcrypt password hashing for security.

Authentication Flow

1

User Login

User submits gafete (badge ID) and password to /api/auth/login.
2

Credential Verification

Server validates credentials against the usuarios table using bcrypt comparison.
3

Token Generation

On success, server generates a JWT token with 12-hour expiration.
4

Client Storage

Frontend stores token in localStorage and includes it in all API requests.
5

Token Validation

Server validates token on each request using JWT middleware.

User Data Structure

types.ts
export interface Usuario {
  id: number;
  nombre: string;
  apellido: string;
  gafete: string;        // Badge ID (unique identifier)
  turno: string;         // Assigned shift
  area: string;          // Assigned area
  tipo: string;          // Role: Admin, Calidad, Supervisor, Operador
  contra: string;        // Bcrypt hashed password
  activo: number;        // 1 = active, 0 = inactive
  permisos?: string;     // JSON array of permissions
  created_at?: string;   // Account creation timestamp
}

export interface Session {
  user: Usuario;
  token: string;
  loginTime: string;
}

Login Process

Frontend Login Component

LoginPage.tsx
const handleLogin = async (e: FormEvent) => {
  e.preventDefault();
  
  const usuario = usuarios.find(
    (u) => u.gafete === gafete && u.activo === 1
  );
  
  if (!usuario) {
    setError('Usuario no encontrado o inactivo');
    return;
  }
  
  // In API mode, verify with backend
  if (dataMode === 'api') {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ gafete, password: contra })
    });
    
    if (!response.ok) {
      setError('Credenciales inválidas');
      return;
    }
    
    const { token, user } = await response.json();
    store.setSession(user, token);
  } else {
    // Local mode: simple password comparison
    if (usuario.contra !== contra) {
      setError('Contraseña incorrecta');
      return;
    }
    store.setCurrentUser(usuario);
  }
};

Backend Authentication Route

auth.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

router.post('/login', async (req, res) => {
  const { gafete, password } = req.body;
  
  // Find user by badge ID
  const [users] = await db.query(
    'SELECT * FROM usuarios WHERE gafete = ? AND activo = 1',
    [gafete]
  );
  
  if (users.length === 0) {
    return res.status(401).json({ error: 'Usuario no encontrado' });
  }
  
  const user = users[0];
  
  // Verify password with bcrypt
  const isValid = await bcrypt.compare(password, user.contra);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Contraseña incorrecta' });
  }
  
  // Generate JWT token (12 hour expiration)
  const token = jwt.sign(
    { id: user.id, gafete: user.gafete, tipo: user.tipo },
    process.env.JWT_SECRET || 'aptiv-scrap-secret-2024',
    { expiresIn: '12h' }
  );
  
  // Store session in database
  await db.query(
    'INSERT INTO sesiones (usuario_id, token, ip, expires_at) VALUES (?, ?, ?, ?)',
    [user.id, token, req.ip, new Date(Date.now() + 12 * 60 * 60 * 1000)]
  );
  
  // Return token and user data (without password)
  delete user.contra;
  res.json({ token, user });
});

JWT Token Structure

Token Payload

{
  "id": 1,
  "gafete": "12345",
  "tipo": "Admin",
  "iat": 1704067200,
  "exp": 1704110400
}

Token Usage

Include the token in the Authorization header for all API requests:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  https://api.example.com/api/scrap
const response = await fetch('/api/scrap', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

JWT Middleware

Server-Side Token Validation

middleware/auth.js
const jwt = require('jsonwebtoken');

const authMiddleware = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(
      token,
      process.env.JWT_SECRET || 'aptiv-scrap-secret-2024'
    );
    
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

module.exports = authMiddleware;

Protecting Routes

server.js
const authMiddleware = require('./middleware/auth');

// Public route
app.post('/api/auth/login', authRoutes);

// Protected routes
app.use('/api/scrap', authMiddleware, scrapRoutes);
app.use('/api/users', authMiddleware, userRoutes);
app.use('/api/reports', authMiddleware, reportsRoutes);

Password Management

Password Hashing

Passwords are hashed using bcrypt with 10 salt rounds:
const bcrypt = require('bcryptjs');

const hashedPassword = await bcrypt.hash(plainPassword, 10);

Creating Users with Hashed Passwords

users.js
router.post('/', authMiddleware, async (req, res) => {
  const { nombre, apellido, gafete, turno, area, tipo, contra } = req.body;
  
  // Hash password before storing
  const hashedPassword = await bcrypt.hash(contra, 10);
  
  await db.query(
    'INSERT INTO usuarios (nombre, apellido, gafete, turno, area, tipo, contra, activo) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
    [nombre, apellido, gafete, turno, area, tipo, hashedPassword]
  );
  
  res.json({ success: true });
});

Password Reset

router.put('/:id/password', authMiddleware, async (req, res) => {
  const { id } = req.params;
  const { newPassword } = req.body;
  
  // Hash new password
  const hashedPassword = await bcrypt.hash(newPassword, 10);
  
  await db.query(
    'UPDATE usuarios SET contra = ? WHERE id = ?',
    [hashedPassword, id]
  );
  
  res.json({ success: true });
});

Session Management

Database Schema

schema.sql
CREATE TABLE IF NOT EXISTS sesiones (
  id int AUTO_INCREMENT PRIMARY KEY,
  usuario_id int NOT NULL,
  token varchar(500) NOT NULL,
  ip varchar(50) DEFAULT NULL,
  expires_at datetime NOT NULL,
  created_at datetime DEFAULT CURRENT_TIMESTAMP
);

Session Cleanup

Expired sessions are automatically cleaned up:
// Run daily at midnight
const cleanupExpiredSessions = async () => {
  await db.query('DELETE FROM sesiones WHERE expires_at < NOW()');
};

setInterval(cleanupExpiredSessions, 24 * 60 * 60 * 1000);

Default Credentials

Change these credentials immediately after installation!
UsernamePasswordRole
adminadmin123Administrator
calidadcalidad123Quality
supervisor1super123Supervisor
operador1oper123Operator

Security Best Practices

Use HTTPS

Always deploy with HTTPS to prevent token interception

Rotate JWT Secret

Change JWT_SECRET environment variable regularly

Short Token Expiration

Default 12-hour expiration balances security and usability

Monitor Failed Logins

Implement rate limiting after multiple failed attempts

Troubleshooting

JWT tokens expire after 12 hours. Users must log in again to get a fresh token.Solution: Implement automatic token refresh or warn users before expiration.
Token signature doesn’t match or token was tampered with.Solution: Ensure JWT_SECRET is consistent across server restarts. Clear localStorage and log in again.
Bcrypt comparison failing even with correct password.Solution: Verify password was hashed before storing. Re-hash password using seed script.
Browser blocking requests due to CORS policy.Solution: Configure CORS middleware in Express to allow your frontend origin:
app.use(cors({ origin: 'http://localhost:8080' }));

Roles & Permissions

Configure user permissions

Audit Logs

Track authentication events

API: Login

Login endpoint documentation

API: Session

Session validation endpoint

Build docs developers (and LLMs) love