From 8c5ef3e1cd04dc3f8072855725bbb498f9c0ab43 Mon Sep 17 00:00:00 2001 From: Annisanzn Date: Sat, 14 Feb 2026 22:05:04 +0700 Subject: [PATCH] landing page dan modal login tapi masih banyak yang error --- .../Controllers/API/ContactController.php | 2 +- .../Http/Controllers/API/HeroController.php | 2 +- .../app/Http/Controllers/AuthController.php | 106 +++++ .../app/Http/Middleware/RoleMiddleware.php | 37 ++ backend/app/Http/Requests/LoginRequest.php | 77 ++++ backend/app/Models/User.php | 29 +- backend/composer.json | 1 + backend/composer.lock | 65 ++- backend/config/cors.php | 2 +- backend/config/sanctum.php | 84 ++++ ... 2024_01_01_000001_create_users_table.php} | 19 +- ...30_create_personal_access_tokens_table.php | 33 ++ backend/database/seeders/DatabaseSeeder.php | 33 +- backend/database/seeders/UserSeeder.php | 45 +++ backend/routes/api.php | 59 ++- backend/test-server.php | 36 ++ frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/src/App-minimal.jsx | 55 +++ frontend/src/App.jsx | 83 +++- frontend/src/components/Articles.jsx | 54 ++- frontend/src/components/Contact.jsx | 32 +- frontend/src/components/LoginModal.jsx | 373 ++++++++++++++++++ frontend/src/components/Navbar.jsx | 49 +-- frontend/src/components/ProtectedRoute.jsx | 47 +++ frontend/src/contexts/AuthContext.jsx | 264 +++++++++++++ frontend/src/index.css | 24 +- frontend/src/pages/KonselorDashboard.jsx | 124 ++++++ frontend/src/pages/LandingPage.jsx | 26 +- frontend/src/pages/OperatorDashboard.jsx | 147 +++++++ frontend/src/pages/UserDashboard.jsx | 120 ++++++ frontend/src/utils/axiosConfig.js | 50 +++ 32 files changed, 1967 insertions(+), 122 deletions(-) create mode 100644 backend/app/Http/Controllers/AuthController.php create mode 100644 backend/app/Http/Middleware/RoleMiddleware.php create mode 100644 backend/app/Http/Requests/LoginRequest.php create mode 100644 backend/config/sanctum.php rename backend/database/migrations/{0001_01_01_000000_create_users_table.php => 2024_01_01_000001_create_users_table.php} (50%) create mode 100644 backend/database/migrations/2026_02_14_072030_create_personal_access_tokens_table.php create mode 100644 backend/database/seeders/UserSeeder.php create mode 100644 backend/test-server.php create mode 100644 frontend/src/App-minimal.jsx create mode 100644 frontend/src/components/LoginModal.jsx create mode 100644 frontend/src/components/ProtectedRoute.jsx create mode 100644 frontend/src/contexts/AuthContext.jsx create mode 100644 frontend/src/pages/KonselorDashboard.jsx create mode 100644 frontend/src/pages/OperatorDashboard.jsx create mode 100644 frontend/src/pages/UserDashboard.jsx create mode 100644 frontend/src/utils/axiosConfig.js diff --git a/backend/app/Http/Controllers/API/ContactController.php b/backend/app/Http/Controllers/API/ContactController.php index 7ff103f..a7d488d 100644 --- a/backend/app/Http/Controllers/API/ContactController.php +++ b/backend/app/Http/Controllers/API/ContactController.php @@ -1,6 +1,6 @@ validated(); + + // Auto-assign role based on email domain + $email = $credentials['email']; + $role = $this->assignRoleByEmail($email); + + // Find user by email + $user = User::where('email', $email)->first(); + + if (!$user || !Hash::check($credentials['password'], $user->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Email atau password salah' + ], 401); + } + + // Update user role if changed + if ($user->role !== $role) { + $user->role = $role; + $user->save(); + } + + // Create token + $token = $user->createToken('auth-token')->plainTextToken; + + return response()->json([ + 'success' => true, + 'message' => 'Login berhasil', + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role, + 'nim' => $user->nim, + ] + ]); + } + + /** + * Handle user logout + */ + public function logout(Request $request) + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Logout berhasil' + ]); + } + + /** + * Get current user + */ + public function user(Request $request) + { + return response()->json([ + 'success' => true, + 'user' => [ + 'id' => $request->user()->id, + 'name' => $request->user()->name, + 'email' => $request->user()->email, + 'role' => $request->user()->role, + 'nim' => $request->user()->nim, + ] + ]); + } + + /** + * Auto-assign role based on email domain + */ + private function assignRoleByEmail(string $email): string + { + // Student email pattern: nim@student.polije.ac.id + if (str_contains($email, 'student.polije.ac.id')) { + return 'user'; + } + + // Staff email pattern: name@polije.ac.id + if (str_contains($email, 'polije.ac.id')) { + return 'konselor'; // Default to konselor for staff + } + + // Default fallback + return 'user'; + } +} diff --git a/backend/app/Http/Middleware/RoleMiddleware.php b/backend/app/Http/Middleware/RoleMiddleware.php new file mode 100644 index 0000000..6686ab5 --- /dev/null +++ b/backend/app/Http/Middleware/RoleMiddleware.php @@ -0,0 +1,37 @@ +check()) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized' + ], 401); + } + + $user = auth()->user(); + + // Check if user has the required role + if ($user->role !== $role) { + return response()->json([ + 'success' => false, + 'message' => 'Forbidden - Insufficient permissions' + ], 403); + } + + return $next($request); + } +} diff --git a/backend/app/Http/Requests/LoginRequest.php b/backend/app/Http/Requests/LoginRequest.php new file mode 100644 index 0000000..1b7bbe2 --- /dev/null +++ b/backend/app/Http/Requests/LoginRequest.php @@ -0,0 +1,77 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => 'required|email', + 'password' => 'required|min:8', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'email.required' => 'Email wajib diisi', + 'email.email' => 'Format email tidak valid', + 'password.required' => 'Password wajib diisi', + 'password.min' => 'Password minimal 8 karakter', + ]; + } + + /** + * Configure the validator instance. + * + * @param \Illuminate\Validation\Validator $validator + * @return void + */ + public function withValidator(Validator $validator): void + { + $validator->after(function ($validator) { + $email = $this->input('email'); + + // Custom validation: email must contain polije.ac.id + if ($email && !str_contains($email, 'polije.ac.id')) { + $validator->errors()->add('email', 'Gunakan email resmi Polije (@polije.ac.id atau @student.polije.ac.id)'); + } + }); + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'email' => 'Email', + 'password' => 'Password', + ]; + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 749c7b7..b89f1a7 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -6,11 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory, Notifiable, HasApiTokens; /** * The attributes that are mass assignable. @@ -21,6 +22,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'role', + 'nim', ]; /** @@ -45,4 +48,28 @@ protected function casts(): array 'password' => 'hashed', ]; } + + /** + * Check if user is student + */ + public function isStudent(): bool + { + return $this->role === 'user'; + } + + /** + * Check if user is konselor + */ + public function isKonselor(): bool + { + return $this->role === 'konselor'; + } + + /** + * Check if user is operator + */ + public function isOperator(): bool + { + return $this->role === 'operator'; + } } diff --git a/backend/composer.json b/backend/composer.json index 52a3a8a..6875678 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.3", "laravel/tinker": "^2.10.1" }, "require-dev": { diff --git a/backend/composer.lock b/backend/composer.lock index 73aa270..38f4bee 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c514d8f7b9fc5970bdd94287905ef584", + "content-hash": "ad91ab9bde70e3576f2b64dd11542bc4", "packages": [ { "name": "brick/math", @@ -1333,6 +1333,69 @@ }, "time": "2026-02-06T12:17:10+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.9", diff --git a/backend/config/cors.php b/backend/config/cors.php index ba38227..8a02cd3 100644 --- a/backend/config/cors.php +++ b/backend/config/cors.php @@ -19,7 +19,7 @@ 'allowed_methods' => ['*'], - 'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175'], + 'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175', 'http://localhost:5176', 'http://127.0.0.1:5176'], 'allowed_origins_patterns' => [], diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/backend/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/2024_01_01_000001_create_users_table.php similarity index 50% rename from backend/database/migrations/0001_01_01_000000_create_users_table.php rename to backend/database/migrations/2024_01_01_000001_create_users_table.php index 05fb5d9..0e4a4ed 100644 --- a/backend/database/migrations/0001_01_01_000000_create_users_table.php +++ b/backend/database/migrations/2024_01_01_000001_create_users_table.php @@ -17,24 +17,11 @@ public function up(): void $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->enum('role', ['user', 'konselor', 'operator'])->default('user'); + $table->string('nim')->nullable(); // For student users $table->rememberToken(); $table->timestamps(); }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); } /** @@ -43,7 +30,5 @@ public function up(): void public function down(): void { Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); } }; diff --git a/backend/database/migrations/2026_02_14_072030_create_personal_access_tokens_table.php b/backend/database/migrations/2026_02_14_072030_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/backend/database/migrations/2026_02_14_072030_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index 12ed069..7012946 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -79,10 +79,33 @@ public function run(): void Article::create($article); } - // Create test user - User::factory()->create([ - 'name' => 'Admin PolijeCare', - 'email' => 'admin@polije.ac.id', - ]); + // Create test users with specific roles + $users = [ + [ + 'name' => 'Ahmad Mahasiswa', + 'email' => '2021001@student.polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'user', + 'nim' => '2021001', + ], + [ + 'name' => 'Siti Konselor', + 'email' => 'siti@polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'konselor', + 'nim' => null, + ], + [ + 'name' => 'Budi Operator', + 'email' => 'budi@polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'operator', + 'nim' => null, + ], + ]; + + foreach ($users as $user) { + User::create($user); + } } } diff --git a/backend/database/seeders/UserSeeder.php b/backend/database/seeders/UserSeeder.php new file mode 100644 index 0000000..70c407a --- /dev/null +++ b/backend/database/seeders/UserSeeder.php @@ -0,0 +1,45 @@ + 'Ahmad Mahasiswa', + 'email' => '2021001@student.polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'user', + 'nim' => '2021001', + ], + [ + 'name' => 'Siti Konselor', + 'email' => 'siti@polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'konselor', + 'nim' => null, + ], + [ + 'name' => 'Budi Operator', + 'email' => 'budi@polije.ac.id', + 'password' => Hash::make('password123'), + 'role' => 'operator', + 'nim' => null, + ], + ]; + + foreach ($users as $user) { + User::create($user); + } + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index fac73c2..3bb8ed9 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -2,9 +2,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -use App\Http\Controllers\Api\ArticleController; -use App\Http\Controllers\Api\ContactController; -use App\Http\Controllers\Api\HeroController; +use App\Http\Controllers\API\ArticleController; +use App\Http\Controllers\API\ContactController; +use App\Http\Controllers\API\HeroController; +use App\Http\Controllers\AuthController; +use App\Http\Middleware\RoleMiddleware; Route::get('/test', function () { return response()->json([ @@ -21,3 +23,54 @@ // Hero Section Route::get('/hero', [HeroController::class, 'index']); + +// Auth routes +Route::post('/login', [AuthController::class, 'login']); + +// Protected routes +Route::middleware('auth:sanctum')->group(function () { + Route::post('/logout', [AuthController::class, 'logout']); + Route::get('/user', [AuthController::class, 'user']); + + // User routes + Route::middleware(RoleMiddleware::class . ':user')->prefix('user')->group(function () { + Route::get('/dashboard', function () { + return response()->json([ + 'success' => true, + 'message' => 'User Dashboard', + 'data' => [ + 'role' => 'user', + 'dashboard' => 'student' + ] + ]); + }); + }); + + // Konselor routes + Route::middleware(RoleMiddleware::class . ':konselor')->prefix('konselor')->group(function () { + Route::get('/dashboard', function () { + return response()->json([ + 'success' => true, + 'message' => 'Konselor Dashboard', + 'data' => [ + 'role' => 'konselor', + 'dashboard' => 'konselor' + ] + ]); + }); + }); + + // Operator routes + Route::middleware(RoleMiddleware::class . ':operator')->prefix('operator')->group(function () { + Route::get('/dashboard', function () { + return response()->json([ + 'success' => true, + 'message' => 'Operator Dashboard', + 'data' => [ + 'role' => 'operator', + 'dashboard' => 'operator' + ] + ]); + }); + }); +}); diff --git a/backend/test-server.php b/backend/test-server.php new file mode 100644 index 0000000..e602663 --- /dev/null +++ b/backend/test-server.php @@ -0,0 +1,36 @@ +make(Illuminate\Contracts\Console\Kernel::class); + +$kernel->bootstrap(); + +echo "Backend server is ready!\n"; +echo "Available routes:\n"; +echo "- GET /api/test\n"; +echo "- POST /api/login\n"; +echo "- GET /api/user\n"; +echo "- GET /api/contact\n"; +echo "- GET /api/hero\n"; +echo "- GET /api/articles\n"; + +// Test database connection +try { + \Illuminate\Support\Facades\DB::connection()->getPdo(); + echo "\n✅ Database connection: OK\n"; + + // Check users + $userCount = \App\Models\User::count(); + echo "✅ Users in database: {$userCount}\n"; + + if ($userCount > 0) { + echo "✅ Test users ready for login\n"; + } +} catch (\Exception $e) { + echo "\n❌ Database connection failed: " . $e->getMessage() . "\n"; +} + +echo "\nTo start the server, run: php artisan serve\n"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 42bb602..f6e01de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "framer-motion": "^10.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", "tailwindcss": "^3.4.19" }, @@ -3482,6 +3483,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1dfb13d..381582f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "framer-motion": "^10.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", "tailwindcss": "^3.4.19" }, diff --git a/frontend/src/App-minimal.jsx b/frontend/src/App-minimal.jsx new file mode 100644 index 0000000..6c2f892 --- /dev/null +++ b/frontend/src/App-minimal.jsx @@ -0,0 +1,55 @@ +import React from 'react'; + +const AppMinimal = () => { + return ( +
+

