initial commit

This commit is contained in:
2026-06-08 19:58:50 +07:00
commit f7da9c6f78
154 changed files with 19586 additions and 0 deletions
@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\ResponseJson; // Import the ResponseJson helper
use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\EditUserRequest;
use App\Http\Requests\UserIdRequest;
use App\Services\AdminUserService;
use App\Services\DiseaseService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class AdminUserController extends Controller
{
protected AdminUserService $adminUserService;
public function __construct(AdminUserService $adminUserService)
{
$this->adminUserService = $adminUserService;
}
public function createUser(CreateUserRequest $request): JsonResponse
{
$validatedData = $request->validated();
[$success, $message, $data] = $this->adminUserService->createUser($validatedData);
if(!$success){
return ResponseJson::failedResponse($message, $validatedData);
}
if ($validatedData['role'] === 'operator' && isset($validatedData['disease_ids'])) {
[$assignSuccess, $assignMessage, $data] = $this->adminUserService->assignOperatorToDiseases($data['id'], $validatedData['disease_ids']);
if ($assignSuccess) {
$message .= ' ' . $assignMessage;
} else {
return ResponseJson::failedResponse($assignMessage);
}
}
return ResponseJson::successResponse($message, $data);
}
public function approveUser(UserIdRequest $request, $userId): JsonResponse
{
[$success, $message, $data] = $this->adminUserService->approveUser($userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('User approved successfully.', $data);
}
public function rejectUser(UserIdRequest $request, $userId): JsonResponse
{
[$success, $message, $data] = $this->adminUserService->rejectUser($userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('User rejected successfully.', $data);
}
public function editUser(EditUserRequest $request, $id): JsonResponse
{
[$success, $message, $data] = $this->adminUserService->editUser($id, $request->validated());
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('User updated successfully.', $data);
}
public function deleteUser(UserIdRequest $request, $userId): JsonResponse
{
[$success, $message, $data] = $this->adminUserService->deleteUser($userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('User deleted successfully.', $data);
}
public function getUsers(Request $request): JsonResponse
{
[$success, $message, $data]= $this->adminUserService->getUsers($request->all());
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Users retrieved successfully.', $data);
}
public function getUserDetails(UserIdRequest $request, $userId): JsonResponse
{
[$success, $message, $data] = $this->adminUserService->getUserDetails($userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('User details retrieved successfully.', $data);
}
public function getUserProfile(Request $request)
{
$user = $request->user(); // Get the authenticated user
return response()->json([
'success' => true,
'message' => 'Profile retrieved successfully.',
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
'institution' => $user->institution,
'gender' => $user->gender,
'phone' => $user->phone_number,
'approval_status' => $user->approval_status,
'created_at' => $user->created_at,
'updated_at' => $user->updated_at,
]
]);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\ResponseJson;
use App\Http\Requests\LoginUserRequest;
use App\Http\Requests\RegisterUserRequest;
use App\Services\AuthUserService;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
private AuthUserService $authUserService;
public function __construct(AuthUserService $authUserService)
{
$this->authUserService = $authUserService;
}
public function register(RegisterUserRequest $request)
{
[$success, $message, $data] = $this->authUserService->register(
$request->name,
$request->email,
$request->password,
$request->institution,
$request->gender,
$request->phone_number,
$request->tujuan_permohonan,
);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse($message, $data);
}
public function login(LoginUserRequest $request)
{
[$success, $message, $data] = $this->authUserService->login(
$request->email,
$request->password
);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse($message, $data);
}
public function logout(Request $request)
{
$this->authUserService->logout($request->user());
return ResponseJson::successResponse('Berhasil Logout', []);
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers;
use App\Services\CommentService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Helpers\ResponseJson;
use App\Http\Requests\CreateCommentRequest;
use App\Http\Requests\DeleteCommentRequest;
use App\Http\Requests\EditCommentRequest;
class CommentController extends Controller
{
protected CommentService $commentService;
public function __construct(CommentService $commentService)
{
$this->commentService = $commentService;
}
public function createComment(CreateCommentRequest $request): JsonResponse
{
$data = $request->validated();
$userId = $request->user()->id;
[$success, $message, $comment] = $this->commentService->createComment($data, $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $comment);
}
return ResponseJson::successResponse($message, $comment);
}
public function editComment(EditCommentRequest $request, $diseaseId, $commentId): JsonResponse
{
$data = $request->validated();
$userId = $request->user()->id;
[$success, $message, $comment] = $this->commentService->editComment($commentId, $data, $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $comment);
}
return ResponseJson::successResponse($message, $comment);
}
public function deleteComment(DeleteCommentRequest $request, $diseaseId, $commentId): JsonResponse
{
$userId = request()->user()->id;
[$success, $message, $comment] = $this->commentService->deleteComment($commentId, $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $comment);
}
return ResponseJson::successResponse($message, $comment);
}
public function getComments($diseaseId): JsonResponse
{
[$success, $message, $comments] = $this->commentService->getComments($diseaseId);
if (!$success) {
return ResponseJson::failedResponse($message, $comments);
}
return ResponseJson::successResponse($message, $comments);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}
@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\ResponseJson; // Import the ResponseJson helper
use App\Services\DiseaseService;
use App\Http\Requests\CreateDiseaseRequest;
use App\Http\Requests\EditDiseaseRequest;
use App\Http\Requests\DiseaseIdRequest;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class DiseaseController extends Controller
{
protected DiseaseService $diseaseService;
public function __construct(DiseaseService $diseaseService)
{
$this->diseaseService = $diseaseService;
}
public function createDisease(CreateDiseaseRequest $request): JsonResponse
{
$userId = $request->user()->id;
[$success, $message, $data] = $this->diseaseService->createDisease($request->validated(), $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse($message, $data);
}
public function editDisease(EditDiseaseRequest $request, $diseaseId): JsonResponse
{
// Log::info('EditDisease Request Data: ', $request->validated());
$userId = $request->user()->id;
[$success, $message, $data] = $this->diseaseService->editDisease($diseaseId, $request->validated(), $userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease updated successfully.', $data);
}
public function deleteDisease(DiseaseIdRequest $request, $diseaseId): JsonResponse
{
$userId = $request->user()->id;
[$success, $message, $data] = $this->diseaseService->deleteDisease($diseaseId, $userId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease deleted successfully.', $data);
}
public function getDiseases(Request $request): JsonResponse
{
$user = $request->user();
[$success, $message, $data] = $this->diseaseService->getDisease($request->all(), $user);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease retrieved successfully.', $data);
}
public function getDiseaseDetails(DiseaseIdRequest $request, $diseaseId): JsonResponse
{
[$success, $message, $data] = $this->diseaseService->getDiseaseDetails($diseaseId);
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease details retrieved successfully.', $data);
}
public function getStatistics(Request $request): JsonResponse
{
[$success, $message, $data]= $this->diseaseService->getStatistics($request->all());
if(!$success){
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease statistics retrieved successfully.', $data);
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\ResponseJson; // Import the ResponseJson helper
use App\Services\DiseaseRecordService;
use App\Http\Requests\CreateDiseaseRecordRequest;
use App\Http\Requests\CreateDiseaseRequest;
use App\Http\Requests\ShowDiseaseRecordRequest;
use App\Http\Requests\EditDiseaseRecordRequest;
use App\Http\Requests\DeleteDiseaseRecordRequest;
use App\Http\Requests\IndexDiseaseRecordRequest;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class DiseaseRecordController extends Controller
{
protected DiseaseRecordService $diseaseRecordService;
public function __construct(DiseaseRecordService $diseaseRecordService)
{
$this->diseaseRecordService = $diseaseRecordService;
}
public function createDiseaseRecord(CreateDiseaseRecordRequest $request): JsonResponse
{
//var_dump($request->all());
// return ResponseJson::failedResponse("tez", []);
$userId = $request->user()->id;
//echo(is_array($request->input('record_data')));
[$success, $message, $data] = $this->diseaseRecordService->createDiseaseRecord($request->validated(), $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse($message, $data);
}
public function editDiseaseRecord(EditDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
{
$userId = $request->user()->id;
[$success, $message, $data] = $this->diseaseRecordService->editDiseaseRecord($recordId, $request->validated(), $userId);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease record updated successfully.', $data);
}
public function deleteDiseaseRecord(DeleteDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
{
$userId = $request->user()->id;
[$success, $message] = $this->diseaseRecordService->deleteDiseaseRecord($recordId, $userId);
if (!$success) {
return ResponseJson::failedResponse($message);
}
return ResponseJson::successResponse($message);
}
public function getDiseaseRecords(IndexDiseaseRecordRequest $request, $diseaseId): JsonResponse
{
[$success, $message, $data] = $this->diseaseRecordService->getDiseaseRecords($diseaseId, $request->all());
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse('Disease records retrieved successfully.', $data);
}
public function getDiseaseRecordDetails(ShowDiseaseRecordRequest $request, $diseaseId, $recordId): JsonResponse
{
[$success, $message, $data] = $this->diseaseRecordService->getDiseaseRecordDetails($diseaseId, $recordId);
if (!$success) {
return ResponseJson::failedResponse($message, $data);
}
return ResponseJson::successResponse($message, $data);
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers;
use App\Models\Disease;
use App\Models\DiseaseRecord;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Services\FileStorageService;
use App\Http\Requests\IndexDiseaseRecordRequest;
use App\Http\Requests\ShowDiseaseRecordRequest;
use Illuminate\Support\Facades\URL;
class DiseaseRecordExportController extends Controller
{
protected $fileStorage;
public function __construct(FileStorageService $fileStorage)
{
$this->fileStorage = $fileStorage;
$this->middleware(['auth:sanctum', 'checkDiseaseAccess']);
}
/**
* Export all disease records to CSV
*/
public function exportToCsv(IndexDiseaseRecordRequest $request, $diseaseId)
{
// Validate disease exists
$disease = Disease::findOrFail($diseaseId);
// Get all columns from schema
$columns = collect($disease->schema['columns'])
->pluck('name')
->toArray();
// Generate filename
$filename = sprintf(
'disease-records-%s-%s.csv',
$disease->name,
now()->format('Y-m-d-His')
);
return $this->generateCsvResponse($disease, $columns, $filename);
}
/**
* Export single record to CSV
*/
public function exportSingleRecord(ShowDiseaseRecordRequest $request, $diseaseId, $recordId)
{
$disease = Disease::findOrFail($diseaseId);
$record = DiseaseRecord::where('disease_id', $diseaseId)
->where('id', $recordId)
->firstOrFail();
$columns = collect($disease->schema['columns'])
->pluck('name')
->toArray();
$filename = sprintf(
'disease-record-%s-%d-%s.csv',
$disease->name,
$recordId,
now()->format('Y-m-d-His')
);
return $this->generateCsvResponse($disease, $columns, $filename, $record);
}
/**
* Generate CSV response for both single and multiple records
*/
private function generateCsvResponse($disease, $columns, $filename, $singleRecord = null)
{
return new StreamedResponse(function () use ($disease, $columns, $singleRecord) {
$handle = fopen('php://output', 'w');
// Write headers
fputcsv($handle, [...$columns, 'created_at', 'updated_at']);
if ($singleRecord) {
// Write single record
$this->writeRecordToCsv($handle, $singleRecord, $columns);
} else {
// Write all records
DiseaseRecord::where('disease_id', $disease->id)
->chunk(100, function ($records) use ($handle, $columns) {
foreach ($records as $record) {
$this->writeRecordToCsv($handle, $record, $columns);
}
});
}
fclose($handle);
}, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Cache-Control' => 'no-cache, no-store, must-revalidate',
'Pragma' => 'no-cache',
'Expires' => '0',
]);
}
private function generateSignedFileUrl($path)
{
// Remove any leading slashes
$path = ltrim($path, '/');
return URL::temporarySignedRoute(
'files.download.record',
now()->addHours(24), // URLs valid for 24 hours
['path' => $path]
);
}
private function generatePublicFileUrl($path)
{
$domain = env('APP_URL');
return $domain . '/storage/public/' . $path;
}
/**
* Write a single record to CSV
*/
private function writeRecordToCsv($handle, $record, $columns)
{
$row = [];
foreach ($columns as $column) {
$value = $record->data[$column] ?? '';
// Handle file paths
if (is_array($value)) {
// Convert array of file paths to public URLs
$urls = array_map(
fn($path) => $this->generatePublicFileUrl($path),
$value
);
$value = implode(' | ', $urls);
} elseif (is_string($value) && str_contains($value, 'diseases/records/')) {
// Convert single file path to public URL
$value = $this->generatePublicFileUrl($value);
}
$row[] = $value;
}
// Add timestamps
$row[] = $record->created_at->format('Y-m-d H:i:s');
$row[] = $record->updated_at->format('Y-m-d H:i:s');
fputcsv($handle, $row);
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
namespace App\Http\Controllers;
use App\Services\FileStorageService;
use Illuminate\Http\Request;
use App\Models\DiseaseRecord;
use App\Models\Disease;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Illuminate\Support\Facades\Log;
class FileController extends Controller
{
protected $fileStorage;
public function __construct(FileStorageService $fileStorage)
{
$this->fileStorage = $fileStorage;
$this->middleware(['auth:sanctum', 'checkDiseaseAccess'])->only(['previewFile']);
// $this->middleware('signed')->only(['downloadRecord']); // Add signed middleware for downloads
}
public function previewFile(Request $request, string $path)
{
try {
$path = $this->sanitizePath($path);
$storagePath = storage_path('app/public/' . $path);
if (!file_exists($storagePath)) {
Log::error('File not found', ['path' => $path]);
abort(404, 'File not found');
}
// Determine MIME type
$mimeType = mime_content_type($storagePath);
// Only allow audio files for preview (extendable later)
if (!str_starts_with($mimeType, 'audio/')) {
abort(415, 'Unsupported media type for inline preview');
}
// Stream audio file
return response()->file($storagePath, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'inline',
]);
} catch (\Exception $e) {
Log::error('File preview failed', [
'error' => $e->getMessage(),
'path' => $path,
'user_id' => $request->user()->id ?? null,
]);
abort(500, 'Failed to preview file');
}
}
public function downloadRecord(Request $request, string $path)
{
try {
if (!$request->hasValidSignature()) {
abort(401, 'Invalid signature');
}
// Clean up the path to match storage structure
$path = $this->sanitizePath($path);
// Determine the full storage path
$storagePath = storage_path('app/public/' . $path);
if (!file_exists($storagePath)) {
Log::error('File not found', [
'path' => $path,
'full_path' => $storagePath
]);
abort(404, 'File not found');
}
// Find record containing this file path
$record = $this->findRecordByPath($path);
if (!$record) {
abort(404, 'Associated record not found');
}
// Log download attempt
Log::info('File download initiated via signed URL', [
'path' => $path,
'disease_id' => $record->disease_id,
'record_id' => $record->id
]);
return $this->fileStorage->streamFile('public/' . $path);
} catch (\Exception $e) {
Log::error('File download failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'path' => $path ?? 'undefined'
]);
abort(500, 'Failed to download file: ' . $e->getMessage());
}
}
private function sanitizePath(string $path): string
{
// Remove any directory traversal attempts and extra slashes
$path = str_replace(['..', '//'], ['', '/'], $path);
// Remove /api/files/records if present at the start
$path = preg_replace('/^\/?(api\/files\/records\/)?/', '', $path);
return trim($path, '/');
}
private function findRecordByPath(string $path): ?DiseaseRecord
{
// First find all diseases that have file fields
$diseasesWithFiles = Disease::whereRaw("jsonb_path_exists(schema->'columns',
'$.** ? (@.type == \"file\")')")
->get();
if ($diseasesWithFiles->isEmpty()) {
return null;
}
// Get all file field names from the schemas
$fileFields = [];
foreach ($diseasesWithFiles as $disease) {
$columns = $disease->schema['columns'] ?? [];
foreach ($columns as $column) {
if (($column['type'] ?? '') === 'file') {
$fileFields[$disease->id][] = [
'name' => $column['name'],
'multiple' => $column['multiple'] ?? false
];
}
}
}
// Build query based on discovered file fields
return DiseaseRecord::where(function ($query) use ($path, $fileFields) {
foreach ($fileFields as $diseaseId => $fields) {
foreach ($fields as $field) {
if ($field['multiple']) {
// Handle array fields using ? operator
$query->orWhere(function ($q) use ($path, $diseaseId, $field) {
$q->where('disease_id', $diseaseId)
->whereRaw("data->? ?? ?", [$field['name'], $path]);
});
} else {
// Handle single file fields
$query->orWhere(function ($q) use ($path, $diseaseId, $field) {
$q->where('disease_id', $diseaseId)
->whereRaw("data->>? = ?", [$field['name'], $path]);
});
}
}
}
})->first();
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Helpers\ResponseJson;
use Illuminate\Support\Facades\Log;
use App\Services\LogActionService;
class LogActionController extends Controller
{
protected LogActionService $logActionService;
public function __construct(LogActionService $logActinoService)
{
$this->logActionService = $logActinoService;
}
public function getLogActions(Request $request){
[$success, $message, $logs] = $this->logActionService->getLogs($request->all());
if (!$success) {
return ResponseJson::errorResponse($message);
}
return ResponseJson::successResponse($message, $logs);
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
//\App\Http\Middleware\EnsureAcceptJson::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\App\Http\Middleware\CheckApiKey::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\EnsureAcceptJson::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'checkRole' => \App\Http\Middleware\CheckRole::class,
'checkDiseaseAccess' => \App\Http\Middleware\CheckDiseaseAccess::class,
'checkCommentAuthorOrAdmin' => \App\Http\Middleware\CheckCommentAuthorOrAdmin::class,
'ensureOperatorManagesDisease' => \App\Http\Middleware\EnsureOperatorManagesDisease::class,
];
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\DB;
class ApiKeyMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next)
{
// TO be implemented in middleware
$apiKey = $request->header('X-API-Key');
if (!$apiKey || !DB::table('api_keys')->where('key', $apiKey)->exists()) {
return response()->json(['message' => 'Unauthorized'], 401);
}
return $next($request);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use App\Helpers\ResponseJson;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
if ($request->expectsJson()) {
return ResponseJson::unauthorizeResponse('You are not logged in', []);
}
// return ResponseJson::failedResponse('Bad Request', []);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Helpers\ResponseJson;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckApiKey
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next)
{
$apiKey = $request->header('X-API-Key');
if (!$apiKey || $apiKey !== config('app.api_key')) {
return ResponseJson::unauthorizeResponse('Invalid API Key', []);
}
return $next($request);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\Comment;
use App\Helpers\ResponseJson;
class CheckCommentAuthorOrAdmin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$user = $request->user();
$commentId = $request->route('commentId');
$comment = Comment::find($commentId);
if (!$comment) {
return ResponseJson::pageNotFoundResponse('Comment not found', []);
}
if ($comment->user_id !== $user->id && $user->role !== 'admin') {
return ResponseJson::forbidenResponse('Unauthorized to edit or delete this comment.', []);
}
return $next($request);
}
}
@@ -0,0 +1,30 @@
<?php
// app/Http/Middleware/CheckDiseaseAccess.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Helpers\ResponseJson;
class CheckDiseaseAccess
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if (!$user) {
return ResponseJson::unauthorizeResponse('Unauthorized Access', []);
}
if (in_array($user->role, ['operator', 'admin'])) {
return $next($request);
}
if (($user->role === 'peneliti') && $user->approval_status === 'approved') {
return $next($request);
}
return ResponseJson::forbidenResponse('User is not approved', []);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, ...$roles): Response
{
if (!in_array(Auth::user()->role, $roles)) {
return response()->json([
'success' => false,
'message' => 'Forbidden',
'data' => []
], 403);
}
return $next($request);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class EnsureAcceptJson
{
public function handle(Request $request, Closure $next)
{
// Allow multipart/form-data for POST and PUT
if (($request->isMethod('post') || $request->isMethod('put')) &&
$request->hasHeader('Content-Type', 'multipart/form-data')) {
return $next($request);
}
// Allow file downloads to pass through
if ($request->route() && strpos($request->route()->getName(), 'download') !== false) {
return $next($request);
}
// Set Accept header to JSON for other requests
$request->headers->set('Accept', 'application/json');
$response = $next($request);
// Only transform non-file responses
if (!$response instanceof BinaryFileResponse &&
!$response instanceof StreamedResponse) {
if (!$response instanceof JsonResponse) {
if ($response instanceof Response) {
$content = $response->getContent();
$decoded = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE) {
$data = $decoded;
} else {
$data = ['message' => $content];
}
$response = response()->json(
$data,
$response->status()
);
}
}
$response->header('Content-Type', 'application/json');
}
return $response;
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Middleware;
use App\Helpers\ResponseJson;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Http\JsonResponse;
class EnsureOperatorManagesDisease
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $diseaseParam
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$userId = auth()->id();
$user = User::findOrFail($userId);
// echo($user);
// Ensure user is authenticated
if (!$user) {
return ResponseJson::unauthorizeResponse('Unauthorized', []);
}
// Allow admins to bypass the check
if ($user->role === 'admin') {
return $next($request);
}
// Check if the user is an operator and manages the specified disease
$diseaseId = $request->route('diseaseId');
// echo($diseaseId);
$managesDisease = $user->managed_diseases['disease_id'];
// echo($managesDisease);
// echo($managesDisease === $diseaseId);
if ($user->role === 'operator') {
if ($managesDisease != $diseaseId) {
return ResponseJson::forbidenResponse('Access denied: Unauthorized to manage this disease', []);
}
return $next($request);
}
return ResponseJson::forbidenResponse('Access denied', []);
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsApproved
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && $request->user()->approval_status !== 'approved') {
return response()->json(['message' => 'Your account is not approved yet.'], 403);
}
return $next($request);
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateCommentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'diseaseId' => 'required|exists:diseases,id',
'content' => 'required|string|max:1000',
'parent_id' => 'nullable|exists:comments,id',
];
}
}
@@ -0,0 +1,218 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
use App\Models\Disease;
use Illuminate\Support\Facades\Log;
class CreateDiseaseRecordRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$mimeTypeMap = [
'audio' => [
'aac', 'midi', 'mp3', 'ogg', 'wav', 'webm', 'flac', 'aiff', 'amr', 'opus'
],
'video' => [
'mp4', 'avi', 'mkv', 'webm', 'ogg', '3gp', 'flv', 'mov',
'wmv', 'mpg', 'mpeg', 'm4v', 'h264', 'hevc'
],
'image' => [
'jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'heif', 'heic',
'ico', 'jp2', 'j2k', 'avif'
],
'text-document' => [
'pdf', 'doc', 'docx', 'txt'
],
'compressed-document' => [
'zip', '7z', 'tar', 'gz', 'rar', 'bz2', 'xz'
],
'spreadsheet' => [
'xls', 'xlsx', 'csv', 'ods'
],
];
$rules = [
'diseaseId' => 'required|integer|exists:diseases,id',
];
if ($this->filled('diseaseId')) {
try {
$disease = Disease::findOrFail($this->route('diseaseId'));
if ($disease && isset($disease->schema['columns'])){
foreach ($disease->schema['columns'] as $column) {
$columnName = $column['name'];
$columnRules = [];
// Base validation based on type
switch ($column['type']) {
case 'string':
case 'text':
$columnRules[] = 'required|string';
break;
case 'integer':
$columnRules[] = 'required|integer';
break;
case 'decimal':
case 'float':
$columnRules[] = 'required|numeric';
break;
case 'datetime':
case 'date':
$columnRules[] = 'required|date';
break;
case 'time':
$columnRules[] = 'required|date_format:H:i';
break;
case 'file':
$columnRules[] = 'required';
if (!empty($column['multiple'])) {
// Multiple file upload handling
$rules[$columnName] = 'required|array|min:1';
$fileRules = ['file', 'max:20480'];
if (!empty($column['format'])) {
$category = trim($column['format'], '.');
if (array_key_exists($category, $mimeTypeMap)) {
$formats = $mimeTypeMap[$category];
$fileRules[] = 'mimes:' . implode(',', $formats);
} else {
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
}
}
$rules[$columnName . '.*'] = implode('|', $fileRules);
// print_r($rules);
} else {
// Single file upload handling.
$columnRules[] = 'required|file|max:20480';
if (!empty($column['format'])) {
$category = trim($column['format'], '.');
if (array_key_exists($category, $mimeTypeMap)) {
$formats = $mimeTypeMap[$category];
$columnRules[] = 'mimes:' . implode(',', $formats);
} else {
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
}
}
$rules[$columnName] = implode('|', $columnRules);
// print_r($rules);
}
// print_r($columnRules);
break;
case 'boolean':
$columnRules[] = 'required|boolean';
break;
case 'enum':
if (!empty($column['options']) && is_array($column['options'])) {
$columnRules[] = 'required|string';
$columnRules[] = 'in:' . implode(',', $column['options']);
}
break;
case 'email':
$columnRules[] = 'required|email';
break;
case 'phone':
$columnRules[] = 'required|string';
$columnRules[] = 'regex:/^([0-9\s\-\+\(\)]*)$/';
break;
}
if (!empty($columnRules)) {
$rules[$columnName] = implode('|', $columnRules);
}
}
}
} catch (\Throwable $e) {
Log::error('Error loading disease schema: ' . $e->getMessage());
}
}
return $rules;
}
public function validated($key = null, $default = null): array
{
$validated = parent::validated();
return [
'diseaseId' => $validated['diseaseId'],
'data' => collect($validated)
->except('diseaseId')
->toArray()
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
// public function messages(): array
// {
// return [
// 'diseaseId.required' => 'ID penyakit harus diisi.',
// 'diseaseId.integer' => 'ID penyakit harus berupa angka.',
// 'diseaseId.exists' => 'ID penyakit tidak ditemukan dalam database.',
// 'data.*.required' => ':attribute harus diisi.',
// 'data.*.string' => ':attribute harus berupa teks.',
// 'data.*.integer' => ':attribute harus berupa angka bulat.',
// 'data.*.numeric' => ':attribute harus berupa angka.',
// 'data.*.date' => ':attribute harus berupa tanggal yang valid.',
// 'data.*.date_format' => ':attribute harus dalam format waktu yang valid (HH:MM:SS).',
// 'data.*.boolean' => ':attribute harus berupa benar atau salah.',
// 'data.*.in' => ':attribute harus salah satu dari pilihan yang tersedia: :values.',
// 'data.*.email' => ':attribute harus berupa alamat email yang valid.',
// 'data.*.regex' => ':attribute harus berupa nomor telepon yang valid.',
// // 'data.*.file' => ':attribute harus berupa file.',
// // 'data.*.mimes' => ':attribute harus bertipe: :values.',
// // 'data.*.array' => ':attribute harus dalam format array jika berisi banyak file.',
// // Uncommented additional validation for required/nullable
// // 'data.*.nullable' => ':attribute boleh dikosongkan.',
// // Rules for minimum and maximum values
// // 'data.*.min' => ':attribute harus minimal :min.',
// // 'data.*.min_digits' => ':attribute harus minimal :min digit.',
// // 'data.*.max' => ':attribute maksimal :max.',
// // 'data.*.max_digits' => ':attribute maksimal :max digit.',
// ];
// }
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class CreateDiseaseRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'deskripsi' => 'nullable|string|max:65535',
'visibilitas' => 'required|in:publik,privat',
'schema' => 'required|array',
'schema.columns' => 'required|array',
'schema.columns.*.name' => [
'required',
'string',
'regex:/^[a-zA-Z0-9_]+$/',
'distinct'
],
'schema.columns.*.type' => 'required|string|in:string,text,integer,decimal,float,date,datetime,time,file,boolean,email,phone',
//'schema.columns.*.type' => 'required|string|in:string,integer,enum,decimal,date,file,time,datetime,boolean,array,float,text,email,phone,json,range',
'schema.columns.*.options' => 'required_if:schema.columns.*.type,enum|array',
'schema.columns.*.format' => 'required_if:schema.columns.*.type,file|string',
'schema.columns.*.multiple' => 'boolean|nullable',
'schema.columns.*.is_visible' => 'boolean|nullable',
//'schema.columns.*.required' => 'boolean|nullable',
//'schema.columns.*.min' => 'numeric|nullable',
//'schema.columns.*.max' => 'numeric|nullable',
//'schema.columns.*.step' => 'numeric|nullable',
//'schema.columns.*.default' => 'nullable',
//'schema.columns.*.description' => 'string|nullable',
//'schema.columns.*.unit' => 'string|nullable',
'cover_page' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
];
}
public function messages(): array
{
return [
'name.required' => 'Nama penyakit harus diisi.',
'name.max' => 'Nama penyakit maksimal 255 karakter.',
'deskripsi.max' => 'Deskripsi penyakit maksimal 65535 karakter.',
'schema.required' => 'Schema harus diisi.',
'schema.array' => 'Schema harus dalam format yang valid.',
'schema.columns.required' => 'Columns dalam schema harus diisi.',
'schema.columns.array' => 'Columns harus dalam format array.',
'schema.columns.*.name.required' => 'Nama kolom harus diisi.',
'schema.columns.*.name.distinct' => 'Nama kolom tidak boleh duplikat',
'schema.columns.*.name.regex' => 'Nama kolom hanya boleh berisi huruf, angka, dan underscore tanpa spasi.',
'schema.columns.*.type.required' => 'Tipe kolom harus diisi.',
'schema.columns.*.type.in' => 'Tipe kolom tidak valid.',
'schema.columns.*.options.required_if' => 'Options harus diisi untuk tipe enum.',
'schema.columns.*.format.required_if' => 'Format harus diisi untuk tipe file.',
'cover_page.image' => 'Cover page harus berupa file gambar.',
'cover_page.mimes' => 'Cover page harus berupa file bertipe: jpeg, png, jpg, gif, atau svg.',
'cover_page.max' => 'Ukuran cover page maksimal 2MB.',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
$pesan = "Field Error";
foreach ($errors as $field => $message) {
if ($message[0] === 'Nama kolom tidak boleh duplikat') {
$pesan .= " - Nama kolom tidak boleh duplikat";
}
throw new HttpResponseException(ResponseJson::failedResponse($pesan, [$field => $message[0]]));
}
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class CreateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:100',
'email' => 'required|email|unique:users,email|max:100',
'password' => 'required|string|min:8|max:64|confirmed',
// Front end add 'password_confirmation' field
'role' => 'required|in:admin,operator,peneliti',
'institution' => 'nullable|string|max:255',
'gender' => 'nullable|in:male,female,prefer not to say',
'phone_number' => 'nullable|string|max:50',
'tujuan_permohonan' => 'nullable|string|max:65535',
'disease_id' => 'required_if:role,operator|integer|exists:diseases,id'
];
}
public function messages()
{
return [
'name.required' => 'Nama harus diisi.',
'name.max' => 'Nama maksimal 100 karakter',
'email.required' => 'Email harus diisi.',
'email.email' => 'Email tidak valid.',
'email.max' => 'Email maksimal 100 karakter',
'email.unique' => 'Email tersebut sudah terdaftar',
'password.required' => 'Password harus diisi.',
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
'password.min' => 'Password minimal 8 karakter',
'password.max' => 'Password maksimal 64 karakter',
'role.required' => 'Role harus diisi',
'role.in' => 'Role tidak sesuai',
'institution.max' => 'Institusi maksimal 255 karakter.',
'gender.in' => 'Gender tidak sesuai.',
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
'tujuan_permohonan.max' => 'Tujuan permohonan maksimal 65535 karakter',
'disease_id.required_if' => 'Disease IDs harus diisi jika role adalah operator.',
'disease_id.integer' => 'Disease IDs harus berupa integer.',
'disease_id.*.exists' => 'Setiap Disease ID yang dipilih harus valid dan ada dalam database.',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
foreach ($errors as $field => $message) {
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DeleteCommentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
'commentId' => $this->route('commentId')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'diseaseId' => 'required|exists:diseases,id',
'commentId' => 'required|exists:comments,id',
];
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class DeleteDiseaseRecordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
'recordId' => $this->route('recordId'),
]);
}
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
'recordId' => [
'required',
'integer',
'exists:disease_records,id',
function ($attribute, $value, $fail) {
$exists = \App\Models\DiseaseRecord::where('id', $value)
->where('disease_id', $this->route('diseaseId'))
->exists();
if (!$exists) {
$fail('The selected record does not belong to the specified disease.');
}
},
],
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class DiseaseIdRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
]);
}
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class EditCommentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
'commentId' => $this->route('commentId')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
'commentId' => 'required|integer|exists:comments,id',
'content' => 'required|string|max:1000',
];
}
}
@@ -0,0 +1,245 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
use App\Models\Disease;
use Illuminate\Support\Facades\Log;
class EditDiseaseRecordRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
'recordId' => $this->route('recordId')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$mimeTypeMap = [
'audio' => [
'aac', 'midi', 'mp3', 'ogg', 'wav', 'webm', 'flac', 'aiff', 'amr', 'opus'
],
'video' => [
'mp4', 'avi', 'mkv', 'webm', 'ogg', '3gp', 'flv', 'mov',
'wmv', 'mpg', 'mpeg', 'm4v', 'h264', 'hevc'
],
'image' => [
'jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'heif', 'heic',
'ico', 'jp2', 'j2k', 'avif'
],
'text-document' => [
'pdf', 'doc', 'docx', 'txt'
],
'compressed-document' => [
'zip', '7z', 'tar', 'gz', 'rar', 'bz2', 'xz'
],
'spreadsheet' => [
'xls', 'xlsx', 'csv', 'ods'
],
];
$rules = [
'diseaseId' => 'required|integer|exists:diseases,id',
'recordId' => [
'required',
'integer',
'exists:disease_records,id',
function ($attribute, $value, $fail) {
$exists = \App\Models\DiseaseRecord::where('id', $value)
->where('disease_id', $this->route('diseaseId'))
->exists();
if (!$exists) {
$fail('The selected record does not belong to the specified disease.');
}
},
],
];
if ($this->filled('diseaseId')) {
try {
$disease = Disease::findOrFail($this->route('diseaseId'));
if ($disease && isset($disease->schema['columns'])) {
foreach ($disease->schema['columns'] as $column) {
$columnName = $column['name'];
$columnRules = [];
switch ($column['type']) {
case 'string':
case 'text':
$columnRules[] = 'sometimes|required|string';
break;
case 'integer':
$columnRules[] = 'sometimes|required|integer';
break;
case 'decimal':
case 'float':
$columnRules[] = 'sometimes|required|numeric';
break;
case 'datetime':
case 'date':
$columnRules[] = 'sometimes|required|date';
break;
case 'time':
$columnRules[] = 'sometimes|required|date_format:H:i';
break;
case 'file':
$columnRules[] = 'sometimes|required'; // Ensures a file is uploaded.
// Check if a 'format' is defined in the schema for the column.
if (!empty($column['multiple'])) {
// Ensure the field is an array of files.
$rules[$columnName] = 'sometimes|required|array|min:1';
// Apply validation to each file in the array.
$fileRules = ['file', 'max:20480']; // Start with basic file validation.
// Add MIME type validation if a format is specified.
if (!empty($column['format'])) {
$category = trim($column['format'], '.'); // Strip the dot from the format.
if (array_key_exists($category, $mimeTypeMap)) {
$formats = $mimeTypeMap[$category];
$fileRules[] = 'mimes:' . implode(',', $formats);
} else {
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
}
}
// Apply the rules to each file in the array.
$rules[$columnName . '.*'] = implode('|', $fileRules);
// print_r($rules);
} else {
// Single file upload handling.
$columnRules[] = 'sometimes|required|file|max:20480';
// Add MIME type validation if a format is specified.
if (!empty($column['format'])) {
$category = trim($column['format'], '.'); // Strip the dot from the format.
if (array_key_exists($category, $mimeTypeMap)) {
$formats = $mimeTypeMap[$category];
$columnRules[] = 'mimes:' . implode(',', $formats);
} else {
Log::warning("Unsupported format '{$column['format']}' for column '{$columnName}'");
}
}
// Apply the accumulated rules for a single file.
$rules[$columnName] = implode('|', $columnRules);
// print_r($rules);
}
// print_r($columnRules);
break;
case 'boolean':
$columnRules[] = 'sometimes|required|boolean';
break;
case 'enum':
if (!empty($column['options']) && is_array($column['options'])) {
$columnRules[] = 'sometimes|required|string';
$columnRules[] = 'in:' . implode(',', $column['options']);
}
break;
case 'email':
$columnRules[] = 'sometimes|required|email';
break;
case 'phone':
$columnRules[] = 'sometimes|required|string';
$columnRules[] = 'regex:/^([0-9\s\-\+\(\)]*)$/';
break;
}
// // Add required/nullable validation
// if (!empty($column['required'])) {
// $columnRules[] = 'required';
// } else {
// $columnRules[] = 'nullable';
// }
// // Add min/max validation if specified
// if (!empty($column['min'])) {
// switch ($column['type']) {
// case 'string':
// case 'text':
// $columnRules[] = 'min:' . $column['min'];
// break;
// case 'integer':
// case 'decimal':
// case 'float':
// $columnRules[] = 'min_digits:' . $column['min'];
// break;
// case 'file':
// $columnRules[] = 'min:' . $column['min']; // min in kilobytes
// break;
// }
// }
// if (!empty($column['max'])) {
// switch ($column['type']) {
// case 'string':
// case 'text':
// $columnRules[] = 'max:' . $column['max'];
// break;
// case 'integer':
// case 'decimal':
// case 'float':
// $columnRules[] = 'max_digits:' . $column['max'];
// break;
// case 'file':
// $columnRules[] = 'max:' . $column['max']; // max in kilobytes
// break;
// }
// }
if (!empty($columnRules)) {
$rules[$columnName] = implode('|', $columnRules);
}
}
}
} catch (\Throwable $e) {
Log::error('Error loading disease schema: ' . $e->getMessage());
}
}
return $rules;
}
public function validated($key = null, $default = null): array
{
$validated = parent::validated();
return [
'diseaseId' => $validated['diseaseId'],
'data' => collect($validated)
->except('diseaseId', 'recordId')
->toArray()
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class EditDiseaseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
'name' => 'sometimes|required|string|max:255',
'deskripsi' => 'sometimes|nullable|string|max:65535',
'visibilitas' => 'sometimes|required|in:publik,privat',
//'schema' => 'required|json',
'cover_page' => 'sometimes|nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
];
}
public function messages()
{
return [
'name.required' => 'Nama penyakit harus diisi.',
'name.max' => 'Nama penyakit maksimal 255 karakter.',
'deskripsi.max' => 'Deskripsi penyakit maksimal 65535 karakter.',
//'schema.required' => 'Schema harus diisi.',
//'schema.json' => 'Schema harus dalam format JSON yang valid.',
'cover_page.image' => 'Cover page harus berupa file gambar.',
'cover_page.mimes' => 'Cover page harus berupa file bertipe: jpeg, png, jpg, gif, atau svg.',
'cover_page.max' => 'Ukuran cover page maksimal 2MB.',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
foreach ($errors as $field => $message) {
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
use App\Models\User;
class EditUserRequest extends FormRequest
{
public function rules(): array
{
$user = $this->route('userId');
$userRecord = User::find($user);
$userRole = $userRecord ? $userRecord->role : null;
return [
'name' => 'sometimes|required|string|max:100',
'email' => 'sometimes|nullable|email|unique:users,email,' . $user . '|max:100',
'password' => 'sometimes|nullable|string|min:8|max:64|confirmed',
'role' => 'sometimes|required|in:admin,operator,peneliti',
'institution' => 'nullable|string|max:255',
'gender' => 'nullable|in:male,female,prefer not to say',
'phone_number' => 'nullable|string|max:50',
'approval_status' => 'sometimes|in:approved,pending,rejected',
'disease_id' => 'sometimes|integer|exists:diseases,id|required_if:role,operator'
];
}
public function messages()
{
return [
'name.required' => 'Nama harus diisi.',
'name.max' => 'Nama maksimal 100 karakter',
'email.email' => 'Email tidak valid.',
'email.max' => 'Email maksimal 100 karakter',
'email.unique' => 'Email tersebut sudah terdaftar',
'password.min' => 'Password minimal 8 karakter',
'password.max' => 'Password maksimal 64 karakter',
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
'role.required' => 'Role harus diisi',
'role.in' => 'Role tidak sesuai',
'institution.max' => 'Institusi maksimal 255 karakter.',
'gender.in' => 'Gender tidak sesuai.',
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
'approval_status.required' => 'Status Approval harus diisi.',
'approval_status.in' => 'Status Approval tidak sesuai.',
'disease_id.required_if' => 'Disease IDs harus diisi jika role adalah operator.',
'disease_id.integer' => 'Disease IDs harus berupa integer.',
'disease_id.*.exists' => 'Setiap Disease ID yang dipilih harus valid dan ada dalam database.',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
foreach ($errors as $field => $message) {
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
}
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class IndexDiseaseRecordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
]);
}
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class LoginUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' => 'required|email|max:100',
'password' => 'required|min:8|max:64',
];
}
public function messages()
{
return [
'email.required' => 'Email harus diisi',
'email.email' => 'Email tidak valid',
'email.max' => 'Email Maksimal 100 Karakter',
'password.required' => 'Password Harus diisi',
'password.min' => 'Password Minimal 8 Karakter',
'password.max' => 'Password Maksimal 64 Karakter',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
foreach ($errors as $field => $message) {
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class RegisterUserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:100',
'email' => 'required|email|unique:users,email|max:100',
'password' => 'required|string|min:8|max:64|confirmed',
//Front end include: 'password_confirmation' field
'institution' => 'required|string|max:255',
'gender' => 'required|in:male,female,prefer not to say',
'phone_number' => 'required|string|max:50',
'tujuan_permohonan' => 'required|string|max:65535',
];
}
public function messages()
{
return [
'name.required' => 'Nama harus diisi.',
'name.max' => 'Nama maksimal 100 karakter',
'email.required' => 'Email harus diisi.',
'email.email' => 'Email tidak valid.',
'email.max' => 'Email maksimal 100 karakter',
'email.unique' => 'Email tersebut sudah terdaftar, silahkan login.',
'password.required' => 'Password harus diisi.',
'password.confirmed' => 'Password konfirmasi tidak sesuai.',
'password.min' => 'Password minimal 8 karakter',
'password.max' => 'Password maksimal 64 karakter',
'institution.max' => 'Institusi maksimal 255 karakter.',
'gender.in' => 'Gender tidak sesuai.',
'phone_number.max' => 'Nomor telepon maksimal 50 karakter.',
'tujuan_permohonan.max' => 'Tujuan permohonan maksimal 65535 karakter',
];
}
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->toArray();
foreach ($errors as $field => $message) {
throw new HttpResponseException(ResponseJson::failedResponse("field error", [$field => $message[0]]));
}
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class ShowDiseaseRecordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'diseaseId' => $this->route('diseaseId'),
'recordId' => $this->route('recordId')
]);
}
public function rules(): array
{
return [
'diseaseId' => 'required|integer|exists:diseases,id',
'recordId' => [
'required',
'integer',
'exists:disease_records,id',
function ($attribute, $value, $fail) {
$exists = \App\Models\DiseaseRecord::where('id', $value)
->where('disease_id', $this->route('diseaseId'))
->exists();
if (!$exists) {
$fail('The selected record does not belong to the specified disease.');
}
},
],
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Helpers\ResponseJson;
class UserIdRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'userId' => $this->route('userId'),
]);
}
public function rules(): array
{
return [
'userId' => 'required|integer|exists:users,id',
];
}
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
ResponseJson::failedResponse('Validation error', $validator->errors()->toArray())
);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserResource extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}