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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ The project includes a lightweight production deployment workflow:

---

## 🩺 Health Check

A lightweight endpoint used for uptime monitoring and deployment verification:

`GET /api/health → { "status": "ok", "timestamp": "..." }`

## 🔄 Message Pipeline (RabbitMQ)

This project implements a production-grade message pipeline:
Expand Down
28 changes: 28 additions & 0 deletions app/DTOs/RegisterDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\DTOs;

class RegisterDTO
{
public function __construct(
public string $name,
public string $email,
public string $password
) {}

/**
* @param array{
* name: string,
* email: string,
* password: string
* } $data
*/
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
password: $data['password']
);
}
}
15 changes: 13 additions & 2 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@

namespace App\Http\Controllers;

use App\Http\Requests\RegisterRequest;
use App\Http\Resources\UserResource;
use App\Http\Services\AuthService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use PHPOpenSourceSaver\JWTAuth\JWTGuard;

class AuthController extends Controller
{
public function register(RegisterRequest $request, AuthService $service): JsonResponse
{
$dto = $request->toDTO();
$data = $service->register($dto);

return response()->json($data, 201);
}

public function login(Request $request): JsonResponse
{
/** @var JWTGuard $guard */
Expand Down Expand Up @@ -56,11 +67,11 @@ public function logout(): Response
return response()->noContent();
}

public function me(): JsonResponse
public function me(): UserResource
{
/** @var JWTGuard $guard */
$guard = auth('api');

return response()->json($guard->user());
return UserResource::make($guard->user());
}
}
16 changes: 16 additions & 0 deletions app/Http/Controllers/HealthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;

class HealthController extends Controller
{
public function __invoke(): JsonResponse
{
return response()->json([
'status' => 'ok',
'timestamp' => now()->toISOString(),
]);
}
}
37 changes: 37 additions & 0 deletions app/Http/Requests/RegisterRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Http\Requests;

use App\DTOs\RegisterDTO;
use Illuminate\Foundation\Http\FormRequest;

class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:80'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
'password_confirmation' => ['required', 'same:password'],
];
}

public function toDTO(): RegisterDTO
{
return RegisterDTO::fromArray($this->validated());
}
}
28 changes: 28 additions & 0 deletions app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Http\Resources;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/**
* @mixin User
*/
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar_url' => $this->avatar_url,
];
}
}
35 changes: 35 additions & 0 deletions app/Http/Services/AuthService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Http\Services;

use App\DTOs\RegisterDTO;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use PHPOpenSourceSaver\JWTAuth\JWTGuard;

class AuthService
{
/**
* @return array<string, mixed>
*/
public function register(RegisterDTO $dto): array
{
$user = User::create([
'name' => $dto->name,
'email' => $dto->email,
'password' => Hash::make($dto->password),
]);

/** @var JWTGuard $guard */
$guard = auth('api');
$token = $guard->login($user);

return [
'token' => $token,
'token_type' => 'Bearer',
'expires_in' => (int) config('jwt.ttl', 60) * 60,
'user' => UserResource::make($guard->user()),
];
}
}
13 changes: 13 additions & 0 deletions docs/api-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ This document contains practical examples showing how to interact with the Task

## Authentication

### Register

```bash
POST /api/auth/register
{
"name": "Demo",
"email": "demo@example.com",
"password": "secret123",
"password_confirmation": "secret123"
}
```

### Login

```bash
POST /api/auth/login
{
Expand Down
63 changes: 62 additions & 1 deletion docs/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ tags:
description: Add comments to tasks.
- name: Task Labels
description: Attach/detach labels to tasks.
- name: System
description: Lightweight system endpoints for health and uptime checks.

security:
- bearerAuth: [] # default: all endpoints require JWT unless overridden per-path
Expand Down Expand Up @@ -215,6 +217,43 @@ components:
required: [data]

paths:
/api/auth/register:
post:
operationId: auth_register
tags: [Auth]
security: []
summary: Register a new user
description: Create a new user account and automatically return a JWT access token.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string, example: John Doe }
email: { type: string, format: email, example: john@example.com }
password: { type: string, format: password, minLength: 8, example: password123 }
password_confirmation: { type: string, format: password, minLength: 8, example: password123 }
required: [ name, email, password, password_confirmation ]
responses:
'201':
description: User registered successfully
content:
application/json:
schema:
type: object
properties:
token: { type: string }
token_type: { type: string, example: Bearer }
expires_in: { type: integer, example: 3600 }
user: { $ref: '#/components/schemas/User' }
'422':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }

/api/auth/login:
post:
operationId: auth_login
Expand Down Expand Up @@ -265,7 +304,11 @@ paths:
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
schema:
type: object
properties:
data:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized

Expand Down Expand Up @@ -815,3 +858,21 @@ paths:
'403': { description: Forbidden }
'404': { description: Not Found }
'409': { description: Conflict (cross-project) }

/api/health:
get:
operationId: health_check
tags: [System]
security: []
summary: Health check endpoint
description: Lightweight health probe used by load balancers, uptime monitoring and deployment checks. Does not touch external dependencies and always returns quickly.
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: ok }
timestamp: { type: string, format: date-time, example: 2025-12-08T12:34:56Z }
42 changes: 42 additions & 0 deletions docs/postman/task_manager_api.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,34 @@
{
"name": "Auth",
"item": [
{
"name": "Register",
"request": {
"method": "POST",
"header": [
{ "key": "Accept", "value": "application/json" },
{ "key": "Content-Type", "value": "application/json" }
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"{{$randomFullName}}\",\n \"email\": \"{{$randomEmail}}\",\n \"password\": \"secret123\",\n \"password_confirmation\": \"secret123\"\n}"
},
"url": "{{base_url}}/api/auth/register"
},
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status 201', () => pm.response.to.have.status(201));",
"const json = pm.response.json();",
"pm.expect(json).to.have.property('token');",
"pm.environment.set('token', json.token);"
]
}
}
]
},
{
"name": "Login",
"request": {
Expand Down Expand Up @@ -483,6 +511,20 @@
}
}
]
},
{
"name": "System",
"item": [
{
"name": "Health Check",
"request": {
"method": "GET",
"header": [],
"url": "{{base_url}}/api/health"
},
"response": []
}
]
}
]
}
Loading