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)