บทที่ 19advanced|10 KB
บทที่ 19: Admin Dashboard & Analytics
สารบัญเนื้อหา
บทที่ 19: Admin Dashboard & Analytics
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจระบบ Role-Based Access Control (RBAC) (Admin vs User)
- สร้าง Middleware ฝั่ง Backend เพื่อป้องกันคนนอกเข้าถึงหน้า Admin
- สร้าง Dashboard แสดงสถิติเบื้องต้น (User Count, Total Notes)
- จัดการผู้ใช้ (ดูรายชื่อ, แบน, ลบ)
📚 ทฤษฎีและแนวคิด
1. RBAC คืออะไร?
Role-Based Access Control คือการกำหนด "สิทธิ์" ตาม "บทบาท":
- User: สร้าง/แก้ไข/ลบ Note ของตัวเองได้เท่านั้น
- Admin: ดูข้อมูลคนอื่นได้, ลบ User ได้, ดูภาพรวมระบบได้
2. Security at Backend
ห้ามเชื่อ Frontend เด็ดขาด! แม้เราจะซ่อนปุ่ม Admin ไว้ แต่ Hacker สามารถยิง API ตรงๆ ได้ ดังนั้นเราต้องมี Middleware ดักจับที่หลังบ้านเสมอ:
- Check Token (Login หรือยัง?)
- Check Role (เป็น Admin หรือเปล่า?)
💻 การเขียนโค้ด (Implementation)
1. Backend: Admin Middleware
ไฟล์: backend/src/middleware/admin.js
/*
Middleware นี้จะทำงาน "ต่อจาก" auth middleware
แปลว่า req.user จะต้องมีค่าแล้ว (จากการ decode jwt)
*/
const adminCheck = (req, res, next) => {
// 1. ตรวจสอบว่ามีข้อมูล user ไหม
if (!req.user) {
return res.status(401).json({ error: "Unauthorized" });
}
// 2. ตรวจสอบ Role
if (req.user.role !== 'admin') {
// 403 Forbidden = รู้ว่าคุณเป็นใคร (Login แล้ว) แต่ไม่มีสิทธิ์
return res.status(403).json({ error: "Admin access required" });
}
// 3. ผ่าน! ไปทำงานต่อ
next();
};
module.exports = adminCheck;
การนำไปใช้ใน Routes: backend/src/routes/adminRoutes.js
const express = require("express");
const router = express.Router();
const auth = require("../middleware/auth");
const admin = require("../middleware/admin");
const User = require("../models/User");
**อธิบายทีละบรรทัด (Imports):**
- `const express = require("express")`
- นำเข้า Express Framework
- `const router = express.Router()`
- สร้าง Router ใหม่เพื่อจัดการ Admin routes โดยเฉพาะ
- `const auth / admin`
- นำเข้า Middlewares ที่เราสร้างไว้ (ต้องผ่าน Auth ก่อน, แล้วค่อยผ่าน Admin Check)
**อธิบาย:**
// ต้องผ่านด่าน auth ก่อน -> แล้วค่อยผ่านด่าน admin
router.get("/users", auth, admin, async (req, res) => {
try {
// ดึงรายชื่อ User ทั้งหมด (ไม่เอา password)
const users = await User.find().select("-passwordHash");
res.json({ users });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
module.exports = router;
2. Frontend: Admin Service
ไฟล์: mobile/lib/services/admin_api.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'api_config.dart';
import 'session.dart';
class AdminApi {
static Map<String, String> _headers() {
return {
"Content-Type": "application/json",
"Authorization": "Bearer ${Session.token}",
};
}
// ดึงสถิติรวม
static Future<Map<String, dynamic>> getStats() async {
final uri = Uri.parse("$kApiBase/api/admin/stats");
final res = await http.get(uri, headers: _headers());
if (res.statusCode != 200) throw Exception("Failed to load stats");
return jsonDecode(res.body);
}
// ดึงรายชื่อ User
static Future<List<dynamic>> getUsers() async {
final uri = Uri.parse("$kApiBase/api/admin/users");
final res = await http.get(uri, headers: _headers());
if (res.statusCode != 200) throw Exception("Access Denied");
final data = jsonDecode(res.body);
return data['users'];
}
}
3. Frontend: Protecting the UI
เราไม่อยากให้ปุ่ม "Admin Dashboard" รกตา User ธรรมดา เราจะซ่อนมันไว้
ไฟล์: mobile/lib/screens/settings_screen.dart
// ตรวจสอบ role จาก Session ที่เราเก็บไว้ตอน Login
bool isAdmin = Session.user?['role'] == 'admin';
if (isAdmin)
ListTile(
leading: Icon(Icons.dashboard, color: Colors.purple),
title: Text("Admin Dashboard"),
trailing: Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AdminDashboardScreen())
);
},
),
4. Admin Dashboard Screen
ไฟล์: mobile/lib/screens/admin_dashboard_screen.dart
หน้าจอนี้จะแสดง Card สถิติ และรายชื่อ User
class AdminDashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ผู้ดูแลระบบ")),
body: FutureBuilder(
future: AdminApi.getStats(), // เรียก API
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text("Error: ${snapshot.error}"));
}
final stats = snapshot.data as Map<String, dynamic>;
return ListView(
padding: EdgeInsets.all(16),
children: [
// 1. Stat Cards
Row(
children: [
_StatCard(
title: "Users",
value: "${stats['userCount']}",
color: Colors.blue
),
SizedBox(width: 16),
_StatCard(
title: "Notes",
value: "${stats['noteCount']}",
color: Colors.green
),
],
),
SizedBox(height: 24),
Text("Recent Users", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
// 2. User List (ตัวอย่าง)
// ในโค้ดจริงใช้ ListView.builder แยก
ListTile(
leading: CircleAvatar(child: Text("A")),
title: Text("Admin User"),
subtitle: Text("admin@example.com"),
trailing: Chip(label: Text("Admin"), backgroundColor: Colors.purple[100]),
),
],
);
},
),
);
}
}
class _StatCard extends StatelessWidget {
final String title;
final String value;
final Color color;
const _StatCard({required this.title, required this.value, required this.color});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Text(value, style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: color)),
Text(title, style: TextStyle(color: Colors.grey[700])),
],
),
),
);
}
}
📝 แบบฝึกหัด
โจทย์ที่ 1: Ban User Function
สิ่งที่ต้องทำ:
- Backend: เพิ่ม Route
POST /api/admin/ban/:id(เปลี่ยน status user เป็น banned) - Frontend: เพิ่มปุ่ม "แบน" สีแดงท้ายชื่อ user
- Middleware: แก้
auth.jsให้เช็คว่าif (user.status == 'banned')ห้ามเข้าระบบ
โจทย์ที่ 2: System Announcement
สิ่งที่ต้องทำ:
- Admin พิมพ์ข้อความประกาศ -> บันทึกลง DB
- User ทุกคนเปิดแอปมา -> เห็น Banner ประกาศด้านบนสุดของ Home
โจทย์ที่ 3: Charts
สิ่งที่ต้องทำ:
- ลง package
fl_chart - แสดงกราฟแท่งจำนวน Note ที่ถูกสร้างในรอบ 7 วัน
📌 สรุป
ระบบ Admin เป็นสิ่งที่ขาดไม่ได้สำหรับ Commercial App
- Security First: อย่าไว้ใจ Frontend, ต้องเช็ค Permission ที่ Backend เสมอ
- Monitoring: Dashboard ช่วยให้เรารู้สุขภาพของระบบ
- Control: ช่วยให้เราจัดการปัญหา (เช่น User ทำผิดกฎ) ได้ทันท่วงที
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 20: Export & Share Features เจาะลึกการทำรูประดับโปรและการแชร์ข้ามแพลตฟอร์ม