PolijeCare - Test Page

+

Halaman test untuk memastikan React berfungsi.

+ +
+

Status System:

+
    +
  • ✅ React berjalan
  • +
  • ✅ CSS terload
  • +
  • ✅ Tidak ada black screen
  • +
+
+ +
+

Test Login:

+

Akun test tersedia:

+
+ Mahasiswa: 2021001@student.polije.ac.id / password123
+ Konselor: siti@polije.ac.id / password123
+ Operator: budi@polije.ac.id / password123 +
+
+ + +
+ ); +}; + +export default AppMinimal; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 74104a4..d895b3a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,29 +1,84 @@ import React from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { ThemeProvider } from './contexts/ThemeContext'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; +import ProtectedRoute from './components/ProtectedRoute'; + +// Pages import LandingPage from './pages/LandingPage'; -import ArticleDetail from './components/ArticleDetail'; -import './App.css'; +import UserDashboard from './pages/UserDashboard'; +import KonselorDashboard from './pages/KonselorDashboard'; +import OperatorDashboard from './pages/OperatorDashboard'; function App() { return ( - -
+ + + {/* Public Routes */} } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + + {/* Protected Dashboard Routes */} + + + + } + /> + + + + } + /> + + + + } + /> + + {/* Redirect */} + } /> + + {/* 404 */} + } /> -
-
+ +
); } +// Redirect component for role-based navigation +const RedirectDashboard = () => { + const { user, isAuthenticated } = useAuth(); + + if (!isAuthenticated || !user) { + return ; + } + + switch (user.role) { + case 'user': + return ; + case 'konselor': + return ; + case 'operator': + return ; + default: + return ; + } +}; + export default App; diff --git a/frontend/src/components/Articles.jsx b/frontend/src/components/Articles.jsx index 488f243..44097c9 100644 --- a/frontend/src/components/Articles.jsx +++ b/frontend/src/components/Articles.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { Link } from 'react-router-dom'; -import { articleService } from '../services/articleService'; import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants'; const Articles = () => { @@ -10,25 +9,40 @@ const Articles = () => { const [error, setError] = useState(null); useEffect(() => { - fetchArticles(); - }, []); - - const fetchArticles = async () => { - try { - setLoading(true); - const response = await articleService.getAll(); - if (response.success) { - setArticles(response.data); - } else { - setError('Failed to load articles'); + // Temporary disable API calls to prevent CORS errors + const mockArticles = [ + { + id: 1, + title: 'Pentingnya Menjaga Lingkungan Kampus Aman dari Kekerasan Seksual', + slug: 'pentingnya-menjaga-lingkungan-kampus-aman-dari-kekerasan-seksual', + image: 'articles/safe-campus.jpg', + content: 'Lingkungan kampus yang aman adalah hak setiap sivitas akademika.', + is_published: true, + published_at: '2024-01-07T00:00:00.000000Z' + }, + { + id: 2, + title: 'Prosedur Pelaporan Kasus Kekerasan Seksual di Polije', + slug: 'prosedur-pelaporan-kasus-kekerasan-seksual-di-polije', + image: 'articles/reporting-procedure.jpg', + content: 'Prosedur pelaporan kasus kekerasan seksual di Politeknik Negeri Jember.', + is_published: true, + published_at: '2024-01-05T00:00:00.000000Z' + }, + { + id: 3, + title: 'Hak dan Kewajiban Korban dan Pelapor Kekerasan Seksual', + slug: 'hak-dan-kewajiban-korban-dan-pelapor-kekerasan-seksual', + image: 'articles/rights-responsibilities.jpg', + content: 'Sebagai korban atau pelapor kekerasan seksual, Anda memiliki hak-hak.', + is_published: true, + published_at: '2024-01-03T00:00:00.000000Z' } - } catch (err) { - setError('Failed to load articles'); - console.error('Error fetching articles:', err); - } finally { - setLoading(false); - } - }; + ]; + + setArticles(mockArticles); + setLoading(false); + }, []); const formatDate = (dateString) => { const options = { year: 'numeric', month: 'long', day: 'numeric' }; @@ -144,7 +158,7 @@ const Articles = () => { > {articles.map((article, index) => ( { @@ -9,26 +8,21 @@ const Contact = () => { const [error, setError] = useState(null); useEffect(() => { - fetchContactInfo(); + // Temporary disable API calls to prevent 404 errors + const mockContactInfo = { + address: 'Jl. Mastrip PO Box 164, Jember 68121, Jawa Timur, Indonesia', + phone: '+62 331-123456', + email: 'satgasppkpt@polije.ac.id', + instagram: '@satgasppkpt_polije', + whatsapp: '+6281234567890', + facebook: 'SatgasPPKPTPolije', + twitter: '@SatgasPPKPTPolije' + }; + + setContactInfo(mockContactInfo); + setLoading(false); }, []); - const fetchContactInfo = async () => { - try { - setLoading(true); - const response = await contactService.get(); - if (response.success) { - setContactInfo(response.data); - } else { - setError('Failed to load contact information'); - } - } catch (err) { - setError('Failed to load contact information'); - console.error('Error fetching contact info:', err); - } finally { - setLoading(false); - } - }; - const defaultContact = { address: 'Jl. Mastrip PO Box 164, Jember 68121, Jawa Timur, Indonesia', phone: '+62 331-123456', diff --git a/frontend/src/components/LoginModal.jsx b/frontend/src/components/LoginModal.jsx new file mode 100644 index 0000000..fb5de2a --- /dev/null +++ b/frontend/src/components/LoginModal.jsx @@ -0,0 +1,373 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { FiMail, FiLock, FiAlertCircle, FiEye, FiEyeOff, FiX } from 'react-icons/fi'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +// Motion variants for animations +const fadeIn = { + hidden: { opacity: 0 }, + show: { opacity: 1, transition: { duration: 0.3 } } +}; + +const slideUp = { + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0, transition: { duration: 0.3, delay: 0.1 } } +}; + +const modalVariants = { + hidden: { opacity: 0, scale: 0.95 }, + show: { + opacity: 1, + scale: 1, + transition: { + duration: 0.2, + type: "spring", + stiffness: 300, + damping: 30 + } + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { duration: 0.2 } + } +}; + +const backdropVariants = { + hidden: { opacity: 0 }, + show: { opacity: 1, transition: { duration: 0.2 } }, + exit: { opacity: 0, transition: { duration: 0.2 } } +}; + +const LoginModal = ({ isOpen, onClose }) => { + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [loginError, setLoginError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@polije\.ac\.id$/; + return emailRegex.test(email); + }; + + const validateForm = () => { + const newErrors = {}; + + if (!formData.email) { + newErrors.email = 'Email wajib diisi'; + } else if (!validateEmail(formData.email)) { + newErrors.email = 'Email harus menggunakan domain @polije.ac.id'; + } + + if (!formData.password) { + newErrors.password = 'Password wajib diisi'; + } else if (formData.password.length < 6) { + newErrors.password = 'Password minimal 6 karakter'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + + // Clear error when user starts typing + if (errors[name]) { + setErrors(prev => ({ + ...prev, + [name]: '' + })); + } + + // Clear login error when user starts typing + if (loginError) { + setLoginError(''); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setIsLoading(true); + setLoginError(''); + + try { + const result = await login({ + email: formData.email, + password: formData.password + }); + + console.log('🔍 Login result:', result); + + if (result && result.success) { + console.log('✅ Login successful'); + onClose(); // Close modal + navigate('/redirect'); + } else { + console.error('❌ Login failed:', result); + const errorMessage = result?.message || result?.error || 'Login gagal. Silakan coba lagi.'; + setLoginError(errorMessage); + } + } catch (error) { + console.error('❌ Login error:', error); + console.error('Error details:', error.response?.data || error.message); + const errorMessage = error.response?.data?.message || error.message || 'Terjadi kesalahan. Silakan coba lagi.'; + setLoginError(errorMessage); + } + + setIsLoading(false); + }; + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + const handleClose = () => { + if (!isLoading) { + onClose(); + // Reset form + setFormData({ email: '', password: '' }); + setErrors({}); + setLoginError(''); + setShowPassword(false); + } + }; + + const handleBackdropClick = (e) => { + if (e.target === e.currentTarget) { + handleClose(); + } + }; + + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + handleClose(); + } + }; + + React.useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } else { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'unset'; + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'unset'; + }; + }, [isOpen]); + + return ( + + {isOpen && ( + + {/* Backdrop */} +
+ + {/* Modal */} + e.stopPropagation()} + > + {/* Close Button */} + + + {/* Modal Content */} +
+ {/* Header with Protection Icon */} + +
+ Polijecare Logo +
+

