Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions app/Http/Controllers/Api/TokenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,43 @@
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Tymon\JWTAuth\Facades\JWTAuth;

class TokenController extends Controller
{
/**
* Display a listing of the resource.
* Generate token sinkronisasi dengan masa berlaku 1 tahun.
*
* @return \Illuminate\Http\Response
* Token ini digunakan untuk keperluan sinkronisasi data antar sistem,
* bukan untuk sesi autentikasi umum.
*
* @return Response
*/
public function index()
{
// Set the token's expiration time, 10 tahun
Config::set('jwt.ttl', 10 * 365 * 24 * 60);
$user = Auth::user();
$token = JWTAuth::fromUser($user);

// Return the token in a response
return response()->json(['token' => $token]);
// Gunakan customClaims untuk set expiry 1 tahun secara terisolasi,
// tanpa mengubah konfigurasi JWT global via Config::set().
// Ini memastikan TTL default untuk token lain tidak terpengaruh.
$expiresAt = now()->addYear()->timestamp;

$token = JWTAuth::customClaims(['exp' => $expiresAt])
->fromUser($user);

Log::info('Token sinkronisasi digenerate', [
'user_id' => $user->id,
'expires_at' => now()->addYear()->toDateTimeString(),
'ip' => request()->ip(),
]);

return response()->json([
'token' => $token,
'expires_at' => now()->addYear()->toDateTimeString(),
'token_type' => 'Bearer',
]);
}
}
75 changes: 46 additions & 29 deletions app/Services/FileUploadService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,36 @@ public function uploadSecure(
): string {
// 1. Validate MIME type
$this->validateMimeType($file, $allowedMimes);

// 2. Validate file size (KB)
$this->validateFileSize($file, $maxSize);

// 3. Sanitize directory to prevent path traversal
$directory = $this->sanitizeDirectoryPath($directory);

// 4. Generate safe filename
$safeFileName = $this->generateSafeFileName($file);

// 5. Store file securely
$path = $file->storeAs($directory, $safeFileName, 'public');

// Sebelum : $path = $file->storeAs($directory, $safeFileName, 'public');
// Menggunakan manual stream alih-alih $file->storeAs() karena pada
// environment tertentu (seperti Laragon/Windows), $file->getRealPath()
// me-return false untuk file di C:\Windows\Temp yang menyebabkan ValueError.
$stream = fopen($file->getPathname(), 'r');
if ($stream === false) {
throw new \RuntimeException('Failed to open file: ' . $file->getPathname());
}

$path = trim($directory . '/' . $safeFileName, '/');

try {
Storage::disk('public')->put($path, $stream);
} finally {
if (is_resource($stream)) {
fclose($stream);
}
}

return $path;
}

Expand All @@ -45,13 +62,13 @@ public function uploadMultiple(
int $maxSize = 2048
): array {
$uploadedPaths = [];

foreach ($files as $file) {
if ($file instanceof UploadedFile) {
$uploadedPaths[] = $this->uploadSecure($file, $directory, $allowedMimes, $maxSize);
}
}

return $uploadedPaths;
}

Expand All @@ -63,7 +80,7 @@ public function delete(string $path): bool
if (Storage::disk('public')->exists($path)) {
return Storage::disk('public')->delete($path);
}

return false;
}

Expand All @@ -75,12 +92,12 @@ protected function validateMimeType(UploadedFile $file, array $allowedMimes): vo
if (empty($allowedMimes)) {
return;
}

$fileMime = $file->getMimeType();
if (!in_array($fileMime, $allowedMimes)) {

if (! in_array($fileMime, $allowedMimes)) {
throw new \InvalidArgumentException(
"File type not allowed. Allowed types: " . implode(', ', $allowedMimes)
'File type not allowed. Allowed types: '.implode(', ', $allowedMimes)
);
}
}
Expand All @@ -91,7 +108,7 @@ protected function validateMimeType(UploadedFile $file, array $allowedMimes): vo
protected function validateFileSize(UploadedFile $file, int $maxSize): void
{
$fileSizeInKB = $file->getSize() / 1024;

if ($fileSizeInKB > $maxSize) {
throw new \InvalidArgumentException(
"File size exceeds maximum allowed size of {$maxSize}KB"
Expand All @@ -106,14 +123,14 @@ protected function sanitizeDirectoryPath(string $directory): string
{
// Remove any path traversal attempts
$directory = str_replace(['../', '..\\', './', '.\\'], '', $directory);

// Additional sanitization to prevent directory traversal
$directory = preg_replace('#/\.{2}/#', '/', $directory); // Prevent /../
$directory = preg_replace('#\\\\\.{2}\\\\#', '\\', $directory); // Prevent \..\

return $directory;
}

/**
* Sanitize file extension to prevent malicious extensions
*/
Expand All @@ -138,7 +155,7 @@ protected function sanitizeExtension(string $extension): string

return $sanitized !== '' ? $sanitized : 'tmp';
}

/**
* Generate safe filename
*/
Expand All @@ -148,17 +165,17 @@ protected function generateSafeFileName(UploadedFile $file): string
$originalName = $file->getClientOriginalName();
// Reject if original name contains traversal or contains directory parts
if (str_contains($originalName, '..') || basename($originalName) !== $originalName || preg_match('/[\\\\\/]/', $originalName)) {
throw new \InvalidArgumentException("File name contains path traversal attempts");
throw new \InvalidArgumentException('File name contains path traversal attempts');
}

// Generate unique hash-based filename
$extension = $file->getClientOriginalExtension();
$timestamp = time();
$random = Str::random(16);

// Make sure extension is safe too
$extension = $this->sanitizeExtension($extension);

return "{$timestamp}_{$random}.{$extension}";
}

Expand All @@ -167,15 +184,15 @@ protected function generateSafeFileName(UploadedFile $file): string
*/
public static function getAllowedMimes(string $category): array
{
return match($category) {
return match ($category) {
'image' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'document' => ['application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'spreadsheet' => ['application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'],
'document' => ['application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'spreadsheet' => ['application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'],
'archive' => ['application/zip', 'application/x-zip-compressed'],
default => [],
};
}
}
}
1 change: 1 addition & 0 deletions catatan_rilis.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Terimakasih [isi disini] yang telah berkontribusi langsung mengembangkan aplikas
4. [#1600](https://github.com/OpenSID/OpenDK/issues/1600) Perbaikan temuan pasca test manual
5. [#1601](https://github.com/OpenSID/OpenDK/issues/1601) Perbaikan Filter Kode Kecamatan pada Semua Halaman DataTable Gabungan
6. [#1605](https://github.com/OpenSID/OpenDK/issues/1605) Perbaikan Data dokumen yang ditambahkan/dihapus tidak tampil sesuai di halaman web
7. [#1608](https://github.com/OpenSID/OpenDK/issues/1608) Perbaikan generate token sinkronisasi opensid

#### TEKNIS

Expand Down