diff --git a/server-connection/geonet-console-deploy.tar.gz b/server-connection/geonet-console-deploy.tar.gz index 8797084..a5edbf4 100644 Binary files a/server-connection/geonet-console-deploy.tar.gz and b/server-connection/geonet-console-deploy.tar.gz differ diff --git a/server-connection/geonet-console/app/Http/Controllers/Api/V1/MeController.php b/server-connection/geonet-console/app/Http/Controllers/Api/V1/MeController.php index fbed28c..4e9491f 100644 --- a/server-connection/geonet-console/app/Http/Controllers/Api/V1/MeController.php +++ b/server-connection/geonet-console/app/Http/Controllers/Api/V1/MeController.php @@ -11,6 +11,8 @@ use App\Services\MailDeliveryService; use App\Support\ApiActor; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use InvalidArgumentException; use RuntimeException; use Throwable; @@ -44,7 +46,10 @@ class MeController extends Controller $data['domain'] = $profile['domain'] ?? ''; $data['phone'] = $meta['phone'] ?? ''; $data['other_email'] = $meta['other_email'] ?? ''; - $data['avatar'] = $meta['avatar'] ?? null; + $avatarPath = $meta['avatar'] ?? null; + $data['avatar'] = $avatarPath + ? Storage::disk('public')->url($avatarPath) + : null; } return $this->ok($data); @@ -126,24 +131,51 @@ class MeController extends Controller } $data = $request->validate([ - 'avatar' => ['nullable', 'string', 'max:262144'], + 'avatar' => ['nullable', 'string', 'max:524288'], ]); try { - $avatarValue = isset($data['avatar']) && $data['avatar'] !== '' - ? $data['avatar'] - : null; + $meta = $this->metadata->getUserMeta($actor->identifier); + $oldPath = $meta['avatar'] ?? null; + $avatarInput = isset($data['avatar']) && $data['avatar'] !== '' ? $data['avatar'] : null; + + if ($avatarInput === null) { + // Hapus foto + if ($oldPath) { + Storage::disk('public')->delete($oldPath); + } + $this->metadata->saveAvatar($actor->identifier, null); + return $this->ok(['message' => 'Foto profil dihapus.', 'avatar_url' => null]); + } - if ($avatarValue !== null && ! preg_match('/^data:image\/(jpeg|png|webp|gif);base64,/', $avatarValue)) { + // Validasi format base64 data-URI + if (! preg_match('/^data:image\/(jpeg|png|webp);base64,(.+)$/s', $avatarInput, $m)) { return $this->fail('Format avatar tidak valid. Harus base64 image (jpeg/png/webp).', 'invalid_format', 422); } - $this->metadata->saveAvatar($actor->identifier, $avatarValue); - } catch (Throwable) { - return $this->fail('Gagal menyimpan avatar.', 'server_error', 500); + $imageData = base64_decode($m[2], strict: true); + if ($imageData === false || strlen($imageData) > 300 * 1024) { + return $this->fail('Ukuran gambar terlalu besar (maks 300KB).', 'too_large', 422); + } + + $ext = $m[1] === 'jpeg' ? 'jpg' : $m[1]; + $filename = 'avatars/' . Str::slug($actor->identifier) . '-' . Str::random(8) . '.' . $ext; + + Storage::disk('public')->put($filename, $imageData); + + // Hapus file lama jika ada + if ($oldPath && $oldPath !== $filename) { + Storage::disk('public')->delete($oldPath); + } + + $this->metadata->saveAvatar($actor->identifier, $filename); + + $avatarUrl = Storage::disk('public')->url($filename); + } catch (Throwable $e) { + return $this->fail('Gagal menyimpan avatar: ' . $e->getMessage(), 'server_error', 500); } - return $this->ok(['message' => 'Avatar berhasil diperbarui.', 'avatar' => $avatarValue]); + return $this->ok(['message' => 'Foto profil berhasil diperbarui.', 'avatar_url' => $avatarUrl]); } public function testEmail(Request $request): JsonResponse diff --git a/server-connection/geonet-console/compose.yaml b/server-connection/geonet-console/compose.yaml index 38a5bea..dcba212 100644 --- a/server-connection/geonet-console/compose.yaml +++ b/server-connection/geonet-console/compose.yaml @@ -45,7 +45,7 @@ services: AUDIT_DB_USERNAME: ${AUDIT_DB_USERNAME:-audit} AUDIT_DB_PASSWORD: ${AUDIT_DB_PASSWORD:-audit_secret_change_me} volumes: - - app_storage:/var/www/html/storage + - /mnt/nas-gisportal/storage:/var/www/html/storage - ./database:/var/www/html/database healthcheck: test: ["CMD-SHELL", "curl -sf http://127.0.0.1/up >/dev/null || exit 1"] @@ -58,6 +58,3 @@ volumes: audit_pg_data: external: true name: databaselist_audit_pg_data - app_storage: - external: true - name: databaselist_app_storage diff --git a/server-connection/geonet-console/docker/entrypoint.sh b/server-connection/geonet-console/docker/entrypoint.sh index 87bb0bf..07d60b1 100644 --- a/server-connection/geonet-console/docker/entrypoint.sh +++ b/server-connection/geonet-console/docker/entrypoint.sh @@ -11,10 +11,15 @@ php artisan config:clear --quiet 2>/dev/null || true php artisan route:clear --quiet 2>/dev/null || true php artisan view:clear --quiet 2>/dev/null || true -mkdir -p storage/framework/{sessions,views,cache} storage/logs bootstrap/cache database +mkdir -p storage/framework/{sessions,views,cache} storage/logs storage/app/public/avatars bootstrap/cache database chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true chmod -R 775 storage bootstrap/cache 2>/dev/null || true +# Buat symlink public/storage -> storage/app/public agar file bisa diakses via URL +if [ ! -L public/storage ]; then + ln -sf /var/www/html/storage/app/public public/storage 2>/dev/null || true +fi + if [ ! -f storage/oauth-private.key ]; then php artisan passport:keys --force --quiet 2>/dev/null || true fi