+ PolijeCare +

+

+ Portal Layanan Perlindungan & Kesejahteraan +

+
+ + {/* Login Form */} + +
+

+ Masuk menggunakan akun resmi Polije
+ untuk menjaga keamanan dan kerahasiaan data Anda. +

+
+ + {/* Login Error Alert */} + {loginError && ( + + + {loginError} + + )} + + {/* Email Field */} +
+ +
+ + +
+ {errors.email && ( + + {errors.email} + + )} +
+ + {/* Password Field */} +
+ +
+ + + +
+ {errors.password && ( + + {errors.password} + + )} +
+ + {/* Submit Button */} + + {isLoading ? ( +
+
+ Masuk... +
+ ) : ( + 'Masuk' + )} +
+
+ + {/* Footer */} + +

+ Dilindungi dengan enkripsi end-to-end
+ © 2026 PolijeCare - Politeknik Negeri Jember +

+
+
+ + {/* Background Decoration */} +
+
+
+
+
+
+ )} +
+ ); +}; + +export default LoginModal; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 3997cfa..03cdb4d 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -4,10 +4,12 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '../hooks/useAuth'; import { fadeIn, slideDown } from '../utils/motionVariants'; import ThemeToggle from './ThemeToggle'; +import LoginModal from './LoginModal'; const Navbar = () => { const [isScrolled, setIsScrolled] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const { isAuthenticated, user, logout } = useAuth(); const navigate = useNavigate(); const location = useLocation(); @@ -78,22 +80,15 @@ const Navbar = () => {
{/* Logo */} - handleNavClick('#hero')} - > -
-
- PolijeCare Logo -
-
+ +
+ Polijecare Logo
- + Polijecare @@ -151,12 +146,12 @@ const Navbar = () => { ) : ( - setIsLoginModalOpen(true)} className="px-6 py-2.5 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-soft font-medium" > Masuk - + )} {/* Theme Toggle */} @@ -259,13 +254,15 @@ const Navbar = () => { ) : ( - setIsMobileMenuOpen(false)} - className="block w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium text-center" + )}
@@ -273,6 +270,12 @@ const Navbar = () => { )} + + {/* Login Modal */} + setIsLoginModalOpen(false)} + /> ); }; diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..e88b270 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +const ProtectedRoute = ({ children, requiredRole }) => { + const { user, isAuthenticated, isLoading } = useAuth(); + + // Show loading while checking auth + if (isLoading) { + return ( +
+
+
+ ); + } + + // Redirect to login if not authenticated + if (!isAuthenticated) { + return ; + } + + // Check role if specified + if (requiredRole && user?.role !== requiredRole) { + return ( +
+
+
🚫
+

