diff --git a/be/.env.development b/be/.env.development index b5c5094..74b36c6 100644 --- a/be/.env.development +++ b/be/.env.development @@ -95,4 +95,7 @@ API_ALLOWED_USER_AGENTS= ENABLE_API_KEY_AUTH=true API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae -API_LOG_KEY_USAGE=true \ No newline at end of file +API_LOG_KEY_USAGE=true + +PUBLIC_PROFILE_TOKEN_TTL_DAYS=7 +FRONTEND_URL=https://mykopkb.koppkb.com \ No newline at end of file diff --git a/be/.env.production b/be/.env.production index 8cfae54..80a9c56 100644 --- a/be/.env.production +++ b/be/.env.production @@ -102,4 +102,7 @@ API_ALLOWED_USER_AGENTS= ENABLE_API_KEY_AUTH=true API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae -API_LOG_KEY_USAGE=true \ No newline at end of file +API_LOG_KEY_USAGE=true + +PUBLIC_PROFILE_TOKEN_TTL_DAYS=7 +FRONTEND_URL=https://mykopkb.koppkb.com \ No newline at end of file diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php index 3c094b5..ac1422e 100644 --- a/be/Modules/Auth/Entities/User.php +++ b/be/Modules/Auth/Entities/User.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Str; use Laravel\Sanctum\HasApiTokens; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity; @@ -51,6 +52,7 @@ class User extends Authenticatable 'marriage_status', 'member_number', 'member_type', + 'public_profile_token', 'join_date', 'birth_date', 'birth_place', @@ -67,8 +69,55 @@ class User extends Authenticatable 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at', + 'public_profile_token', ]; + protected static function booted(): void + { + static::creating(function (User $user) { + $expiryDays = self::publicProfileTokenTtlDays(); + + if (empty($user->public_profile_token)) { + $user->public_profile_token = Str::random(48); + } + + if (empty($user->public_profile_token_expires_at)) { + $user->public_profile_token_expires_at = now()->addDays($expiryDays); + } + }); + } + + public static function publicProfileTokenTtlDays(): int + { + return (int) config('user.public_profile_token_ttl_days', 90); + } + + public function isPublicProfileTokenExpired(): bool + { + return $this->public_profile_token_expires_at !== null + && $this->public_profile_token_expires_at->isPast(); + } + + public function ensurePublicProfileToken(): string + { + $expiryDays = self::publicProfileTokenTtlDays(); + + if (blank($this->public_profile_token) || $this->isPublicProfileTokenExpired()) { + $this->forceFill([ + 'public_profile_token' => Str::random(48), + 'public_profile_token_expires_at' => now()->addDays($expiryDays), + ])->save(); + + return $this->public_profile_token; + } + + $this->forceFill([ + 'public_profile_token_expires_at' => now()->addDays($expiryDays), + ])->save(); + + return $this->public_profile_token; + } + /** * Get the attributes that should be cast. * @@ -84,6 +133,7 @@ class User extends Authenticatable 'marriage_status' => 'string', 'member_number' => 'integer', 'member_type' => 'string', + 'public_profile_token_expires_at' => 'datetime', 'join_date' => 'date', 'birth_date' => 'date', 'birth_place' => 'string', diff --git a/be/Modules/Auth/Http/Controllers/SessionController.php b/be/Modules/Auth/Http/Controllers/SessionController.php index 34a0652..cfcde33 100644 --- a/be/Modules/Auth/Http/Controllers/SessionController.php +++ b/be/Modules/Auth/Http/Controllers/SessionController.php @@ -24,6 +24,7 @@ class SessionController extends Controller ], 401); } + $user->ensurePublicProfileToken(); $user->load(['roles.permissions']); return response()->json([ diff --git a/be/Modules/Auth/Services/AuthSessionService.php b/be/Modules/Auth/Services/AuthSessionService.php index bca4178..8cd803c 100644 --- a/be/Modules/Auth/Services/AuthSessionService.php +++ b/be/Modules/Auth/Services/AuthSessionService.php @@ -12,6 +12,8 @@ class AuthSessionService { public function createAuthResponse(User $user, string $message, int $status = 200): JsonResponse { + $user->ensurePublicProfileToken(); + if (config('app.env') !== 'local') { $user->tokens()->delete(); } diff --git a/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php index 98518ef..bc89bde 100644 --- a/be/Modules/Auth/Transformers/UserResource.php +++ b/be/Modules/Auth/Transformers/UserResource.php @@ -36,6 +36,7 @@ class UserResource extends JsonResource 'marriage_status' => $this->marriage_status, 'member_number' => $this->member_number, 'member_type' => $this->member_type, + 'public_profile_token' => $this->public_profile_token, 'join_date' => $this->join_date, 'birth_date' => $this->birth_date, 'birth_place' => $this->birth_place, diff --git a/be/Modules/User/Config/config.php b/be/Modules/User/Config/config.php index 566de5a..57bdafc 100644 --- a/be/Modules/User/Config/config.php +++ b/be/Modules/User/Config/config.php @@ -2,4 +2,7 @@ return [ 'name' => 'User', + 'public_profile_token_ttl_days' => (int) env('PUBLIC_PROFILE_TOKEN_TTL_DAYS', 90), + 'frontend_url' => rtrim(env('FRONTEND_URL', env('APP_URL', 'http://localhost')), '/'), + 'digital_card_logo_path' => env('DIGITAL_CARD_LOGO_PATH', public_path('images/logo/logo-kopkb.svg')), ]; diff --git a/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php b/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php new file mode 100644 index 0000000..51eee6d --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_07_05_100000_add_public_profile_token_to_users_table.php @@ -0,0 +1,34 @@ +string('public_profile_token', 64)->nullable()->unique()->after('member_type'); + }); + + User::withTrashed() + ->whereNull('public_profile_token') + ->cursor() + ->each(function (User $user) { + $user->forceFill([ + 'public_profile_token' => Str::random(48), + ])->saveQuietly(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropUnique(['public_profile_token']); + $table->dropColumn('public_profile_token'); + }); + } +}; diff --git a/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php b/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php new file mode 100644 index 0000000..cd5ccc4 --- /dev/null +++ b/be/Modules/User/Database/Migrations/2026_07_05_100001_add_public_profile_token_expires_at_to_users_table.php @@ -0,0 +1,35 @@ +timestamp('public_profile_token_expires_at')->nullable()->after('public_profile_token'); + }); + + $expiryDays = (int) config('user.public_profile_token_ttl_days', 90); + + User::withTrashed() + ->whereNotNull('public_profile_token') + ->whereNull('public_profile_token_expires_at') + ->cursor() + ->each(function (User $user) use ($expiryDays) { + $user->forceFill([ + 'public_profile_token_expires_at' => now()->addDays($expiryDays), + ])->saveQuietly(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('public_profile_token_expires_at'); + }); + } +}; diff --git a/be/Modules/User/Http/Controllers/MemberDigitalCardController.php b/be/Modules/User/Http/Controllers/MemberDigitalCardController.php new file mode 100644 index 0000000..c0dcabc --- /dev/null +++ b/be/Modules/User/Http/Controllers/MemberDigitalCardController.php @@ -0,0 +1,61 @@ +validate([ + 'side' => 'sometimes|in:depan,belakang', + ]); + + $user = $request->user(); + $side = $validated['side'] ?? 'depan'; + + if ($user->status !== 'active') { + return response()->json([ + 'success' => false, + 'message' => 'Kad digital hanya tersedia untuk anggota aktif.', + 'code' => 'digital_card_inactive', + 'data' => null, + ], 403); + } + + try { + $png = $this->memberDigitalCardService->renderPng($user, $side); + $memberNumber = $user->member_number ?: 'anggota'; + $filename = sprintf('kad-digital-%s-%s.png', $memberNumber, $side); + + return response($png, Response::HTTP_OK, [ + 'Content-Type' => 'image/png', + 'Content-Disposition' => 'attachment; filename="'.$filename.'"', + 'Cache-Control' => 'no-store, private', + ]); + } catch (\Throwable $exception) { + Log::error('Failed to render member digital card.', [ + 'user_id' => $user->id, + 'side' => $side, + 'message' => $exception->getMessage(), + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal menjana kad digital. Sila cuba lagi.', + 'code' => 'digital_card_render_failed', + 'data' => null, + ], 500); + } + } +} diff --git a/be/Modules/User/Http/Controllers/PublicMemberProfileController.php b/be/Modules/User/Http/Controllers/PublicMemberProfileController.php new file mode 100644 index 0000000..c25357e --- /dev/null +++ b/be/Modules/User/Http/Controllers/PublicMemberProfileController.php @@ -0,0 +1,37 @@ +publicMemberProfileService->findMemberByToken($token); + + if (! $member) { + return response()->json([ + 'success' => false, + 'message' => 'Anggota tidak dijumpai atau tidak sah.', + 'code' => 'public_profile_not_found', + 'data' => null, + ], 404); + } + + $member = $this->publicMemberProfileService->loadVerifiableRelations($member); + + return response()->json([ + 'success' => true, + 'message' => 'Anggota disahkan.', + 'data' => new PublicMemberProfileResource($member), + ]); + } +} diff --git a/be/Modules/User/Providers/UserServiceProvider.php b/be/Modules/User/Providers/UserServiceProvider.php index 9c28f9e..e9f7dba 100644 --- a/be/Modules/User/Providers/UserServiceProvider.php +++ b/be/Modules/User/Providers/UserServiceProvider.php @@ -5,6 +5,8 @@ namespace Modules\User\Providers; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Cache\RateLimiting\Limit; use Modules\Auth\Entities\User; use Modules\User\Policies\UserPolicy; use Modules\User\Entities\Address; @@ -34,6 +36,7 @@ class UserServiceProvider extends ServiceProvider */ public function boot(): void { + $this->registerRateLimiters(); $this->registerCommands(); $this->registerCommandSchedules(); $this->registerTranslations(); @@ -96,6 +99,13 @@ class UserServiceProvider extends ServiceProvider Gate::policy(Heir::class, HeirPolicy::class); } + protected function registerRateLimiters(): void + { + RateLimiter::for('public-member-profile', function ($request) { + return Limit::perMinute(60)->by($request->ip()); + }); + } + /** * Register commands in the format of Command::class */ diff --git a/be/Modules/User/Resources/Views/digital-card/back.blade.php b/be/Modules/User/Resources/Views/digital-card/back.blade.php new file mode 100644 index 0000000..fd0f376 --- /dev/null +++ b/be/Modules/User/Resources/Views/digital-card/back.blade.php @@ -0,0 +1,166 @@ + + + + + + + +
+
+
+ +
+
+ @if ($logoDataUri) + + @endif +
Belakang · Kod QR
+
+ +
+
+ Kod QR profil anggota +
+ +
+

