Vamos a implementar un sistema de autenticación desde terminal usando Clean Architecture (Link al repositorio del proyecto).
Aquí te presento la estructura del proyecto:
Estructura del Proyecto
src/
├── core/                    # Capa de dominio
│   ├── entities/           # Entidades de negocio
│   ├── repositories/       # Interfaces de repositorios
│   ├── usecases/           # Casos de uso
│   └── interfaces/         # Interfaces adicionales
├── infrastructure/         # Capa de infraestructura
│   ├── repositories/      # Implementaciones concretas de repositorios
│   ├── cli/               # Interfaz de línea de comandos
│   └── security/          # Utilidades de seguridad
├── application/           # Capa de aplicación (coordinadores)
└── main.ts                # Punto de entrada
Implementación paso a paso
1. Configuración inicial
Primero, instala las dependencias necesarias:
npm init -y
npm install typescript ts-node @types/node bcryptjs inquirer chalk figlet uuid @types/uuid @types/bcryptjs @types/inquirer @types/figlet --save-dev
npx tsc --init
2. Entidad de Usuario (Capa de Dominio)
src/core/entities/user.ts:
export interface User {
  id: string;
  username: string;
  passwordHash: string; // Nunca almacenar contraseñas en texto plano
  createdAt: Date;
}
3. Interfaz del Repositorio (Capa de Dominio)
src/core/repositories/user.repository.ts:
import { User } from "../entities/user";
export interface UserRepository {
  createUser(user: User): Promise<User>;
  findByUsername(username: string): Promise<User | null>;
  findAllUsers(): Promise<User[]>;
  verifyPassword(password: string, hash: string): Promise<boolean>;
}
4. Casos de Uso (Capa de Dominio)
src/core/usecases/auth.usecase.ts:
import { UserRepository } from "../repositories/user.repository";
import { User } from "../entities/user";
export class AuthUseCase {
  constructor(private readonly userRepository: UserRepository) {}
  async register(username: string, password: string): Promise<User> {
    if (!username || !password) {
      throw new Error("Username and password are required");
    }
    const existingUser = await this.userRepository.findByUsername(username);
    if (existingUser) {
      throw new Error("Username already exists");
    }
    const user: User = {
      id: this.generateUserId(),
      username,
      passwordHash: password,
      createdAt: new Date(),
    };
    return this.userRepository.createUser(user);
  }
  async login(username: string, password: string): Promise<User> {
    const user = await this.userRepository.findByUsername(username);
    if (!user) {
      throw new Error("User not found");
    }
    const isValid = await this.userRepository.verifyPassword(password, user.passwordHash);
    if (!isValid) {
      throw new Error("Invalid password");
    }
    return user;
  }
  private generateUserId(): string {
    return "temp-id";
  }
}
5. Implementación del Repositorio (Infraestructura)
src/infrastructure/repositories/user.file.repository.ts:
import { UserRepository } from "../../core/repositories/user.repository";
import { User } from "../../core/entities/user";
import * as fs from "fs";
import * as path from "path";
import * as bcrypt from "bcryptjs";
import { v4 as uuidv4 } from "uuid";
const FILE_PATH = path.join(__dirname, "../../../data/users.json");
export class UserFileRepository implements UserRepository {
  private users: User[] = [];
  constructor() {
    this.ensureFileExists();
    this.loadUsers();
  }
  private ensureFileExists(): void {
    if (!fs.existsSync(FILE_PATH)) {
      fs.writeFileSync(FILE_PATH, "[]", "utf-8");
    }
  }
  private loadUsers(): void {
    const data = fs.readFileSync(FILE_PATH, "utf-8");
    this.users = JSON.parse(data || "[]");
  }
  private saveUsers(): void {
    fs.writeFileSync(FILE_PATH, JSON.stringify(this.users, null, 2), "utf-8");
  }
  async createUser(user: User): Promise<User> {
    const salt = await bcrypt.genSalt(10);
    const passwordHash = await bcrypt.hash(user.passwordHash, salt);
    const newUser: User = {
      ...user,
      id: uuidv4(),
      passwordHash,
      createdAt: new Date(),
    };
    this.users.push(newUser);
    this.saveUsers();
    return newUser;
  }
  async findByUsername(username: string): Promise<User | null> {
    return this.users.find((u) => u.username === username) || null;
  }
  async findAllUsers(): Promise<User[]> {
    return [...this.users];
  }
  async verifyPassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}
