Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

jobs:
test:
name: PHPUnit (Laravel)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
extensions: mbstring, pdo, pdo_sqlite, sqlite3, bcmath
coverage: none

- name: Copy .env
run: cp .env.example .env

- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress

- name: Generate app key
run: php artisan key:generate

- name: Run tests
run: php artisan test
217 changes: 86 additions & 131 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,164 +1,119 @@
TaskManager – API & Plataforma de Gestión de Tareas
# ✅ TaskManager — API de Gestión de Tareas

Proyecto personal pensado para mostrar buenas prácticas en desarrollo Full‑Stack con Laravel + PostgreSQL + Docker.
API REST en **Laravel 12** para gestionar tareas con **autenticación JWT**.
Diseñada en capas (**controllers → services → repositories → models**) para
mantener el dominio desacoplado de la infraestructura. Lista para Docker y con
suite de tests en GitHub Actions.

✨ Características principales
![CI](https://github.com/AlejandroVegaFullstackDev/taskManager/actions/workflows/ci.yml/badge.svg)
![PHP](https://img.shields.io/badge/php-8.2-777BB4)
![Laravel](https://img.shields.io/badge/laravel-12-FF2D20)
![PostgreSQL](https://img.shields.io/badge/postgres-15-336791)
![Tests](https://img.shields.io/badge/tests-PHPUnit-3776AB)

API RESTful con operaciones CRUD para tareas.
---

Autenticación JWT (Laravel Sanctum) con expiración configurable.
## ✨ Características

Arquitectura hexagonal: controladores → servicios → repositorios → modelos.
- **CRUD** de tareas vía API REST.
- **Autenticación JWT** con `tymon/jwt-auth` (register, login, refresh, logout).
- **Arquitectura en capas**: el controlador delega en un **servicio**, que usa un
**repositorio** (interfaz) — inversión de dependencias vía contenedor de Laravel.
- **Validación** con Form Requests.
- **Tests** de feature (API + auth) y unitarios (servicio) con PHPUnit.

Docker‑first: entorno replicable en cualquier máquina.
> El portafolio mencionaba "Sanctum": la autenticación real usa
> **`tymon/jwt-auth`** (tokens JWT con TTL configurable).

CI/CD listo para GitHub Actions (tests + Lint + Build).
---

Cobertura de tests: unitarios y de integración (PHPUnit).

🚀 Demo local en 5 pasos

# 1. Clona el repo
$ git clone https://github.com/tu‑usuario/taskmanager.git && cd taskmanager

# 2. Copia variables de entorno
$ cp .env.example .env # ajusta valores si lo deseas

# 3. Levanta servicios
$ docker compose up -d --build

# 4. Instala dependencias & ejecuta migraciones
$ docker compose exec app composer install
$ docker compose exec app php artisan migrate --seed

# 5. Visita la app (frontend opcional)
http://localhost:3000 # si usas el front React opcional

Credenciales iniciales (Seeds)email: admin@example.compassword: passwordPuedes cambiarlas en database/seeders/UserSeeder.php antes de levantar el stack.

🗄️ Stack Tecnológico

Capa

Tecnología

Versión

Backend

PHP / Laravel

8.3 / 10.x

Base de datos

PostgreSQL

15

Autenticación

JWT (Sanctum)

—

Contenedores

Docker & Compose

26+

CI/CD

GitHub Actions

—

Testing

PHPUnit + Pest

—

📂 Estructura de carpetas (backend)
## 🧱 Arquitectura

```
app/
├─ Http/Controllers // Entradas HTTP
├─ Domain/Models // Entidades de dominio (Eloquent)
├─ Domain/Repositories // Interfaces
├─ Infrastructure/Repos // Implementaciones Eloquent
└─ Services // Casos de uso

🔐 Autenticación

Login – POST /api/login

{ "email": "admin@example.com", "password": "password" }

Respuesta → access_token, token_type, expires_in.

Incluye la cabecera:

Authorization: Bearer <token>

📑 Endpoints de Tareas

Método

Endpoint

Descripción

GET

/api/tasks

Listar tareas
├── Http/
│ ├── Controllers/ TaskController, AuthController (capa de entrada, delgada)
│ └── Requests/ StoreTaskRequest, UpdateTaskRequest (validación)
├── Services/ TaskService (lógica de aplicación)
├── Repositories/ TaskRepositoryInterface + TaskRepository (persistencia)
├── Exceptions/ TaskNotFoundException (render 404 JSON)
└── Models/ Task, User
```

POST
El binding `TaskRepositoryInterface → TaskRepository` se registra en
`AppServiceProvider`, así el servicio depende de la **interfaz**, no de Eloquent.

/api/tasks
---

Crear tarea
## 🔗 Endpoints

GET
| Método | Ruta | Auth | Descripción |
|--------|------|------|-------------|
| `POST` | `/api/register` | público | Crea usuario y devuelve token |
| `POST` | `/api/login` | público | Devuelve token JWT |
| `POST` | `/api/refresh` | JWT | Renueva el token |
| `POST` | `/api/logout` | JWT | Invalida el token |
| `GET` | `/api/tasks` | JWT | Lista tareas |
| `POST` | `/api/tasks` | JWT | Crea tarea |
| `GET` | `/api/tasks/{id}` | JWT | Detalle de tarea |
| `PUT` | `/api/tasks/{id}` | JWT | Actualiza tarea |
| `DELETE` | `/api/tasks/{id}` | JWT | Elimina tarea |

/api/tasks/{id}
Las rutas protegidas requieren `Authorization: Bearer <token>`. El campo
`status` de una tarea acepta `pendiente` o `completada`.

Obtener tarea
```jsonc
// POST /api/tasks
{ "title": "Comprar pan", "description": "En la tienda", "status": "pendiente" }
```

PUT
---

/api/tasks/{id}
## 🚀 Cómo correrlo

Actualizar tarea
### Local (SQLite, rápido)

DELETE
```bash
composer install
cp .env.example .env
php artisan key:generate
php artisan jwt:secret # genera JWT_SECRET
touch database/database.sqlite # .env trae DB_CONNECTION=sqlite
php artisan migrate
php artisan serve # http://localhost:8000
```

/api/tasks/{id}
### Docker (PostgreSQL)

Eliminar tarea
```bash
cp .env.example .env # ajusta DB_* y pon DB_CONNECTION=pgsql
docker compose up -d --build
docker compose exec app php artisan migrate --seed
```

Consulta docs/openapi.yaml para una especificación completa (OpenAPI 3.1).
El seeder crea un usuario de prueba `test@example.com` / `password`.

🧪 Pruebas
---

# Ejecutar todas las pruebas
$ docker compose exec app php artisan test
## 🧪 Tests

# Cobertura (HTML)
$ docker compose exec app phpdbg -qrr vendor/bin/phpunit --coverage-html storage/coverage
```bash
php artisan test # corre la suite (SQLite en memoria, vía phpunit.xml)
```

☁️ Despliegue en producción
Cubren registro/login/refresh, protección por JWT y el CRUD completo de tareas,
además de tests unitarios del `TaskService` con repositorio simulado (Mockery).
La suite corre en **GitHub Actions** (`.github/workflows/ci.yml`).

Se puede desplegar en cualquier PaaS que soporte Docker (AWS ECS/Fargate, Railway, Fly.io, etc.).
Ejemplo de workflow GitHub Actions a Railway incluido en .github/workflows/deploy.yml.
---

📄 Licencia
## 🛠️ Stack

Publicado bajo la licencia MIT. Siéntete libre de usarlo como base para tus propios proyectos.
`PHP 8.2` · `Laravel 12` · `tymon/jwt-auth` · `PostgreSQL 15` / `SQLite` ·
`PHPUnit 11` · `Docker`

🤝 Créditos y contexto
---

Este repositorio nació como una prueba técnica; posteriormente fue refactorizado y ampliado para servir como ejemplo público de buenas prácticas. Todo el código mostrado aquí es 100 % original y no contiene información ni activos privados de terceros.
## 📄 Licencia

MIT
19 changes: 19 additions & 0 deletions app/Exceptions/TaskNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class TaskNotFoundException extends Exception
{
/**
* Renderiza la excepción como una respuesta JSON 404.
* Laravel la captura automáticamente, manteniendo el controlador delgado.
*/
public function render(Request $request): JsonResponse
{
return response()->json(['error' => 'Tarea no encontrada'], 404);
}
}
66 changes: 49 additions & 17 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,72 @@

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
public function login(Request $request)
/**
* Registra un usuario nuevo y devuelve un token JWT.
*/
public function register(Request $request): JsonResponse
{
$credentials = $request->only('email', 'password');

$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:6'],
]);

// El cast 'hashed' del modelo User se encarga de hashear la contraseña.
$user = User::create($data);

$token = auth('api')->login($user);

return $this->respondWithToken($token, 201);
}

/**
* Autentica con email/password y devuelve un token JWT.
*/
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);

if (! $token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Credenciales inválidas'], 401);
}

return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);

return $this->respondWithToken($token);
}


public function logout()
/**
* Invalida el token actual.
*/
public function logout(): JsonResponse
{
auth()->logout();
auth('api')->logout();

return response()->json(['message' => 'Sesión cerrada correctamente']);
}

protected function respondWithToken($token)
/**
* Emite un nuevo token a partir del actual.
*/
public function refresh(): JsonResponse
{
return $this->respondWithToken(auth('api')->refresh());
}

protected function respondWithToken(string $token, int $status = 200): JsonResponse
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60, // en segundos
]);
'expires_in' => auth('api')->factory()->getTTL() * 60, // segundos
], $status);
}
}
Loading
Loading