Imbas untuk sahkan profil anggota MyKOPKB.

+ +
+
No. Anggota
+
{{ $memberNumber }}
+
+
+
+ + +
+
+ + diff --git a/be/Modules/User/Resources/Views/digital-card/front.blade.php b/be/Modules/User/Resources/Views/digital-card/front.blade.php new file mode 100644 index 0000000..9352c56 --- /dev/null +++ b/be/Modules/User/Resources/Views/digital-card/front.blade.php @@ -0,0 +1,214 @@ + + + + + + + +
+
+
+ +
+ @if ($avatarDataUri) + {{ $memberName }} + @else +
{{ $avatarInitials }}
+ @endif +
+ +
+
+ @if ($logoDataUri) + + @endif +
Kad Digital
+
+ +
+
+
No. Anggota
+
{{ $memberNumber }}
+
+ +
+
Unit
+
{{ $companyName }}
+
+
+ + +
+
+ + diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php index 28c0793..36d1c39 100644 --- a/be/Modules/User/Routes/api.php +++ b/be/Modules/User/Routes/api.php @@ -8,6 +8,14 @@ use Modules\User\Http\Controllers\EmploymentController; use Modules\User\Http\Controllers\BankController; use Modules\User\Http\Controllers\BankDetailController; use Modules\User\Http\Controllers\HeirController; +use Modules\User\Http\Controllers\MemberDigitalCardController; +use Modules\User\Http\Controllers\PublicMemberProfileController; + +Route::prefix('v1/public')->group(function () { + Route::middleware('throttle:public-member-profile') + ->get('members/{token}', [PublicMemberProfileController::class, 'show']) + ->name('user.public.member-profile'); +}); Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () { Route::get('users/deleted', [UserController::class, 'deletedIndex'])->name('users.deleted.index'); @@ -28,6 +36,8 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct // User profile management routes Route::post('/profile', [UserController::class, 'updateProfile']); Route::put('/profile/password', [UserController::class, 'updatePassword']); + Route::get('/profile/digital-card', [MemberDigitalCardController::class, 'download']) + ->name('profile.digital-card.download'); // Impersonation routes Route::get('/impersonate/take/{id}', [ImpersonateController::class, 'take'])->name('impersonate'); diff --git a/be/Modules/User/Services/MemberDigitalCardService.php b/be/Modules/User/Services/MemberDigitalCardService.php new file mode 100644 index 0000000..de68afd --- /dev/null +++ b/be/Modules/User/Services/MemberDigitalCardService.php @@ -0,0 +1,141 @@ +prepareViewData($user))->render(); + + return $this->makeBrowsershot($html)->screenshot(); + } + + /** + * @return array + */ + protected function prepareViewData(User $user): array + { + $user = app(PublicMemberProfileService::class)->loadVerifiableRelations($user); + $user->ensurePublicProfileToken(); + + $currentEmployment = $user->employments->firstWhere('is_current', true) + ?? $user->employments->first(); + + $logoPath = config('user.digital_card_logo_path'); + $profileUrl = rtrim(config('user.frontend_url'), '/').'/v/'.$user->public_profile_token; + + return [ + 'memberNumber' => $this->displayCardValue($user->member_number), + 'memberName' => $user->name ?: '-', + 'memberType' => $this->displayCardValue($user->member_type), + 'companyName' => $this->displayCardValue($currentEmployment?->company_name), + 'avatarDataUri' => $this->resolveStorageDataUri($user->image_url), + 'avatarInitials' => $this->avatarInitials($user->name), + 'logoDataUri' => $this->resolveFileDataUri($logoPath), + 'profileUrl' => $profileUrl, + 'qrDataUri' => $this->generateQrDataUri($profileUrl), + ]; + } + + protected function makeBrowsershot(string $html): Browsershot + { + $browsershot = Browsershot::html($html) + ->setNodeModulePath(config('browsershot.node_module_path')) + ->timeout(config('browsershot.timeout')) + ->windowSize(self::CARD_WIDTH, self::CARD_HEIGHT) + ->deviceScaleFactor(2) + ->showBackground(); + + if ($nodeBinary = config('browsershot.node_binary')) { + $browsershot->setNodeBinary($nodeBinary); + } + + if ($npmBinary = config('browsershot.npm_binary')) { + $browsershot->setNpmBinary($npmBinary); + } + + if ($chromePath = config('browsershot.chrome_path')) { + $browsershot->setChromePath($chromePath); + } + + if (config('browsershot.no_sandbox')) { + $browsershot->noSandbox(); + } + + return $browsershot; + } + + protected function generateQrDataUri(string $content): string + { + $renderer = new ImageRenderer( + new RendererStyle(220, 1), + new SvgImageBackEnd + ); + $writer = new Writer($renderer); + $svg = $writer->writeString($content); + + return 'data:image/svg+xml;base64,'.base64_encode($svg); + } + + protected function resolveFileDataUri(?string $path): ?string + { + if (empty($path) || ! file_exists($path)) { + return null; + } + + $mime = mime_content_type($path) ?: 'image/svg+xml'; + + return 'data:'.$mime.';base64,'.base64_encode(file_get_contents($path)); + } + + protected function resolveStorageDataUri(?string $path): ?string + { + if (blank($path)) { + return null; + } + + $fullPath = Storage::disk('public')->path($path); + + return $this->resolveFileDataUri($fullPath); + } + + protected function avatarInitials(?string $name): string + { + $trimmed = trim((string) $name); + + if ($trimmed === '') { + return '--'; + } + + return strtoupper(mb_substr($trimmed, 0, 2)); + } + + protected function displayCardValue(mixed $value): string + { + if ($value === null || $value === '') { + return '-'; + } + + $trimmed = trim((string) $value); + + return $trimmed !== '' ? $trimmed : '-'; + } +} diff --git a/be/Modules/User/Services/PublicMemberProfileService.php b/be/Modules/User/Services/PublicMemberProfileService.php new file mode 100644 index 0000000..2a0c468 --- /dev/null +++ b/be/Modules/User/Services/PublicMemberProfileService.php @@ -0,0 +1,25 @@ +where('public_profile_token', $token) + ->where('status', 'active') + ->first(); + } + + public function loadVerifiableRelations(User $member): User + { + $member->load(['employments' => function ($query) { + $query->orderByDesc('is_current')->orderByDesc('start_date'); + }]); + + return $member; + } +} diff --git a/be/Modules/User/Transformers/PublicMemberProfileResource.php b/be/Modules/User/Transformers/PublicMemberProfileResource.php new file mode 100644 index 0000000..283f9d3 --- /dev/null +++ b/be/Modules/User/Transformers/PublicMemberProfileResource.php @@ -0,0 +1,26 @@ +employments->firstWhere('is_current', true) + ?? $this->employments->first(); + + return [ + 'name' => $this->name, + 'member_number' => $this->member_number, + 'member_type' => $this->member_type, + 'status' => $this->status, + 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, + 'company_name' => $currentEmployment?->company_name, + 'verified_at' => now()->toIso8601String(), + ]; + } +} diff --git a/fe/package.json b/fe/package.json index 6d389fe..b54979a 100644 --- a/fe/package.json +++ b/fe/package.json @@ -57,6 +57,7 @@ "lodash": "^4.17.21", "maplibre-gl": "^5.18.0", "pinia": "^3.0.4", + "qrcode": "^1.5.4", "sweetalert2": "^11.26.25", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.1.18", @@ -71,6 +72,7 @@ "@types/jsdom": "^27.0.0", "@types/lodash": "^4.17.21", "@types/node": "^24.10.4", + "@types/qrcode": "^1.5.6", "@vitejs/plugin-vue": "^6.0.3", "@vitest/eslint-plugin": "^1.6.4", "@vue/eslint-config-prettier": "^10.2.0", diff --git a/fe/pnpm-lock.yaml b/fe/pnpm-lock.yaml index ef1bc00..c4f6589 100644 --- a/fe/pnpm-lock.yaml +++ b/fe/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: pinia: specifier: ^3.0.4 version: 3.0.4(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 sweetalert2: specifier: ^11.26.25 version: 11.26.25 @@ -156,6 +159,9 @@ importers: '@types/node': specifier: ^24.10.4 version: 24.12.4 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@vitejs/plugin-vue': specifier: ^6.0.3 version: 6.0.7(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3)) @@ -1041,6 +1047,9 @@ packages: '@types/pako@2.0.4': resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/raf@3.4.3': resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} @@ -1588,6 +1597,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} @@ -1637,6 +1650,9 @@ packages: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1758,6 +1774,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -1784,6 +1804,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dompurify@3.4.7: resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} @@ -2055,6 +2078,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2109,6 +2136,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -2501,6 +2532,10 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2679,14 +2714,26 @@ packages: ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2769,6 +2816,10 @@ packages: typescript: optional: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -2833,6 +2884,11 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.15.2: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} @@ -2856,10 +2912,17 @@ packages: request-progress@3.0.0: resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2916,6 +2979,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3393,6 +3459,9 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3420,6 +3489,10 @@ packages: resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} engines: {node: '>=0.8'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3467,9 +3540,20 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yauzl@3.3.2: resolution: {integrity: sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA==} engines: {node: '>=12'} @@ -4215,6 +4299,10 @@ snapshots: '@types/pako@2.0.4': {} + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.12.4 + '@types/raf@3.4.3': optional: true @@ -4978,6 +5066,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001793: {} canvg@3.0.11: @@ -5033,6 +5123,12 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.1 + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + clsx@2.1.1: {} codepage@1.15.0: {} @@ -5172,6 +5268,8 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decamelize@1.2.0: {} + decimal.js@10.6.0: {} deep-is@0.1.4: {} @@ -5189,6 +5287,8 @@ snapshots: detect-libc@2.1.2: {} + dijkstrajs@1.0.3: {} + dompurify@3.4.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -5505,6 +5605,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5554,6 +5659,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -5928,6 +6035,10 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -6107,14 +6218,24 @@ snapshots: ospath@1.2.2: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} pako@2.1.0: {} @@ -6177,6 +6298,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + pngjs@5.0.0: {} + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -6225,6 +6348,12 @@ snapshots: punycode@2.3.1: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -6250,8 +6379,12 @@ snapshots: dependencies: throttleit: 1.0.1 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-protobuf-schema@2.1.0: @@ -6323,6 +6456,8 @@ snapshots: semver@7.8.1: {} + set-blocking@2.0.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6781,6 +6916,8 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.1 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6800,6 +6937,12 @@ snapshots: word@0.3.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6843,8 +6986,29 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + yallist@3.1.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yauzl@3.3.2: dependencies: pend: 1.2.0 diff --git a/fe/src/modules/auth/types/auth.types.ts b/fe/src/modules/auth/types/auth.types.ts index d985f1c..b46120d 100644 --- a/fe/src/modules/auth/types/auth.types.ts +++ b/fe/src/modules/auth/types/auth.types.ts @@ -37,6 +37,7 @@ export interface AuthUser { image_url: string | null member_number: number | null member_type: string | null + public_profile_token: string | null status: string gender: string | null marriage_status: string | null diff --git a/fe/src/modules/profile/components/MemberDigitalCard.vue b/fe/src/modules/profile/components/MemberDigitalCard.vue new file mode 100644 index 0000000..6e2b4a9 --- /dev/null +++ b/fe/src/modules/profile/components/MemberDigitalCard.vue @@ -0,0 +1,186 @@ + + + diff --git a/fe/src/modules/profile/components/MemberDigitalCardBack.vue b/fe/src/modules/profile/components/MemberDigitalCardBack.vue new file mode 100644 index 0000000..bc49c86 --- /dev/null +++ b/fe/src/modules/profile/components/MemberDigitalCardBack.vue @@ -0,0 +1,104 @@ + + + diff --git a/fe/src/modules/profile/components/MemberDigitalCardFlip.vue b/fe/src/modules/profile/components/MemberDigitalCardFlip.vue new file mode 100644 index 0000000..ba3c43f --- /dev/null +++ b/fe/src/modules/profile/components/MemberDigitalCardFlip.vue @@ -0,0 +1,34 @@ + + + diff --git a/fe/src/modules/profile/components/MemberDigitalCardFront.vue b/fe/src/modules/profile/components/MemberDigitalCardFront.vue new file mode 100644 index 0000000..7929541 --- /dev/null +++ b/fe/src/modules/profile/components/MemberDigitalCardFront.vue @@ -0,0 +1,97 @@ + + + diff --git a/fe/src/modules/profile/index.ts b/fe/src/modules/profile/index.ts index 6213d79..b69430f 100644 --- a/fe/src/modules/profile/index.ts +++ b/fe/src/modules/profile/index.ts @@ -1,2 +1,2 @@ -export { profileLayoutRoutes } from './routes' +export { profileLayoutRoutes, profilePublicRoutes } from './routes' export { profileMenu } from './menu' diff --git a/fe/src/modules/profile/pages/PublicMemberProfile.vue b/fe/src/modules/profile/pages/PublicMemberProfile.vue new file mode 100644 index 0000000..60722bb --- /dev/null +++ b/fe/src/modules/profile/pages/PublicMemberProfile.vue @@ -0,0 +1,140 @@ + + + diff --git a/fe/src/modules/profile/routes.ts b/fe/src/modules/profile/routes.ts index 22e4a07..5d02d5a 100644 --- a/fe/src/modules/profile/routes.ts +++ b/fe/src/modules/profile/routes.ts @@ -1,5 +1,14 @@ import type { RouteRecordRaw } from 'vue-router' +export const profilePublicRoutes: RouteRecordRaw[] = [ + { + path: '/v/:token', + name: 'public-member-profile', + component: () => import('./pages/PublicMemberProfile.vue'), + meta: { public: true, module: 'profile' }, + }, +] + export const profileLayoutRoutes: RouteRecordRaw[] = [ { path: 'profile-overview-2', diff --git a/fe/src/modules/profile/services/member-digital-card.service.ts b/fe/src/modules/profile/services/member-digital-card.service.ts new file mode 100644 index 0000000..05c7b92 --- /dev/null +++ b/fe/src/modules/profile/services/member-digital-card.service.ts @@ -0,0 +1,35 @@ +import { saveAs } from 'file-saver' +import axios from 'axios' +import { api } from '@/core/services/api' +import { buildCardDownloadFileName } from '../utils/member-digital-card.utils' + +export async function downloadMemberDigitalCard( + memberNumber: string | number | null | undefined, + side: 'depan' | 'belakang', +) { + try { + const { data } = await api.get('/v1/profile/digital-card', { + params: { side }, + responseType: 'blob', + }) + + saveAs(data, buildCardDownloadFileName(memberNumber, side)) + } catch (error) { + if (axios.isAxiosError(error) && error.response?.data instanceof Blob) { + const text = await error.response.data.text() + + try { + const payload = JSON.parse(text) as { message?: string } + throw new Error(payload.message ?? 'Gagal menyimpan kad.') + } catch (parseError) { + if (parseError instanceof SyntaxError) { + throw error + } + + throw parseError + } + } + + throw error + } +} diff --git a/fe/src/modules/profile/services/public-member-profile.service.ts b/fe/src/modules/profile/services/public-member-profile.service.ts new file mode 100644 index 0000000..bbc7158 --- /dev/null +++ b/fe/src/modules/profile/services/public-member-profile.service.ts @@ -0,0 +1,12 @@ +import { api } from '@/core/services/api' +import type { PublicMemberProfileApiResponse } from '../types/public-member-profile.types' + +export async function getPublicMemberProfile( + token: string, +): Promise { + const { data } = await api.get( + `/v1/public/members/${encodeURIComponent(token)}`, + ) + + return data +} diff --git a/fe/src/modules/profile/types/public-member-profile.types.ts b/fe/src/modules/profile/types/public-member-profile.types.ts new file mode 100644 index 0000000..4f694d4 --- /dev/null +++ b/fe/src/modules/profile/types/public-member-profile.types.ts @@ -0,0 +1,16 @@ +export interface PublicMemberProfile { + name: string + member_number: number | null + member_type: string | null + status: string + image_url: string | null + company_name: string | null + verified_at: string +} + +export interface PublicMemberProfileApiResponse { + success: boolean + message?: string + code?: 'public_profile_not_found' | 'public_profile_token_expired' + data: PublicMemberProfile | null +} diff --git a/fe/src/modules/profile/utils/member-digital-card.utils.ts b/fe/src/modules/profile/utils/member-digital-card.utils.ts new file mode 100644 index 0000000..81f23ed --- /dev/null +++ b/fe/src/modules/profile/utils/member-digital-card.utils.ts @@ -0,0 +1,27 @@ +export function displayCardValue(value: string | number | null | undefined) { + if (value === null || value === undefined || value === '') return '-' + return String(value).trim() || '-' +} + +export function buildCardDownloadFileName( + memberNumber: string | number | null | undefined, + side: 'depan' | 'belakang', +) { + const number = memberNumber ?? 'anggota' + return `kad-digital-${number}-${side}.png` +} + +export function toProxiedStorageUrl(url: string | null | undefined): string | null | undefined { + if (!url) return url + + try { + const parsed = new URL(url, window.location.origin) + if (parsed.pathname.startsWith('/storage/')) { + return `${parsed.pathname}${parsed.search}` + } + } catch { + // Keep original URL when parsing fails. + } + + return url +} diff --git a/fe/src/router/index.ts b/fe/src/router/index.ts index c251694..3fe8fe2 100644 --- a/fe/src/router/index.ts +++ b/fe/src/router/index.ts @@ -5,7 +5,7 @@ import { hasAnyActiveRolePermission } from '@/core/utils/activeRolePermission' import { getRouteRequiredPermissions } from '@/core/utils/routePermission' import { authPublicRoutes, resolvePostAuthRoute } from '@/modules/auth' import { membershipApplicationPublicRoutes, membershipApplicationLayoutRoutes } from '@/modules/membership-application' -import { profileLayoutRoutes } from '@/modules/profile' +import { profileLayoutRoutes, profilePublicRoutes } from '@/modules/profile' import { pinia } from '@/pinia' import { useAuthStore } from '@/stores/auth' import { userLayoutRoutes } from '@/modules/user' @@ -70,11 +70,6 @@ const router = createRouter({ name: 'reviews', component: () => import('../views/Reviews.vue'), }, - { - path: 'inbox', - name: 'inbox', - component: () => import('../views/Inbox.vue'), - }, { path: 'file-manager', name: 'file-manager', @@ -85,51 +80,11 @@ const router = createRouter({ name: 'point-of-sale', component: () => import('../views/PointOfSale.vue'), }, - { - path: 'chat', - name: 'chat', - component: () => import('../views/Chat.vue'), - }, - { - path: 'post', - name: 'post', - component: () => import('../views/Post.vue'), - }, { path: 'crud-form', name: 'crud-form', component: () => import('../views/CrudForm.vue'), }, - { - path: 'users-layout-1', - name: 'users-layout-1', - component: () => import('../views/UsersLayout1.vue'), - }, - { - path: 'users-layout-2', - name: 'users-layout-2', - component: () => import('../views/UsersLayout2.vue'), - }, - { - path: 'users-layout-3', - name: 'users-layout-3', - component: () => import('../views/UsersLayout3.vue'), - }, - { - path: 'wizard-layout-1', - name: 'wizard-layout-1', - component: () => import('../views/WizardLayout1.vue'), - }, - { - path: 'wizard-layout-2', - name: 'wizard-layout-2', - component: () => import('../views/WizardLayout2.vue'), - }, - { - path: 'wizard-layout-3', - name: 'wizard-layout-3', - component: () => import('../views/WizardLayout3.vue'), - }, { path: 'blog-layout-1', name: 'blog-layout-1', @@ -191,26 +146,6 @@ const router = createRouter({ component: DocsLayout, children: [ // Documentation - { - path: "accordion", - name: "accordion", - component: () => import("../docs/pages/accordion.vue"), - }, - { - path: "alert", - name: "alert", - component: () => import("../docs/pages/alert.vue"), - }, - { - path: "avatar", - name: "avatar", - component: () => import("../docs/pages/avatar.vue"), - }, - { - path: "badge", - name: "badge", - component: () => import("../docs/pages/badge.vue"), - }, { path: "box", name: "box", @@ -221,26 +156,11 @@ const router = createRouter({ name: "breadcrumb", component: () => import("../docs/pages/breadcrumb.vue"), }, - { - path: "button", - name: "button", - component: () => import("../docs/pages/button.vue"), - }, - { - path: "carousel", - name: "carousel", - component: () => import("../docs/pages/carousel.vue"), - }, { path: "chart", name: "chart", component: () => import("../docs/pages/chart.vue"), }, - { - path: "checkbox", - name: "checkbox", - component: () => import("../docs/pages/checkbox.vue"), - }, { path: "combobox", name: "combobox", @@ -261,16 +181,6 @@ const router = createRouter({ name: "field", component: () => import("../docs/pages/field.vue"), }, - { - path: "input", - name: "input", - component: () => import("../docs/pages/input.vue"), - }, - { - path: "map", - name: "map", - component: () => import("../docs/pages/map.vue"), - }, { path: "menu", name: "menu", @@ -331,11 +241,6 @@ const router = createRouter({ name: "switch", component: () => import("../docs/pages/switch.vue"), }, - { - path: "table", - name: "table", - component: () => import("../docs/pages/table.vue"), - }, { path: "tabs", name: "tabs", @@ -362,6 +267,7 @@ const router = createRouter({ }, ...authPublicRoutes, ...membershipApplicationPublicRoutes, + ...profilePublicRoutes, { path: '/error-page', name: 'error-page', diff --git a/fe/vite.config.ts b/fe/vite.config.ts index 8717bea..2f64aec 100644 --- a/fe/vite.config.ts +++ b/fe/vite.config.ts @@ -18,4 +18,12 @@ export default defineConfig({ '@mykopkb/core': fileURLToPath(new URL('./src/components/ui', import.meta.url)) }, }, + server: { + proxy: { + '/storage': { + target: process.env.VITE_API_BASE_URL || 'http://localhost', + changeOrigin: true, + }, + }, + }, })