6. CLI Interface (Infraestructura)
src/infrastructure/cli/auth.cli.ts:
import inquirer from "inquirer";
import chalk from "chalk";
import figlet from "figlet";
import { AuthUseCase } from "../../core/usecases/auth.usecase";
import { User } from "../../core/entities/user";
export class AuthCLI {
  private currentUser: User | null = null;
  constructor(private authUseCase: AuthUseCase) {}
  async start() {
    console.log(chalk.green(figlet.textSync("Auth System", { horizontalLayout: "full" })));
    while (true) {
      if (!this.currentUser) {
        await this.showMainMenu();
      } else {
        await this.showUserMenu();
      }
    }
  }
  private async showMainMenu() {
    const { action } = await inquirer.prompt([
      {
        type: "list",
        name: "action",
        message: "What do you want to do?",
        choices: ["Login", "Register", "Exit"],
      },
    ]);
    switch (action) {
      case "Login":
        await this.handleLogin();
        break;
      case "Register":
        await this.handleRegister();
        break;
      case "Exit":
        process.exit(0);
    }
  }
  private async showUserMenu() {
    const { action } = await inquirer.prompt([
      {
        type: "list",
        name: "action",
        message: `Welcome ${this.currentUser!.username}, what do you want to do?`,
        choices: ["Logout", "Exit"],
      },
    ]);
    switch (action) {
      case "Logout":
        this.currentUser = null;
        console.log(chalk.blue("You have been logged out"));
        break;
      case "Exit":
        process.exit(0);
    }
  }
  private async handleLogin() {
    const { username, password } = await inquirer.prompt([
      {
        type: "input",
        name: "username",
        message: "Enter your username:",
      },
      {
        type: "password",
        name: "password",
        message: "Enter your password:",
        mask: "*",
      },
    ]);
    try {
      this.currentUser = await this.authUseCase.login(username, password);
      console.log(chalk.green("Login successful!"));
    } catch (error) {
      console.log(chalk.red(`Error: ${(error as Error).message}`));
    }
  }
  private async handleRegister() {
    const { username, password } = await inquirer.prompt([
      {
        type: "input",
        name: "username",
        message: "Choose a username:",
      },
      {
        type: "password",
        name: "password",
        message: "Choose a password:",
        mask: "*",
      },
    ]);
    try {
      await this.authUseCase.register(username, password);
      console.log(chalk.green("Registration successful! You can now login."));
    } catch (error) {
      console.log(chalk.red(`Error: ${(error as Error).message}`));
    }
  }
}
7. Punto de Entrada
src/main.ts:
import { UserFileRepository } from "./infrastructure/repositories/user.file.repository";
import { AuthUseCase } from "./core/usecases/auth.usecase";
import { AuthCLI } from "./infrastructure/cli/auth.cli";
// Configuración e inyección de dependencias
const userRepository = new UserFileRepository();
const authUseCase = new AuthUseCase(userRepository);
const authCLI = new AuthCLI(authUseCase);
// Iniciar la aplicación
authCLI.start().catch((error) => {
  console.error("Application error:", error);
  process.exit(1);
});
Características implementadas:
- 
Clean Architecture:
- Capa de dominio (core) con entidades, repositorios y casos de uso
 - Capa de infraestructura con implementaciones concretas
 - Desacoplamiento mediante interfaces
 
 - 
Seguridad:
- Encriptación de contraseñas con bcrypt
 - Nunca se almacenan contraseñas en texto plano
 
 - 
Persistencia:
- Almacenamiento en archivo JSON (data/users.json)
 - Fácil migración a base de datos cambiando el repositorio
 
 - 
Interfaz de usuario:
- CLI interactiva con inquirer
 - Menús para registro, login y logout
 
 
Cómo ejecutar el proyecto:
- Instala las dependencias como se indicó al principio
 - Crea la estructura de directorios y archivos
 - Ejecuta con: 
npx ts-node src/main.ts 
Posibles mejoras:
- Migrar a base de datos (solo implementar nuevo repositorio)
 - Añadir más validaciones (fortaleza de contraseña, etc.)
 - Implementar JWT para sesiones más robustas
 - Añadir tests unitarios e integración
 
Este proyecto sigue los principios de Clean Architecture manteniendo las capas bien separadas y las dependencias apuntando hacia el centro (dominio).
[🔗 Conéctame para más contenido técnico] [👨💻 Visita mi GitHub]
📢 ¡Feedback y contribuciones son bienvenidos! ¿Qué funcionalidades añadirías? ¿Has implementado algo similar? Hablemos en los comentarios 👇
    
Top comments (0)