Akses Ditolak

+

+ Anda tidak memiliki izin untuk mengakses halaman ini. +

+ +
+
+ ); + } + + // Render children if authenticated and authorized + return children; +}; + +export default ProtectedRoute; diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..415532d --- /dev/null +++ b/frontend/src/contexts/AuthContext.jsx @@ -0,0 +1,264 @@ +import React, { createContext, useContext, useReducer, useEffect } from 'react'; +import axios from '../utils/axiosConfig'; + +// Initial state +const initialState = { + user: null, + token: localStorage.getItem('token'), + isAuthenticated: false, + isLoading: false, // Changed to false initially + error: null, +}; + +// Action types +const AUTH_ACTIONS = { + LOGIN_START: 'LOGIN_START', + LOGIN_SUCCESS: 'LOGIN_SUCCESS', + LOGIN_FAILURE: 'LOGIN_FAILURE', + LOGOUT: 'LOGOUT', + LOAD_USER_START: 'LOAD_USER_START', + LOAD_USER_SUCCESS: 'LOAD_USER_SUCCESS', + LOAD_USER_FAILURE: 'LOAD_USER_FAILURE', + CLEAR_ERROR: 'CLEAR_ERROR', +}; + +// Reducer +const authReducer = (state, action) => { + switch (action.type) { + case AUTH_ACTIONS.LOGIN_START: + return { + ...state, + isLoading: true, + error: null, + }; + + case AUTH_ACTIONS.LOGIN_SUCCESS: + return { + ...state, + isLoading: false, + isAuthenticated: true, + user: action.payload.user, + token: action.payload.token, + error: null, + }; + + case AUTH_ACTIONS.LOGIN_FAILURE: + return { + ...state, + isLoading: false, + isAuthenticated: false, + user: null, + token: null, + error: action.payload, + }; + + case AUTH_ACTIONS.LOGOUT: + return { + ...state, + isAuthenticated: false, + user: null, + token: null, + isLoading: false, + error: null, + }; + + case AUTH_ACTIONS.LOAD_USER_START: + return { + ...state, + isLoading: true, + }; + + case AUTH_ACTIONS.LOAD_USER_SUCCESS: + return { + ...state, + user: action.payload, + isAuthenticated: true, + isLoading: false, + }; + + case AUTH_ACTIONS.LOAD_USER_FAILURE: + return { + ...state, + user: null, + isAuthenticated: false, + token: null, + isLoading: false, + }; + + case AUTH_ACTIONS.CLEAR_ERROR: + return { + ...state, + error: null, + }; + + default: + return state; + } +}; + +// Create context +const AuthContext = createContext(); + +// Provider component +export const AuthProvider = ({ children }) => { + const [state, dispatch] = useReducer(authReducer, initialState); + + // Set axios default header + useEffect(() => { + if (state.token) { + axios.defaults.headers.common['Authorization'] = `Bearer ${state.token}`; + } else { + delete axios.defaults.headers.common['Authorization']; + } + }, [state.token]); + + // Load user on mount + useEffect(() => { + const loadUser = async () => { + const token = localStorage.getItem('token'); + + if (token) { + // Set token immediately to prevent white screen + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + dispatch({ type: AUTH_ACTIONS.LOAD_USER_START }); + + try { + const response = await axios.get('/api/user'); + if (response.data.success) { + dispatch({ + type: AUTH_ACTIONS.LOAD_USER_SUCCESS, + payload: response.data.user, + }); + } else { + dispatch({ type: AUTH_ACTIONS.LOAD_USER_FAILURE }); + localStorage.removeItem('token'); + delete axios.defaults.headers.common['Authorization']; + } + } catch (error) { + console.error('Load user error:', error); + dispatch({ type: AUTH_ACTIONS.LOAD_USER_FAILURE }); + localStorage.removeItem('token'); + delete axios.defaults.headers.common['Authorization']; + } + } else { + // No token - set loading to false immediately + dispatch({ + type: AUTH_ACTIONS.LOAD_USER_SUCCESS, + payload: null, + }); + } + }; + + loadUser(); + }, []); + + // Login function + const login = async (credentials) => { + try { + dispatch({ type: AUTH_ACTIONS.LOGIN_START }); + + console.log('🔍 Attempting login to:', '/api/login'); + console.log('🔍 Credentials:', { email: credentials.email, password: '***' }); + + const response = await axios.post('/api/login', credentials); + + console.log('🔍 Auth response:', response.data); + console.log('🔍 Response status:', response.status); + + if (response.data && response.data.success) { + const { token, user } = response.data; + + // Store token + localStorage.setItem('token', token); + + // Set axios header + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + dispatch({ + type: AUTH_ACTIONS.LOGIN_SUCCESS, + payload: { token, user }, + }); + + return { success: true, data: response.data }; + } else { + const message = response.data?.message || 'Login gagal'; + console.error('❌ Login failed:', message); + return { success: false, message }; + } + } catch (error) { + console.error('❌ Auth error:', error); + console.error('❌ Error config:', error.config); + console.error('❌ Error response:', error.response); + console.error('❌ Error status:', error.response?.status); + console.error('❌ Error data:', error.response?.data); + + // Handle different error types + let errorMessage = 'Login gagal. Silakan coba lagi.'; + + if (error.code === 'ECONNREFUSED' || error.code === 'ERR_NETWORK') { + errorMessage = 'Tidak dapat terhubung ke server. Pastikan backend berjalan.'; + } else if (error.response?.status === 401) { + errorMessage = error.response.data?.message || 'Email atau password salah.'; + } else if (error.response?.status === 422) { + errorMessage = 'Validasi gagal. Periksa kembali input Anda.'; + } else if (error.response?.data?.message) { + errorMessage = error.response.data.message; + } else if (error.message) { + errorMessage = error.message; + } + + dispatch({ + type: AUTH_ACTIONS.LOGIN_FAILURE, + payload: errorMessage, + }); + + return { success: false, error: errorMessage }; + } + }; + + // Logout function + const logout = async () => { + try { + if (state.token) { + await axios.post('/api/logout'); + } + } catch (error) { + console.error('Logout error:', error); + } finally { + localStorage.removeItem('token'); + delete axios.defaults.headers.common['Authorization']; + dispatch({ type: AUTH_ACTIONS.LOGOUT }); + } + }; + + // Clear error + const clearError = () => { + dispatch({ type: AUTH_ACTIONS.CLEAR_ERROR }); + }; + + const value = { + ...state, + login, + logout, + clearError, + }; + + return ( + + {children} + + ); +}; + +// Custom hook to use auth context +export const useAuth = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +}; + +export default AuthContext; diff --git a/frontend/src/index.css b/frontend/src/index.css index b966cf2..92a28f1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -278,10 +278,31 @@ body { .hover\:bg-green-600:hover, .hover\:bg-green-700:hover, .hover\:bg-green-800:hover, -.hover\:bg-green-900:hover { +.hover\:bg-green-900:hover, +.from-green-50, +.from-green-100, +.from-green-200, +.from-green-300, +.from-green-400, +.from-green-500, +.from-green-600, +.from-green-700, +.from-green-800, +.from-green-900, +.to-green-50, +.to-green-100, +.to-green-200, +.to-green-300, +.to-green-400, +.to-green-500, +.to-green-600, +.to-green-700, +.to-green-800, +.to-green-900 { background-color: transparent !important; color: inherit !important; border-color: transparent !important; + background-image: none !important; } /* Force remove green from any element */ @@ -289,6 +310,7 @@ body { background-color: transparent !important; color: inherit !important; border-color: transparent !important; + background-image: none !important; } /* Additional Variables */ diff --git a/frontend/src/pages/KonselorDashboard.jsx b/frontend/src/pages/KonselorDashboard.jsx new file mode 100644 index 0000000..0b186f8 --- /dev/null +++ b/frontend/src/pages/KonselorDashboard.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { FiUsers, FiCalendar, FiFileText, FiCheckCircle } from 'react-icons/fi'; + +const KonselorDashboard = () => { + return ( +
+ {/* Header */} +
+
+
+

