Compare commits

...

1 Commits

Author SHA1 Message Date
Leon Morival a3da1bed63 feat: users ulid
Laravel CI-CD (Gitea) / Tests Unitaires (push) Successful in 19s Details
Laravel CI-CD (Gitea) / Build & Push Docker (push) Successful in 1m52s Details
Laravel CI-CD (Gitea) / Déploiement Simplifié (push) Successful in 1s Details
2026-04-22 20:35:47 +02:00
27 changed files with 9 additions and 746 deletions

View File

@ -1,17 +0,0 @@
<?php
namespace App\Enums;
enum PriorityStatus: string
{
case LOW = 'low';
case MEDIUM = 'medium';
case HIGH = "high";
case CRITICAL = "critical";
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Enums;
enum TodoProgress: string
{
case COMPLETED = 'completed';
case IN_PROGRESS = 'in_progress';
case TO_DO = "to_do";
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Pages;
use App\Filament\Resources\Todos\TodoResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTodo extends CreateRecord
{
protected static string $resource = TodoResource::class;
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Pages;
use App\Filament\Resources\Todos\TodoResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
class EditTodo extends EditRecord
{
protected static string $resource = TodoResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Pages;
use App\Filament\Resources\Todos\TodoResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListTodos extends ListRecords
{
protected static string $resource = TodoResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Pages;
use App\Filament\Resources\Todos\TodoResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewTodo extends ViewRecord
{
protected static string $resource = TodoResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Schemas;
use App\Enums\TodoProgress;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
class TodoForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required(),
Select::make('user_id')
->relationship('user', 'name')
->required(),
FileUpload::make('image_url')
->image(),
Select::make('status')
->options(TodoProgress::class)
->default('to_do')
->required(),
Textarea::make('description')
->columnSpanFull(),
]);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Schemas;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
class TodoInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextEntry::make('name'),
TextEntry::make('user.name')
->label('User'),
TextEntry::make('created_at')
->dateTime()
->placeholder('-'),
TextEntry::make('updated_at')
->dateTime()
->placeholder('-'),
ImageEntry::make('image_url')
->placeholder('-'),
TextEntry::make('status')
->badge(),
TextEntry::make('description')
->placeholder('-')
->columnSpanFull(),
]);
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Filament\Resources\Todos\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class TodosTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('user.name')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
ImageColumn::make('image_url'),
TextColumn::make('status')
->badge()
->searchable(),
])
->filters([
//
])
->recordActions([
ViewAction::make(),
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Filament\Resources\Todos;
use App\Filament\Resources\Todos\Pages\CreateTodo;
use App\Filament\Resources\Todos\Pages\EditTodo;
use App\Filament\Resources\Todos\Pages\ListTodos;
use App\Filament\Resources\Todos\Pages\ViewTodo;
use App\Filament\Resources\Todos\Schemas\TodoForm;
use App\Filament\Resources\Todos\Schemas\TodoInfolist;
use App\Filament\Resources\Todos\Tables\TodosTable;
use App\Models\Todo;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class TodoResource extends Resource
{
protected static ?string $model = Todo::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
{
return TodoForm::configure($schema);
}
public static function infolist(Schema $schema): Schema
{
return TodoInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return TodosTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListTodos::route('/'),
'create' => CreateTodo::route('/create'),
'view' => ViewTodo::route('/{record}'),
'edit' => EditTodo::route('/{record}/edit'),
];
}
}

View File

@ -29,7 +29,7 @@ class AuthController extends Controller
'avatar_url' => $avatarPath,
]);
$token = $user->createToken('todos', ['user'])->plainTextToken;
$token = $user->createToken('api', ['user'])->plainTextToken;
return response()->json([
'user' => new UserResource($user),
@ -48,7 +48,7 @@ class AuthController extends Controller
], 401);
}
$token = $user->createToken('todos')->plainTextToken;
$token = $user->createToken('api')->plainTextToken;
return response()->json([
'user' => new UserResource($user),

View File

@ -1,101 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\TodoRequest;
use App\Http\Resources\todoResource;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class todoController extends Controller
{
/**
* Liste des todos de l'utilisateur connecté.
*/
public function index(Request $request)
{
$perPage = (int) $request->query('per_page', 15);
$perPage = max(1, min($perPage, 100));
$todos = $request->user()
->todos()
->latest()
->paginate($perPage)
->withQueryString();
return todoResource::collection($todos);
}
/**
* Création d'un nouveau todo avec image optionnelle.
*/
public function store(TodoRequest $request)
{
$data = $request->validated();
if ($request->hasFile('image')) {
// On stocke dans le dossier 'todos' du disque 'public'
// $path contiendra par ex: "todos/nom_du_fichier.jpg"
$path = $request->file('image')->store('todos', 'public');
$data['image_url'] = $path;
}
$todo = $request->user()->todos()->create($data);
return new todoResource($todo);
}
/**
* Mise à jour du todo (texte, statut ou image).
*/
public function update(TodoRequest $request, Todo $todo)
{
Gate::authorize('update', $todo);
$data = $request->validated();
if ($request->hasFile('image')) {
// 1. Supprimer l'ancienne image si elle existe
if ($todo->image_url) {
Storage::disk('public')->delete($todo->image_url);
}
// 2. Stocker la nouvelle image
$path = $request->file('image')->store('todos', 'public');
$data['image_url'] = $path;
}
$todo->update($data);
return new todoResource($todo);
}
/**
* Affichage d'un todo précis.
*/
public function show(Todo $todo)
{
Gate::authorize('view', $todo);
return new todoResource($todo);
}
/**
* Suppression d'un todo et de son image associée.
*/
public function destroy(Todo $todo)
{
Gate::authorize('delete', $todo);
// Nettoyage : supprimer l'image du serveur avant de supprimer le record
if ($todo->image_url) {
Storage::disk('public')->delete($todo->image_url);
}
$todo->delete();
return response()->json(['message' => 'Todo supprimé avec succès']);
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\PriorityStatus;
use App\Enums\TodoProgress;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class TodoRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => [$this->isMethod('post') ? 'required' : 'sometimes', 'string', 'max:255'],
'status' => ['sometimes','required', Rule::enum(TodoProgress::class)],
'description'=> ['sometimes','nullable', 'string'],
'image' => ['sometimes', 'nullable', 'image', 'max:2048'],
'due_date' => ['sometimes', 'nullable', 'date'],
'priority' => ['sometimes', 'required', Rule::enum(PriorityStatus::class)],
];
}
public function messages(): array
{
return [
'image.max' => "L'image ne doit pas dépasser 2 Mo.",
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class todoResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,
'description' => $this->description,
'image_url' => $this->image_url ? asset(Storage::url($this->image_url)): null,
'due_date' => $this->due_date,
'priority' => $this->priority,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'user_id' => $this->user_id,
];
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Models;
use App\Enums\PriorityStatus;
use App\Enums\TodoProgress;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Scout\Searchable;
class Todo extends Model
{
use HasFactory, Searchable;
protected $table = 'todo';
protected $fillable = [
'name',
'status',
'description',
'image_url',
'due_date',
'priority',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
protected function casts(): array
{
return [
'status' => TodoProgress::class,
'due_date' => 'date',
'priority' => PriorityStatus::class,
];
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
$array = $this->toArray();
// Customize the data array...
return $array;
}
}

View File

@ -5,8 +5,8 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
@ -14,7 +14,7 @@ use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements FilamentUser
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens;
use HasFactory, Notifiable, HasApiTokens, HasUlids;
/**
* The attributes that are mass assignable.
@ -51,10 +51,6 @@ class User extends Authenticatable implements FilamentUser
];
}
public function todos(): HasMany
{
return $this->hasMany(Todo::class);
}
public function isAdmin()
{

View File

@ -1,50 +0,0 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Todo;
use Illuminate\Support\Facades\Log;
class TodoPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Todo $todo): bool
{
return $user->id === $todo->user_id;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\Todo;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class todoFactory extends Factory
{
protected $model = Todo::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'is_done' => $this->faker->boolean(),
'image_url' => null,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'user_id' => User::factory(),
];
}
}

View File

@ -12,11 +12,13 @@ return new class extends Migration
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->ulid('id')->primary();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('avatar_url', 2048)->nullable();
$table->rememberToken();
$table->timestamps();
});
@ -29,7 +31,7 @@ return new class extends Migration
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->foreignUlid('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');

View File

@ -13,7 +13,7 @@ return new class extends Migration
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->ulidMorphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();

View File

@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('todo', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->boolean('is_done')->default(false);
$table->foreignId('user_id')->constrained('users');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('todo');
}
};

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('todo', function (Blueprint $table) {
$table->string('image_url')->nullable()->after('is_done');
});
}
public function down(): void
{
Schema::table('todo', function (Blueprint $table) {
$table->dropColumn('image_url');
});
}
};

View File

@ -1,29 +0,0 @@
<?php
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::table('todo', function (Blueprint $table) {
$table->string('status')->default('to_do');
$table->dropColumn('is_done');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('todo', function (Blueprint $table) {
$table->dropColumn('status');
});
}
};

View File

@ -1,28 +0,0 @@
<?php
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::table('todo', function (Blueprint $table) {
$table->text("description")->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('todo', function (Blueprint $table) {
$table->dropColumn('description');
});
}
};

View File

@ -1,28 +0,0 @@
<?php
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::table('users', function (Blueprint $table) {
$table->string('avatar_url', 2048)->nullable()->after('password');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar_url');
});
}
};

View File

@ -1,30 +0,0 @@
<?php
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::table('todo', function (Blueprint $table) {
$table->string('priority')->default('low');
$table->date('due_date')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('todo', function (Blueprint $table) {
$table->dropColumn('priority');
$table->dropColumn('due_date');
});
}
};

View File

@ -3,7 +3,6 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\todoController;
Route::prefix('auth')->group(function (): void {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
@ -14,10 +13,3 @@ Route::prefix('auth')->group(function (): void {
});
});
Route::middleware('auth:sanctum')->prefix('todos')->group(function (): void {
Route::get('/', [todoController::class, 'index']);
Route::post('/', [todoController::class, 'store']);
Route::get('/{todo}', [todoController::class, 'show']);
Route::patch('/{todo}', [todoController::class, 'update']);
Route::delete('/{todo}', [todoController::class, 'destroy']);
});