feat: add API routes for authentication and task management

- Created api.php for handling authentication routes (register, login, logout) with token middleware.
- Implemented task management routes using API resource controller.

feat: add console routes for artisan commands

- Added console.php to define an inspiring quote command.

feat: add web routes for homepage

- Created web.php to serve the welcome view at the root URL.

chore: add .gitignore files for storage directories

- Added .gitignore files in storage/app, storage/framework, and storage/logs to exclude unnecessary files from version control.

test: add feature and unit tests

- Created ExampleTest in Feature and Unit directories to verify basic application responses and assertions.

build: add Vite configuration for Laravel and Tailwind CSS

- Added vite.config.js to configure Vite with Laravel and Tailwind CSS for asset management.
This commit is contained in:
2026-06-23 20:33:06 +07:00
commit d62725c5af
72 changed files with 11481 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\AuthRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'message' => 'Email atau password salah!'
], 401);
}
$token = $user->createToken('token')->plainTextToken;
return response()->json([
'message' => 'Login berhasil!',
'user' => $user,
'token' => $token
]);
}
public function register(AuthRequest $request)
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
$token = $user->createToken('token')->plainTextToken;
return response()->json([
'message' => 'Register berhasil! Anda sudah otomatis masuk.',
'user' => $user,
'token' => $token
], 201);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logout berhasil!'
]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\TaskRequest;
use App\Http\Repositories\TaskRepository;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller
{
protected $taskRepository;
public function __construct(TaskRepository $taskRepository)
{
$this->taskRepository = $taskRepository;
}
public function index(Request $request)
{
$user = Auth::user();
$filters = [
'is_done' => $request->is_done,
'per_page' => $request->per_page ?? 10,
];
// admin bisa lihat semua task
if ($user->role !== 'admin') {
$filters['user_id'] = $user->id;
}
return TaskResource::collection(
$this->taskRepository->getTasks($filters)
);
}
public function store(TaskRequest $request)
{
$task = $this->taskRepository->store([
'user_id' => Auth::id(),
'title' => $request->title,
'description' => $request->description
]);
return response()->json([
'message' => 'Task berhasil dibuat!',
'task' => new TaskResource($task)
], 201);
}
public function update(TaskRequest $request, $id)
{
$task = Task::find($id);
if (!$task) {
return response()->json(['message' => 'Task tidak ditemukan!'], 404);
}
if ($task->user_id !== Auth::id()) {
return response()->json(['message' => 'Forbidden!'], 403);
}
$task = $this->taskRepository->update($task, [
'title' => $request->title ?? $task->title,
'description' => $request->description ?? $task->description,
'is_done' => $request->is_done ?? $task->is_done,
'status' => $request->status ?? $request->status,
'due_date' => $request->due_date ?? $request->due_date
]);
return response()->json([
'message' => 'Task berhasil diupdate!',
'task' => new TaskResource($task)
]);
}
public function destroy($id)
{
$task = Task::find($id);
if (!$task) {
return response()->json(['message' => 'Task tidak ditemukan!'], 404);
}
if ($task->user_id !== Auth::id()) {
return response()->json(['message' => 'Forbidden!'], 403);
}
$task->delete();
return response()->json([
'message' => 'Task berhasil dihapus!'
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;
use Illuminate\Support\Facades\Log;
class CheckToken
{
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();
// tidak ada token, lanjut
if (!$token) {
return $next($request);
}
// sanctum menyimpan token dengan format "id|token"
$parts = explode('|', $token);
// pastikan format token benar
if (count($parts) !== 2) {
return $next($request);
}
// cari token berdasarkan id
$accessToken = PersonalAccessToken::find($parts[0]);
// token tidak ditemukan
if (!$accessToken) {
return $next($request);
}
// verifikasi hash token
$hashedToken = hash('sha256', $parts[1]);
if (!hash_equals($accessToken->token, $hashedToken)) {
return $next($request);
}
// token valid, tolak request
Log::info('CheckToken - Token valid, tolak request');
return response()->json([
'message' => 'Anda sudah login, silahkan logout terlebih dahulu!'
], 403);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Repositories;
use App\Interfaces\TaskRepositoryInterface;
use App\Models\Task;
class TaskRepository implements TaskRepositoryInterface
{
public function getTasks(array $filters)
{
$query = Task::query();
// filter berdasarkan user
if (isset($filters['user_id'])) {
$query->where('user_id', $filters['user_id']);
}
// filter berdasarkan status is_done
if (isset($filters['is_done'])) {
$query->where('is_done', $filters['is_done']);
}
// pagination
$perPage = $filters['per_page'] ?? 10;
return $query->paginate($perPage);
}
public function store(array $data)
{
return Task::create($data);
}
public function update($task, array $data)
{
$task->update($data);
return $task;
}
public function delete($task)
{
return $task->delete();
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AuthRequest 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.
*
*/
public function rules(): array
{
return [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
];
}
public function messages()
{
return [
'name.required' => 'Username wajib diisi!',
'email.required' => 'Email wajib diisi!',
'email.email' => 'Format Email tidak valid!',
'email.unique' => 'Email sudah terdaftar!',
'password.required' => 'Password wajib diisi!',
'password.min' => 'Password minimal 8 karakter!'
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
];
// jika update, title tidak wajib diisi
if ($this->isMethod('put') || $this->isMethod('patch')) {
$rules['title'] = 'sometimes|string|max:255';
$rules['is_done'] = 'sometimes|boolean';
}
return $rules;
}
public function messages(): array
{
return [
'title.required' => 'Judul task wajib diisi!',
'title.string' => 'Judul task harus berupa teks!',
'title.max' => 'Judul task maksimal 255 karakter!',
'is_done.boolean' => 'Status task harus bernilai 0 atau 1!',
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'is_done' => $this->is_done,
'status' => $this->status,
'due_date' => $this->due_date,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Interfaces;
interface TaskRepositoryInterface
{
public function getTasks(array $filters);
public function store(array $data);
public function update($task, array $data);
public function delete($task);
}

17
app/Models/Task.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = [
'user_id',
'title',
'description',
'is_done',
'status',
'due_date'
];
}

48
app/Models/User.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
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 HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use App\Http\Repositories\TaskRepository;
use App\Interfaces\TaskRepositoryInterface;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(
TaskRepositoryInterface::class,
TaskRepository::class
);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}