Dashboard Konselor

+
+ Konselor +
+ +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Welcome Section */} + +

Dashboard Konseling

+

+ Kelola dan monitoring layanan konseling untuk sivitas akademika +

+
+ + {/* Stats Cards */} +
+ +
+ + 45 +
+

Total Laporan

+

Semua laporan masuk

+
+ + +
+ + 8 +
+

Jadwal Hari Ini

+

Sesi konseling

+
+ + +
+ + 32 +
+

Selesai

+

Laporan selesai

+
+ + +
+ + 13 +
+

Sedang Diproses

+

Laporan aktif

+
+
+ + {/* Recent Reports */} + +

Laporan Membutuhkan Perhatian

+
+ {[1, 2, 3].map((item) => ( +
+
+

Kasus #{item}

+

Prioritas Tinggi • 1 jam lalu

+
+ +
+ ))} +
+
+
+
+ ); +}; + +export default KonselorDashboard; diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx index 72b03c8..6848bed 100644 --- a/frontend/src/pages/LandingPage.jsx +++ b/frontend/src/pages/LandingPage.jsx @@ -6,32 +6,28 @@ import Services from '../components/Services'; import Articles from '../components/Articles'; import Contact from '../components/Contact'; import Footer from '../components/Footer'; -import { heroService } from '../services/heroService'; +import LoginModal from '../components/LoginModal'; const LandingPage = () => { const [heroData, setHeroData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { - fetchHeroData(); + // Temporary disable API calls to prevent 404 errors + const mockHeroData = { + title: 'Aman Bicara, Aman Melapor', + subtitle: 'Satgas PPKPT Politeknik Negeri Jember', + description: 'Kami siap mendengar dan membantu Anda dengan profesionalisme dan kerahasiaan terjamin. Setiap laporan akan ditangani dengan empati dan seksama.' + }; + + setHeroData(mockHeroData); + setLoading(false); }, []); - const fetchHeroData = async () => { - try { - const response = await heroService.get(); - if (response.success) { - setHeroData(response.data); - } - } catch (error) { - console.error('Error fetching hero data:', error); - } finally { - setLoading(false); - } - }; - return (
+ diff --git a/frontend/src/pages/OperatorDashboard.jsx b/frontend/src/pages/OperatorDashboard.jsx new file mode 100644 index 0000000..e258af1 --- /dev/null +++ b/frontend/src/pages/OperatorDashboard.jsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { FiSettings, FiUsers, FiFileText, FiEdit, FiDatabase } from 'react-icons/fi'; + +const OperatorDashboard = () => { + return ( +
+ {/* Header */} +
+
+
+

Dashboard Operator

+
+ Operator +
+ +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Welcome Section */} + +

Sistem Administrasi

+

+ Kelola seluruh sistem PolijeCare dan monitoring aktivitas +

+
+ + {/* Admin Functions */} +
+ +
+ + 156 +
+

Manajemen Pengguna

+

Kelola akun pengguna

+
+ + +
+ + 89 +
+

Manajemen Pengaduan

+

Kelola semua pengaduan

+
+ + +
+ + 24 +
+

Manajemen Artikel

+

Kelola konten artikel

+
+ + +
+ + 12 +
+

Manajemen Kategori

+

Kelola kategori laporan

+
+ + +
+ + Pengaturan +
+

Setting Admin

+

Konfigurasi sistem

+
+ + +
+ + 5 +
+

Audit Trail

+

Log aktivitas sistem

+
+
+ + {/* System Status */} + +

Status Sistem

+
+
+

Database

+

Normal • 99.9% Uptime

+
+
+

API Server

+

Normal • Response: 45ms

+
+
+

Storage

+

Warning • 85% Used

+
+
+
+
+
+ ); +}; + +export default OperatorDashboard; diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx new file mode 100644 index 0000000..969ab5f --- /dev/null +++ b/frontend/src/pages/UserDashboard.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { FiFileText, FiClock, FiUser, FiPlus } from 'react-icons/fi'; + +const UserDashboard = () => { + return ( +
+ {/* Header */} +
+
+
+

Dashboard Mahasiswa

+
+ Mahasiswa +
+ +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Welcome Section */} + +

Selamat Datang di PolijeCare

+

+ Portal layanan perlindungan dan kesejahteraan sivitas akademika Politeknik Negeri Jember +

+
+ + {/* Quick Actions */} +
+ +
+ + + +
+

Buat Laporan

+

Ajukan pengaduan atau konseling

+
+ + +
+ + 12 +
+

Riwayat Laporan

+

Lihat semua laporan Anda

+
+ + +
+ + 3 +
+

Sedang Diproses

+

Laporan dalam proses

+
+ + +
+ + Profil +
+

Pengaturan

+

Kelola akun Anda

+
+
+ + {/* Recent Reports */} + +

Laporan Terbaru

+
+ {[1, 2, 3].map((item) => ( +
+
+

Laporan #{item}

+

Dibuat 2 hari yang lalu

+
+ + Sedang Diproses + +
+ ))} +
+
+
+
+ ); +}; + +export default UserDashboard; diff --git a/frontend/src/utils/axiosConfig.js b/frontend/src/utils/axiosConfig.js new file mode 100644 index 0000000..dd31fd6 --- /dev/null +++ b/frontend/src/utils/axiosConfig.js @@ -0,0 +1,50 @@ +import axios from 'axios'; + +// Create axios instance +const api = axios.create({ + baseURL: 'http://localhost:8000', // Backend URL + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, +}); + +// Request interceptor to add auth token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor for error handling +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // Handle 401 Unauthorized - only redirect if not already on home page + if (error.response?.status === 401 && window.location.pathname !== '/') { + localStorage.removeItem('token'); + delete api.defaults.headers.common['Authorization']; + // Use navigate instead of direct location change to avoid infinite loop + console.log('🔐 Token expired, redirecting to home...'); + } + + // Handle 403 Forbidden + if (error.response?.status === 403) { + console.error('Access forbidden - insufficient permissions'); + } + + return Promise.reject(error); + } +); + +export default api;