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
91 changes: 91 additions & 0 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace App\Http\Controllers;

use App\Http\Requests\TokenRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
/**
* Generate a new API token for the user.
*
* @throws ValidationException
*/
public function token(TokenRequest $request): JsonResponse
{
/** @var array{email: string, password: string, device_name?: string} $validated */
$validated = $request->validated();

$user = User::where('email', $validated['email'])->first();

if (! $user || ! Hash::check($validated['password'], $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}

$deviceName = $validated['device_name'] ?? 'api-client';
$token = $user->createToken($deviceName);

return response()->json([
'token' => $token->plainTextToken,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
],
], 201);
}

/**
* Revoke the current user's access token.
*/
public function logout(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();
$user->currentAccessToken()->delete();

return response()->json([
'message' => 'Token revoked successfully.',
]);
}

/**
* Revoke all tokens for the authenticated user.
*/
public function logoutAll(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();

$user->tokens()->delete();

return response()->json([
'message' => 'All tokens revoked successfully.',
]);
}

/**
* Get the authenticated user's information.
*/
public function me(Request $request): JsonResponse
{
/** @var User $user */
$user = $request->user();

return response()->json([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
}
}
48 changes: 48 additions & 0 deletions app/Http/Requests/TokenRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TokenRequest 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 [
'email' => 'required|email',
'password' => 'required|string',
'device_name' => 'nullable|string|max:255',
];
}

/**
* Get custom validation error messages.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'email.required' => 'Email address is required.',
'email.email' => 'Please provide a valid email address.',
'password.required' => 'Password is required.',
'device_name.max' => 'Device name must not exceed 255 characters.',
];
}
}
3 changes: 2 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;

/**
* The attributes that are mass assignable.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
7 changes: 5 additions & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ parameters:
# PEST testing framework limitations: PHPStan cannot infer Laravel TestCase methods
# in PEST's closure-based tests. These are valid Laravel testing methods.
-
message: '#Call to an undefined method PHPUnit\\Framework\\TestCase::(get|post|put|patch|delete|getJson|postJson|putJson|patchJson|deleteJson)\(\)#'
message: '#Call to an undefined method PHPUnit\\Framework\\TestCase::(get|post|put|patch|delete|getJson|postJson|putJson|patchJson|deleteJson|withHeader|actingAs)\(\)#'
path: tests/Feature/*
-
message: '#Cannot call method (assertStatus|assertJson|assertJsonStructure|assertJsonFragment|assertJsonPath|assertExactJson|assertSee|assertDontSee)\(\) on mixed#'
message: '#Cannot call method (assertStatus|assertJson|assertJsonStructure|assertJsonFragment|assertJsonPath|assertExactJson|assertJsonValidationErrors|assertJsonMissing|assertSee|assertDontSee|assertSuccessful|assertForbidden|assertNotFound|assertRedirect|assertSessionHas|assertSessionMissing|assertViewIs|assertViewHas|assertCreated|assertOk|assertUnprocessable|assertUnauthorized|json|getJson|postJson|putJson|patchJson|deleteJson)\(\) on mixed#'
path: tests/Feature/*
-
message: '#Cannot (call method|access property) .* on (App\\Models\\User|Laravel\\Sanctum\\PersonalAccessToken)\|null#'
path: tests/Feature/*
16 changes: 9 additions & 7 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;

/*
Expand All @@ -27,12 +28,13 @@

// API v1 routes
Route::prefix('v1')->group(function () {
// Authentication routes
// Route::post('/login', [AuthController::class, 'login']);
// Route::post('/register', [AuthController::class, 'register']);
// Authentication routes (public)
Route::post('/auth/token', [AuthController::class, 'token']);

// Protected routes
// Route::middleware('auth:sanctum')->group(function () {
// Route::apiResource('users', UserController::class);
// });
// Protected routes (require auth:sanctum)
Route::middleware('auth:sanctum')->group(function () {
Route::post('/auth/logout', [AuthController::class, 'logout']);
Route::post('/auth/logout-all', [AuthController::class, 'logoutAll']);
Route::get('/me', [AuthController::class, 'me']);
});
});
Loading