diff --git a/app/Http/Controllers/AcceptanceTemplateController.php b/app/Http/Controllers/AcceptanceTemplateController.php index c87b98a..4008275 100644 --- a/app/Http/Controllers/AcceptanceTemplateController.php +++ b/app/Http/Controllers/AcceptanceTemplateController.php @@ -3,89 +3,192 @@ namespace App\Http\Controllers; use App\Models\AcceptanceTemplate; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Exception; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Validation\ValidationException; +/** + * @group Template Management + * + * APIs for managing acceptance letter templates + * @authenticated + */ class AcceptanceTemplateController extends Controller { /** - * Menampilkan semua data acceptance template. - * GET /api/acceptance_templates + * Display all templates + * + * @authenticated + * @return JsonResponse */ - public function index() + public function index(): JsonResponse { - $templates = AcceptanceTemplate::all(); - return response()->json($templates); + try { + $templates = AcceptanceTemplate::latest()->get(); + + return response()->json([ + 'success' => true, + 'data' => $templates + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error fetching templates', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menyimpan data acceptance template baru. - * POST /api/acceptance_templates + * Store new template + * + * @authenticated + * @param Request $request + * @return JsonResponse */ - public function store(Request $request) + public function store(Request $request): JsonResponse { - $validated = $request->validate([ - 'greeting' => 'required|string', - 'content' => 'required|string', - 'closing' => 'required|string', - 'sign_person' => 'required|string', - 'position_name' => 'required|string', - 'signature' => 'nullable|string', - ]); + try { + $validated = $request->validate([ + 'greeting' => 'required|string|max:255', + 'content' => 'required|string', + 'closing' => 'required|string|max:255', + 'sign_person' => 'required|string|max:255', + 'position_name' => 'required|string|max:255', + 'signature' => 'nullable|string' + ]); - $template = AcceptanceTemplate::create($validated); + $template = AcceptanceTemplate::create($validated); - return response()->json([ - 'message' => 'Acceptance Template berhasil dibuat.', - 'data' => $template - ], 201); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil dibuat', + 'data' => $template + ], 201); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error creating template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menampilkan detail data acceptance template. - * GET /api/acceptance_templates/{id} + * Display specific template + * + * @authenticated + * @param string $id + * @return JsonResponse */ - public function show($id) + public function show(string $id): JsonResponse { - $template = AcceptanceTemplate::findOrFail($id); - return response()->json($template); + try { + $template = AcceptanceTemplate::findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $template + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error retrieving template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Mengupdate data acceptance template. - * PUT/PATCH /api/acceptance_templates/{id} + * Update template + * + * @authenticated + * @param Request $request + * @param string $id + * @return JsonResponse */ - public function update(Request $request, $id) + public function update(Request $request, string $id): JsonResponse { - $template = AcceptanceTemplate::findOrFail($id); + try { + $template = AcceptanceTemplate::findOrFail($id); - $validated = $request->validate([ - 'greeting' => 'required|string', - 'content' => 'required|string', - 'closing' => 'required|string', - 'sign_person' => 'required|string', - 'position_name' => 'required|string', - 'signature' => 'nullable|string', - ]); + $validated = $request->validate([ + 'greeting' => 'sometimes|string|max:255', + 'content' => 'sometimes|string', + 'closing' => 'sometimes|string|max:255', + 'sign_person' => 'sometimes|string|max:255', + 'position_name' => 'sometimes|string|max:255', + 'signature' => 'nullable|string' + ]); - $template->update($validated); + $template->update($validated); - return response()->json([ - 'message' => 'Acceptance Template berhasil diupdate.', - 'data' => $template - ]); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil diperbarui', + 'data' => $template + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error updating template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menghapus data acceptance template. - * DELETE /api/acceptance_templates/{id} + * Delete template + * + * @authenticated + * @param string $id + * @return JsonResponse */ - public function destroy($id) + public function destroy(string $id): JsonResponse { - $template = AcceptanceTemplate::findOrFail($id); - $template->delete(); + try { + $template = AcceptanceTemplate::findOrFail($id); + $template->delete(); - return response()->json([ - 'message' => 'Acceptance Template berhasil dihapus.' - ]); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil dihapus' + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error deleting template', + 'error' => $e->getMessage() + ], 500); + } } } diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..b033aae --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,180 @@ +validate([ + 'email' => 'required|email', + 'password' => 'required' + ]); + + if (!Auth::attempt($validated)) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid credentials' + ], 401); + } + + $user = User::where('email', $validated['email'])->first(); + $token = $user->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'success' => true, + 'message' => 'Login successful', + 'data' => [ + 'user' => $user, + 'token' => $token, + 'token_type' => 'Bearer' + ] + ]); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Login failed', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Handle user registration + * + * @param Request $request + * @return JsonResponse + */ + public function register(Request $request): JsonResponse + { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8|confirmed' + ]); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => Hash::make($validated['password']) + ]); + + $token = $user->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'success' => true, + 'message' => 'Registration successful', + 'data' => [ + 'user' => $user, + 'token' => $token, + 'token_type' => 'Bearer' + ] + ], 201); + + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Registration failed', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Handle user logout + * + * @param Request $request + * @return JsonResponse + */ + public function logout(Request $request): JsonResponse + { + try { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Successfully logged out' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Logout failed', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get authenticated user details + * + * @param Request $request + * @return JsonResponse + */ + public function me(Request $request): JsonResponse + { + try { + return response()->json([ + 'success' => true, + 'data' => [ + 'user' => $request->user() + ] + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch user details', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/app/Http/Controllers/CompletionTemplateController.php b/app/Http/Controllers/CompletionTemplateController.php index ca76f96..797da76 100644 --- a/app/Http/Controllers/CompletionTemplateController.php +++ b/app/Http/Controllers/CompletionTemplateController.php @@ -3,89 +3,192 @@ namespace App\Http\Controllers; use App\Models\CompletionTemplate; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Exception; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Validation\ValidationException; +/** + * @group Template Management + * + * APIs for managing completion letter templates + * @authenticated + */ class CompletionTemplateController extends Controller { /** - * Menampilkan semua data completion template. - * GET /api/completion_templates + * Display all completion templates + * + * @authenticated + * @return JsonResponse */ - public function index() + public function index(): JsonResponse { - $templates = CompletionTemplate::all(); - return response()->json($templates); + try { + $templates = CompletionTemplate::latest()->get(); + + return response()->json([ + 'success' => true, + 'data' => $templates + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error fetching templates', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menyimpan data completion template baru. - * POST /api/completion_templates + * Store new completion template + * + * @authenticated + * @param Request $request + * @return JsonResponse */ - public function store(Request $request) + public function store(Request $request): JsonResponse { - $validated = $request->validate([ - 'greeting' => 'required|string', - 'content' => 'required|string', - 'closing' => 'required|string', - 'sign_person' => 'required|string', - 'position_name' => 'required|string', - 'signature' => 'nullable|string', - ]); + try { + $validated = $request->validate([ + 'greeting' => 'required|string|max:255', + 'content' => 'required|string', + 'closing' => 'required|string|max:255', + 'sign_person' => 'required|string|max:255', + 'position_name' => 'required|string|max:255', + 'signature' => 'nullable|string' + ]); - $template = CompletionTemplate::create($validated); + $template = CompletionTemplate::create($validated); - return response()->json([ - 'message' => 'Completion Template berhasil dibuat.', - 'data' => $template - ], 201); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil dibuat', + 'data' => $template + ], 201); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error creating template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menampilkan detail data completion template. - * GET /api/completion_templates/{id} + * Display specific completion template + * + * @authenticated + * @param string $id + * @return JsonResponse */ - public function show($id) + public function show(string $id): JsonResponse { - $template = CompletionTemplate::findOrFail($id); - return response()->json($template); + try { + $template = CompletionTemplate::findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $template + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error retrieving template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Mengupdate data completion template. - * PUT/PATCH /api/completion_templates/{id} + * Update completion template + * + * @authenticated + * @param Request $request + * @param string $id + * @return JsonResponse */ - public function update(Request $request, $id) + public function update(Request $request, string $id): JsonResponse { - $template = CompletionTemplate::findOrFail($id); + try { + $template = CompletionTemplate::findOrFail($id); - $validated = $request->validate([ - 'greeting' => 'required|string', - 'content' => 'required|string', - 'closing' => 'required|string', - 'sign_person' => 'required|string', - 'position_name' => 'required|string', - 'signature' => 'nullable|string', - ]); + $validated = $request->validate([ + 'greeting' => 'sometimes|string|max:255', + 'content' => 'sometimes|string', + 'closing' => 'sometimes|string|max:255', + 'sign_person' => 'sometimes|string|max:255', + 'position_name' => 'sometimes|string|max:255', + 'signature' => 'nullable|string' + ]); - $template->update($validated); + $template->update($validated); - return response()->json([ - 'message' => 'Completion Template berhasil diupdate.', - 'data' => $template - ]); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil diperbarui', + 'data' => $template + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error updating template', + 'error' => $e->getMessage() + ], 500); + } } /** - * Menghapus data completion template. - * DELETE /api/completion_templates/{id} + * Delete completion template + * + * @authenticated + * @param string $id + * @return JsonResponse */ - public function destroy($id) + public function destroy(string $id): JsonResponse { - $template = CompletionTemplate::findOrFail($id); - $template->delete(); + try { + $template = CompletionTemplate::findOrFail($id); + $template->delete(); - return response()->json([ - 'message' => 'Completion Template berhasil dihapus.' - ]); + return response()->json([ + 'success' => true, + 'message' => 'Template berhasil dihapus' + ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'success' => false, + 'message' => 'Template not found' + ], 404); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error deleting template', + 'error' => $e->getMessage() + ], 500); + } } } diff --git a/app/Http/Controllers/LetterController.php b/app/Http/Controllers/LetterController.php index c330b32..a6bf665 100644 --- a/app/Http/Controllers/LetterController.php +++ b/app/Http/Controllers/LetterController.php @@ -33,6 +33,7 @@ class LetterController extends Controller { try { $letters = Letter::with('fileUpload') + ->latest() // Sort by latest created_at ->get() ->map(fn($letter) => $this->formatLetterResponse($letter)); @@ -49,6 +50,40 @@ class LetterController extends Controller } } + /** + * Search letters based on given criteria + * + * @param Request $request + * @return JsonResponse + */ + public function search(Request $request): JsonResponse + { + try { + $query = Letter::with('fileUpload'); + + // Search by NIM + if ($request->has('nim')) { + $query->where('nomor_induk_mahasiswa', 'like', '%' . $request->nim . '%'); + } + + $letters = $query + ->latest() // Always sort by latest + ->get() + ->map(fn($letter) => $this->formatLetterResponse($letter)); + + return response()->json([ + 'success' => true, + 'data' => $letters + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error searching letters', + 'error' => $e->getMessage() + ], 500); + } + } + /** * Store a newly created letter * @@ -69,7 +104,7 @@ class LetterController extends Controller 'tahun_ajaran' => 'required|string', 'start_date' => 'required|date', 'completion_date' => 'required|date', - 'lampiran' => 'nullable|file|mimes:pdf,jpg,png|max:2048', // Max 2MB + 'lampiran' => 'nullable|file|mimes:pdf,jpg,png|max:2048', ]); // Create letter with UUID and optional fields diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..3d6f700 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use Dedoc\Scramble\Scramble; +use Dedoc\Scramble\Support\Generator\OpenApi; +use Dedoc\Scramble\Support\Generator\SecurityScheme; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +22,11 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Scramble::configure() + ->withDocumentTransformers(function (OpenApi $openApi) { + $openApi->secure( + SecurityScheme::http('bearer') + ); + }); } } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..74b3b62 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::create('users', function (Blueprint $table) { - $table->id(); + $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); @@ -29,7 +29,8 @@ return new class extends Migration Schema::create('sessions', function (Blueprint $table) { $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->uuid('user_id')->nullable()->index(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->string('ip_address', 45)->nullable(); $table->text('user_agent')->nullable(); $table->longText('payload'); diff --git a/database/migrations/2025_03_08_140737_create_personal_access_tokens_table.php b/database/migrations/2025_03_08_140737_create_personal_access_tokens_table.php index e828ad8..29db28a 100644 --- a/database/migrations/2025_03_08_140737_create_personal_access_tokens_table.php +++ b/database/migrations/2025_03_08_140737_create_personal_access_tokens_table.php @@ -13,7 +13,7 @@ return new class extends Migration { Schema::create('personal_access_tokens', function (Blueprint $table) { $table->id(); - $table->morphs('tokenable'); + $table->uuidMorphs('tokenable'); $table->string('name'); $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..1b576a5 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,11 +13,10 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', + 'password' => bcrypt('password') ]); } } diff --git a/routes/api.php b/routes/api.php index 09483d4..bb6f742 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\AcceptanceLetterController; use App\Http\Controllers\AcceptanceTemplateController; +use App\Http\Controllers\AuthController; use App\Http\Controllers\CompletionLetterController; use App\Http\Controllers\CompletionTemplateController; use App\Http\Controllers\FileViewController; @@ -23,29 +24,31 @@ use Illuminate\Support\Facades\Route; */ // Authentication Routes -Route::get('/user', function (Request $request) { - return $request->user(); -})->middleware('auth:sanctum'); +Route::post('/login', [AuthController::class, 'login']); -// Letter Management Routes -Route::apiResource('letter', LetterController::class); +// Protected Route +Route::middleware('auth:sanctum')->group(function () { + // Letter Management Routes + Route::apiResource('letter', LetterController::class); + Route::get('/letters/search', [LetterController::class, 'search']); -// File Attachment Routes -Route::get('/lampiran/{id}', [LetterController::class, 'letterLampiran']) - ->name('letters.lampiran'); + // File Attachment Routes + Route::get('/lampiran/{id}', [LetterController::class, 'letterLampiran']) + ->name('letters.lampiran'); -// Template Management Routes -Route::apiResource('completion_templates', CompletionTemplateController::class); -Route::apiResource('acceptance_templates', AcceptanceTemplateController::class); + // Template Management Routes + Route::apiResource('completion_templates', CompletionTemplateController::class); + Route::apiResource('acceptance_templates', AcceptanceTemplateController::class); -// Letter Type Routes -Route::apiResource('acceptance_letters', AcceptanceLetterController::class); -Route::apiResource('completion_letters', CompletionLetterController::class); + // Letter Type Routes + Route::apiResource('acceptance_letters', AcceptanceLetterController::class); + Route::apiResource('completion_letters', CompletionLetterController::class); -// File Viewer Route (Internal use for signed URLs) -Route::get('/file/{id}', [FileViewController::class, 'show']) - ->name('file.view') - ->middleware('signed'); + // File Viewer Route (Internal use for signed URLs) + Route::get('/file/{id}', [FileViewController::class, 'show']) + ->name('file.view') + ->middleware('signed'); +}); /** * API Endpoints Documentation: