From 7a9030f9ef9ee6b00419c11fddefd81eeb1c6093 Mon Sep 17 00:00:00 2001 From: dangthaison12 Date: Fri, 14 Nov 2025 12:58:31 +0700 Subject: [PATCH 1/2] initial commit --- ...273\201 Xu\341\272\245t Implementation.md" | 923 ++++++++ document/Checklist Debug Backend Admin.md | 146 ++ ...72\277 Float - N\303\272t Import Paper.md" | 486 +++++ ...72\253n Test Admin Endpoints - Postman.md" | 635 ++++++ ...241o T\303\240i Kho\341\272\243n Admin.md" | 174 ++ ...LabVerse_Admin_API.postman_collection.json | 346 +++ ...73\231ng - Team v\303\240 Reading List.md" | 824 +++++++ ...\241n Gi\341\272\243i Quy\341\272\277t.md" | 324 +++ document/Test_Admin_Endpoints_SQL.sql | 147 ++ ...273\223ng S3 - Tr\303\254nh B\303\240y.md" | 195 ++ ...e Specification - Team and Reading List.md | 1938 +++++++++++++++++ .../com/se1853_jv/config/SecurityConfig.java | 18 + .../se1853_jv/controller/AdminController.java | 145 ++ .../dto/request/ChangeUserRoleRequest.java | 25 + .../response/AdminUserDetailsResponse.java | 137 ++ .../response/OverviewStatisticsResponse.java | 115 + .../se1853_jv/repository/TeamRepository.java | 16 + .../se1853_jv/repository/UserRepository.java | 23 + .../com/se1853_jv/service/AdminService.java | 25 + .../service/impl/AdminServiceImpl.java | 234 ++ .../resources/database/create_admin_user.sql | 57 + .../database/migration_add_admin_role.sql | 42 + web/src/App.tsx | 2 + web/src/components/AdminRoute.tsx | 32 + web/src/components/AppNavigation.tsx | 9 +- web/src/pages/Header.tsx | 8 +- web/src/pages/admin/Dashboard.tsx | 154 ++ .../admin/components/CollectionManagement.tsx | 240 ++ .../admin/components/StatisticsDashboard.tsx | 139 ++ .../pages/admin/components/UserManagement.tsx | 346 +++ web/src/services/admin.service.ts | 421 ++++ 31 files changed, 8324 insertions(+), 2 deletions(-) create mode 100644 "document/Admin Dashboard - \304\220\341\273\201 Xu\341\272\245t Implementation.md" create mode 100644 document/Checklist Debug Backend Admin.md create mode 100644 "document/C\306\241 Ch\341\272\277 Float - N\303\272t Import Paper.md" create mode 100644 "document/H\306\260\341\273\233ng D\341\272\253n Test Admin Endpoints - Postman.md" create mode 100644 "document/H\306\260\341\273\233ng D\341\272\253n T\341\272\241o T\303\240i Kho\341\272\243n Admin.md" create mode 100644 document/LabVerse_Admin_API.postman_collection.json create mode 100644 "document/M\303\264 t\341\272\243 Lu\341\273\223ng Ho\341\272\241t \304\220\341\273\231ng - Team v\303\240 Reading List.md" create mode 100644 "document/Reading List - B\303\240i To\303\241n Gi\341\272\243i Quy\341\272\277t.md" create mode 100644 document/Test_Admin_Endpoints_SQL.sql create mode 100644 "document/T\303\263m T\341\272\257t Lu\341\273\223ng S3 - Tr\303\254nh B\303\240y.md" create mode 100644 document/Use Case Specification - Team and Reading List.md create mode 100644 services/AccountService/src/main/java/com/se1853_jv/controller/AdminController.java create mode 100644 services/AccountService/src/main/java/com/se1853_jv/dto/request/ChangeUserRoleRequest.java create mode 100644 services/AccountService/src/main/java/com/se1853_jv/dto/response/AdminUserDetailsResponse.java create mode 100644 services/AccountService/src/main/java/com/se1853_jv/dto/response/OverviewStatisticsResponse.java create mode 100644 services/AccountService/src/main/java/com/se1853_jv/service/AdminService.java create mode 100644 services/AccountService/src/main/java/com/se1853_jv/service/impl/AdminServiceImpl.java create mode 100644 services/AccountService/src/main/resources/database/create_admin_user.sql create mode 100644 services/AccountService/src/main/resources/database/migration_add_admin_role.sql create mode 100644 web/src/components/AdminRoute.tsx create mode 100644 web/src/pages/admin/Dashboard.tsx create mode 100644 web/src/pages/admin/components/CollectionManagement.tsx create mode 100644 web/src/pages/admin/components/StatisticsDashboard.tsx create mode 100644 web/src/pages/admin/components/UserManagement.tsx create mode 100644 web/src/services/admin.service.ts diff --git "a/document/Admin Dashboard - \304\220\341\273\201 Xu\341\272\245t Implementation.md" "b/document/Admin Dashboard - \304\220\341\273\201 Xu\341\272\245t Implementation.md" new file mode 100644 index 00000000..7755f1e5 --- /dev/null +++ "b/document/Admin Dashboard - \304\220\341\273\201 Xu\341\272\245t Implementation.md" @@ -0,0 +1,923 @@ +# Admin Dashboard - Đề Xuất Implementation + +## Tổng Quan + +Tài liệu này đề xuất cách implement Admin Dashboard cho LabVerse dựa trên codebase hiện tại, bao gồm: +- Thêm role ADMIN +- Các tính năng admin phù hợp +- APIs cần thiết +- Frontend dashboard structure + +--- + +## 1. Phân Tích Codebase Hiện Tại + +### 1.1. Entities Chính + +**AccountService:** +- ✅ `User` - có `Role`, `isActive` +- ✅ `Role` - có thể mở rộng thêm ADMIN +- ✅ `Team` - quản lý teams +- ✅ `TeamMember` - quản lý members + +**PaperService:** +- ✅ `Paper` - research papers +- ✅ `Tag` - tags cho papers + +**GroupService:** +- ✅ `Collection` - shared collections +- ✅ `CollectionUser` - collection members + +**ReadingService:** +- ✅ `ReadingList` - reading lists + +**NotificationService:** +- ✅ `Notification` - user notifications + +### 1.2. Authentication & Authorization Hiện Tại + +- ✅ JWT Authentication +- ✅ `@PreAuthorize("isAuthenticated()")` +- ✅ `UserPrincipal` chứa user info +- ⚠️ Chưa có role-based authorization + +--- + +## 2. Đề Xuất Tính Năng Admin Dashboard + +### 2.1. User Management + +**Mục đích**: Quản lý tất cả users trong hệ thống + +**Tính năng:** +1. **View All Users** (Paginated) + - Danh sách users với filters (role, status, search) + - Hiển thị: email, name, role, status, created date + - Sort by: name, email, created date + +2. **User Details** + - View chi tiết user + - Xem papers của user + - Xem teams/collections user tham gia + - Xem activity history + +3. **Activate/Deactivate User** + - Ban/unban users + - Set `isActive = false` để disable account + +4. **Change User Role** + - Thay đổi role của user (PI, Researcher, Intern) + - Validation: Không thể change role của chính mình + +5. **Delete User** (Soft delete) + - Set `isActive = false` + - Hoặc hard delete (cẩn thận với foreign keys) + +### 2.2. Paper Management + +**Mục đích**: Quản lý và kiểm duyệt papers + +**Tính năng:** +1. **View All Papers** (Paginated) + - Danh sách tất cả papers + - Filters: author, journal, year, created date + - Search by title, DOI + +2. **Paper Details** + - View chi tiết paper + - Xem metadata, tags, keywords + - Xem user đã upload + +3. **Delete Paper** + - Xóa papers không phù hợp + - Xóa papers vi phạm bản quyền + - Warning: Có thể ảnh hưởng đến collections/reading lists + +4. **Paper Statistics** + - Tổng số papers + - Papers theo tháng/năm + - Top authors, journals + - Papers per user + +### 2.3. Team Management + +**Mục đích**: Quản lý teams và members + +**Tính năng:** +1. **View All Teams** (Paginated) + - Danh sách tất cả teams + - Filters: privacy, research field + - Hiển thị: name, privacy, member count, paper count + +2. **Team Details** + - View chi tiết team + - Xem members và roles + - Xem papers trong team + +3. **Delete Team** + - Xóa teams không phù hợp + - Warning: Members sẽ mất access + +### 2.4. Collection Management + +**Tính năng tương tự Team Management:** +- View all collections +- View collection details +- Delete collections + +### 2.5. Reading List Management + +**Tính năng:** +- View all reading lists +- View list details +- Delete lists + +### 2.6. Statistics Dashboard + +**Mục đích**: Tổng quan về hệ thống + +**Metrics:** +1. **User Statistics** + - Total users + - Active users (isActive = true) + - Users by role (PI, Researcher, Intern) + - New users this month/year + - User growth chart + +2. **Paper Statistics** + - Total papers + - Papers uploaded this month/year + - Papers by year (publication year) + - Top journals + - Top authors + +3. **Team Statistics** + - Total teams + - Public vs Private teams + - Average members per team + - Teams by research field + +4. **Collection Statistics** + - Total collections + - Average papers per collection + - Collections by access level + +5. **Reading List Statistics** + - Total reading lists + - Average papers per list + - Average members per list + +6. **Activity Statistics** + - Papers uploaded per day/week/month + - Teams created per month + - User registrations per month + +### 2.7. System Settings (Optional) + +**Tính năng:** +- Configure system-wide settings +- Manage tags (create, edit, delete) +- Manage institutions +- Email templates +- Feature flags + +--- + +## 3. Implementation Plan + +### 3.1. Backend - Thêm Role ADMIN + +#### Step 1: Update Role Model + +**File**: `services/AccountService/src/main/java/com/se1853_jv/model/Role.java` + +Không cần thay đổi, chỉ cần thêm role "ADMIN" vào database. + +#### Step 2: Create Admin Role in Database + +**File**: `services/AccountService/src/main/resources/database/init.sql` hoặc migration script + +```sql +-- Insert ADMIN role if not exists +IF NOT EXISTS (SELECT 1 FROM Role WHERE name = 'ADMIN') +BEGIN + INSERT INTO Role (id, name) VALUES (NEWID(), 'ADMIN'); +END +``` + +#### Step 3: Create Admin User (Manual hoặc Script) + +```sql +-- Create admin user (password: admin123 - should be hashed) +-- Note: Use BCrypt hash in production +INSERT INTO Users (id, email, username, full_name, password, created_date, updated_date, Roleid, is_active) +SELECT + NEWID(), + 'admin@labverse.com', + 'admin', + 'System Administrator', + '$2a$10$...', -- BCrypt hash of password + GETDATE(), + GETDATE(), + (SELECT id FROM Role WHERE name = 'ADMIN'), + 1; +``` + +#### Step 4: Create Admin Controller + +**File**: `services/AccountService/src/main/java/com/se1853_jv/controller/AdminController.java` + +```java +package com.se1853_jv.controller; + +import com.se1853_jv.dto.request.*; +import com.se1853_jv.dto.response.*; +import com.se1853_jv.service.AdminService; +import com.se1853_jv.util.IdEncoder; +import jakarta.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/v1/api/admin") +@PreAuthorize("hasRole('ADMIN')") +public class AdminController { + + private final AdminService adminService; + + // ========== USER MANAGEMENT ========== + + @GetMapping("/users") + public ResponseEntity getAllUsers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String search, + @RequestParam(required = false) String role, + @RequestParam(required = false) Boolean isActive) { + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getAllUsers(page, size, search, role, isActive) + )); + } + + @GetMapping("/users/{id}") + public ResponseEntity getUserDetails(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getUserDetails(decodedId) + )); + } + + @PatchMapping("/users/{id}/activate") + public ResponseEntity activateUser(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + adminService.activateUser(decodedId); + return ResponseEntity.ok(WrapperApiResponse.success("User activated successfully")); + } + + @PatchMapping("/users/{id}/deactivate") + public ResponseEntity deactivateUser(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + adminService.deactivateUser(decodedId); + return ResponseEntity.ok(WrapperApiResponse.success("User deactivated successfully")); + } + + @PatchMapping("/users/{id}/role") + public ResponseEntity changeUserRole( + @PathVariable String id, + @Valid @RequestBody ChangeUserRoleRequest request) { + String decodedId = IdEncoder.decode(id); + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.changeUserRole(decodedId, request.getRoleId()) + )); + } + + @DeleteMapping("/users/{id}") + public ResponseEntity deleteUser(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + adminService.deleteUser(decodedId); + return ResponseEntity.ok(WrapperApiResponse.success("User deleted successfully")); + } + + // ========== PAPER MANAGEMENT ========== + + @GetMapping("/papers") + public ResponseEntity getAllPapers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String search, + @RequestParam(required = false) String author, + @RequestParam(required = false) String journal) { + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getAllPapers(page, size, search, author, journal) + )); + } + + @GetMapping("/papers/{id}") + public ResponseEntity getPaperDetails(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getPaperDetails(decodedId) + )); + } + + @DeleteMapping("/papers/{id}") + public ResponseEntity deletePaper(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + adminService.deletePaper(decodedId); + return ResponseEntity.ok(WrapperApiResponse.success("Paper deleted successfully")); + } + + // ========== TEAM MANAGEMENT ========== + + @GetMapping("/teams") + public ResponseEntity getAllTeams( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String search, + @RequestParam(required = false) String privacy) { + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getAllTeams(page, size, search, privacy) + )); + } + + @DeleteMapping("/teams/{id}") + public ResponseEntity deleteTeam(@PathVariable String id) { + String decodedId = IdEncoder.decode(id); + adminService.deleteTeam(decodedId); + return ResponseEntity.ok(WrapperApiResponse.success("Team deleted successfully")); + } + + // ========== STATISTICS ========== + + @GetMapping("/statistics/overview") + public ResponseEntity getOverviewStatistics() { + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getOverviewStatistics() + )); + } + + @GetMapping("/statistics/users") + public ResponseEntity getUserStatistics( + @RequestParam(required = false) String period) { // daily, weekly, monthly + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getUserStatistics(period) + )); + } + + @GetMapping("/statistics/papers") + public ResponseEntity getPaperStatistics( + @RequestParam(required = false) String period) { + return ResponseEntity.ok(WrapperApiResponse.success( + adminService.getPaperStatistics(period) + )); + } +} +``` + +#### Step 5: Create AdminService + +**File**: `services/AccountService/src/main/java/com/se1853_jv/service/AdminService.java` + +```java +package com.se1853_jv.service; + +import com.se1853_jv.dto.response.*; +import org.springframework.data.domain.Page; + +public interface AdminService { + // User Management + Page getAllUsers(int page, int size, String search, String role, Boolean isActive); + AdminUserDetailsResponse getUserDetails(String userId); + void activateUser(String userId); + void deactivateUser(String userId); + UserResponse changeUserRole(String userId, String roleId); + void deleteUser(String userId); + + // Paper Management (cần call PaperService) + Page getAllPapers(int page, int size, String search, String author, String journal); + PaperResponse getPaperDetails(String paperId); + void deletePaper(String paperId); + + // Team Management + Page getAllTeams(int page, int size, String search, String privacy); + void deleteTeam(String teamId); + + // Statistics + OverviewStatisticsResponse getOverviewStatistics(); + UserStatisticsResponse getUserStatistics(String period); + PaperStatisticsResponse getPaperStatistics(String period); +} +``` + +#### Step 6: Update SecurityConfig + +**File**: `services/AccountService/src/main/java/com/se1853_jv/config/SecurityConfig.java` + +```java +// Thêm method để check role ADMIN +@Bean +public MethodSecurityExpressionHandler methodSecurityExpressionHandler() { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + handler.setRoleHierarchy(roleHierarchy()); + return handler; +} + +@Bean +public RoleHierarchy roleHierarchy() { + RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); + hierarchy.setHierarchy("ADMIN > PI > RESEARCHER > INTERN"); + return hierarchy; +} +``` + +### 3.2. Frontend - Admin Dashboard + +#### Step 1: Create Admin Route Protection + +**File**: `web/src/components/AdminRoute.tsx` + +```typescript +import { Navigate } from "react-router-dom"; +import { useAuth } from "@/contexts/AuthContext"; + +interface AdminRouteProps { + children: React.ReactNode; +} + +const AdminRoute = ({ children }: AdminRouteProps) => { + const { user, isLoading } = useAuth(); + + if (isLoading) { + return
Loading...
; + } + + if (!user) { + return ; + } + + // Check if user has ADMIN role + if (user.role?.name !== "ADMIN") { + return ; + } + + return <>{children}; +}; + +export default AdminRoute; +``` + +#### Step 2: Create Admin Dashboard Page + +**File**: `web/src/pages/admin/Dashboard.tsx` + +```typescript +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Users, FileText, UsersRound, BarChart3 } from "lucide-react"; +import Header from "@/pages/Header"; +import AdminRoute from "@/components/AdminRoute"; +import UserManagement from "./components/UserManagement"; +import PaperManagement from "./components/PaperManagement"; +import TeamManagement from "./components/TeamManagement"; +import StatisticsDashboard from "./components/StatisticsDashboard"; +import { getAdminStatistics } from "@/services/admin.service"; + +const AdminDashboard = () => { + const { data: stats } = useQuery({ + queryKey: ["admin-statistics"], + queryFn: getAdminStatistics, + }); + + return ( + +
+
+
+
+

Admin Dashboard

+

Manage users, papers, teams, and view system statistics

+
+ + {/* Statistics Cards */} +
+ + + Total Users + + + +
{stats?.totalUsers || 0}
+

+ {stats?.activeUsers || 0} active +

+
+
+ + + + Total Papers + + + +
{stats?.totalPapers || 0}
+

+ {stats?.papersThisMonth || 0} this month +

+
+
+ + + + Total Teams + + + +
{stats?.totalTeams || 0}
+

+ {stats?.publicTeams || 0} public +

+
+
+ + + + Statistics + + + +
View
+

+ Detailed analytics +

+
+
+
+ + {/* Management Tabs */} + + + Users + Papers + Teams + Statistics + + + + + + + + + + + + + + + + + + +
+
+
+ ); +}; + +export default AdminDashboard; +``` + +#### Step 3: Create Admin Service + +**File**: `web/src/services/admin.service.ts` + +```typescript +import { BASE_API_URL, METHOD } from "@/type/constant"; + +const ADMIN_SERVICE_PREDICATE = "/account-service/v1/api/admin"; + +export interface AdminUser { + id: string; + email: string; + username: string; + fullName: string; + role: { + id: string; + name: string; + }; + isActive: boolean; + createdDate: string; +} + +export interface AdminStatistics { + totalUsers: number; + activeUsers: number; + totalPapers: number; + papersThisMonth: number; + totalTeams: number; + publicTeams: number; + totalCollections: number; + totalReadingLists: number; +} + +export const getAdminUsers = async ( + page: number = 0, + size: number = 20, + search?: string, + role?: string, + isActive?: boolean +) => { + const params = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + }); + if (search) params.append("search", search); + if (role) params.append("role", role); + if (isActive !== undefined) params.append("isActive", isActive.toString()); + + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/users?${params}`, + { + method: METHOD.GET, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const activateUser = async (userId: string) => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/users/${userId}/activate`, + { + method: METHOD.PATCH, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const deactivateUser = async (userId: string) => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/users/${userId}/deactivate`, + { + method: METHOD.PATCH, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const changeUserRole = async (userId: string, roleId: string) => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/users/${userId}/role`, + { + method: METHOD.PATCH, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify({ roleId }), + } + ); + return response.json(); +}; + +export const deleteUser = async (userId: string) => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/users/${userId}`, + { + method: METHOD.DELETE, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const getAdminPapers = async ( + page: number = 0, + size: number = 20, + search?: string, + author?: string, + journal?: string +) => { + const params = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + }); + if (search) params.append("search", search); + if (author) params.append("author", author); + if (journal) params.append("journal", journal); + + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/papers?${params}`, + { + method: METHOD.GET, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const deletePaper = async (paperId: string) => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/papers/${paperId}`, + { + method: METHOD.DELETE, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + return response.json(); +}; + +export const getAdminStatistics = async (): Promise => { + const response = await fetch( + `${BASE_API_URL}${ADMIN_SERVICE_PREDICATE}/statistics/overview`, + { + method: METHOD.GET, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + const data = await response.json(); + return data.data; +}; +``` + +#### Step 4: Add Admin Link to Navigation + +**File**: `web/src/components/Navigation.tsx` hoặc `AppNavigation.tsx` + +```typescript +// Thêm vào navigation menu +{user?.role?.name === "ADMIN" && ( + + Admin Dashboard + +)} +``` + +#### Step 5: Add Route + +**File**: `web/src/App.tsx` hoặc router file + +```typescript +import AdminDashboard from "./pages/admin/Dashboard"; + +// Trong routes +} /> +``` + +--- + +## 4. API Endpoints Summary + +### User Management +- `GET /v1/api/admin/users` - Get all users (paginated, filtered) +- `GET /v1/api/admin/users/{id}` - Get user details +- `PATCH /v1/api/admin/users/{id}/activate` - Activate user +- `PATCH /v1/api/admin/users/{id}/deactivate` - Deactivate user +- `PATCH /v1/api/admin/users/{id}/role` - Change user role +- `DELETE /v1/api/admin/users/{id}` - Delete user + +### Paper Management +- `GET /v1/api/admin/papers` - Get all papers (paginated, filtered) +- `GET /v1/api/admin/papers/{id}` - Get paper details +- `DELETE /v1/api/admin/papers/{id}` - Delete paper + +### Team Management +- `GET /v1/api/admin/teams` - Get all teams (paginated, filtered) +- `DELETE /v1/api/admin/teams/{id}` - Delete team + +### Statistics +- `GET /v1/api/admin/statistics/overview` - Get overview statistics +- `GET /v1/api/admin/statistics/users` - Get user statistics +- `GET /v1/api/admin/statistics/papers` - Get paper statistics + +--- + +## 5. Database Changes + +### 5.1. Add ADMIN Role + +```sql +-- Migration script +IF NOT EXISTS (SELECT 1 FROM Role WHERE name = 'ADMIN') +BEGIN + INSERT INTO Role (id, name) VALUES (NEWID(), 'ADMIN'); +END +``` + +### 5.2. Create Admin User (Optional - có thể tạo manual) + +```sql +-- Create admin user +-- Password should be hashed with BCrypt +-- Example: password "admin123" -> BCrypt hash +DECLARE @adminRoleId UNIQUEIDENTIFIER = (SELECT id FROM Role WHERE name = 'ADMIN'); + +IF NOT EXISTS (SELECT 1 FROM Users WHERE email = 'admin@labverse.com') +BEGIN + INSERT INTO Users (id, email, username, full_name, password, created_date, updated_date, Roleid, is_active) + VALUES ( + NEWID(), + 'admin@labverse.com', + 'admin', + 'System Administrator', + '$2a$10$...', -- Replace with actual BCrypt hash + GETDATE(), + GETDATE(), + @adminRoleId, + 1 + ); +END +``` + +--- + +## 6. Security Considerations + +### 6.1. Role-Based Access Control + +- ✅ Sử dụng `@PreAuthorize("hasRole('ADMIN')")` trên tất cả admin endpoints +- ✅ Check role trong frontend trước khi hiển thị admin links +- ✅ Validate role trong JWT token + +### 6.2. Validation + +- ✅ Không cho phép admin delete chính mình +- ✅ Không cho phép admin change role của chính mình +- ✅ Validate foreign keys trước khi delete (users, papers, teams) + +### 6.3. Audit Logging (Optional) + +- Log tất cả admin actions +- Track: who, what, when, why + +--- + +## 7. Implementation Priority + +### Phase 1: Core Features (MVP) +1. ✅ Add ADMIN role +2. ✅ User Management (view, activate/deactivate) +3. ✅ Basic Statistics Dashboard +4. ✅ Admin route protection + +### Phase 2: Extended Features +1. ✅ Paper Management +2. ✅ Team Management +3. ✅ Change User Role +4. ✅ Advanced Statistics + +### Phase 3: Advanced Features +1. ✅ Collection Management +2. ✅ Reading List Management +3. ✅ System Settings +4. ✅ Audit Logging + +--- + +## 8. Testing Checklist + +- [ ] Admin can access admin dashboard +- [ ] Non-admin cannot access admin dashboard +- [ ] Admin can view all users +- [ ] Admin can activate/deactivate users +- [ ] Admin can change user roles +- [ ] Admin can view all papers +- [ ] Admin can delete papers +- [ ] Admin can view all teams +- [ ] Admin can delete teams +- [ ] Statistics display correctly +- [ ] Pagination works +- [ ] Filters work +- [ ] Search works + +--- + +**Tài liệu này cung cấp roadmap chi tiết để implement Admin Dashboard cho LabVerse.** + diff --git a/document/Checklist Debug Backend Admin.md b/document/Checklist Debug Backend Admin.md new file mode 100644 index 00000000..808d665a --- /dev/null +++ b/document/Checklist Debug Backend Admin.md @@ -0,0 +1,146 @@ +# Checklist Debug Backend Admin + +## 🔍 Kiểm Tra Lỗi 500 + +### Step 1: Kiểm Tra Database Connection +```sql +-- Test connection +SELECT @@VERSION; +``` + +### Step 2: Kiểm Tra Role Table +```sql +-- Xem tất cả roles +SELECT * FROM Role; + +-- Nếu thiếu ADMIN role: +INSERT INTO Role (id, name) VALUES (NEWID(), 'ADMIN'); +``` + +### Step 3: Kiểm Tra Admin User +```sql +-- Xem users và roles +SELECT + u.id, + u.email, + u.username, + u.isActive, + r.id as role_id, + r.name as role_name +FROM Users u +LEFT JOIN Role r ON u.Roleid = r.id; + +-- Kiểm tra admin user cụ thể +SELECT u.*, r.name as role +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE u.email = 'admin@labverse.com'; +``` + +### Step 4: Kiểm Tra Users Không Có Role +```sql +-- Users không có role sẽ gây lỗi +SELECT u.* +FROM Users u +WHERE u.Roleid IS NULL; + +-- Fix: Gán role mặc định +UPDATE Users +SET Roleid = (SELECT TOP 1 id FROM Role WHERE name = 'RESEARCHER') +WHERE Roleid IS NULL; +``` + +### Step 5: Test Query Trực Tiếp +```sql +-- Test query với NULL +DECLARE @search NVARCHAR(255) = NULL; +DECLARE @role NVARCHAR(255) = NULL; +DECLARE @isActive BIT = NULL; + +SELECT u.* +FROM Users u +LEFT JOIN Role r ON u.Roleid = r.id +WHERE (@search IS NULL OR @search = '' OR + LOWER(u.email) LIKE LOWER('%' + @search + '%') OR + LOWER(u.username) LIKE LOWER('%' + @search + '%') OR + LOWER(u.full_name) LIKE LOWER('%' + @search + '%')) +AND (@role IS NULL OR @role = '' OR r.name = @role) +AND (@isActive IS NULL OR u.is_active = @isActive); +``` + +### Step 6: Kiểm Tra Application Logs +Xem logs trong console để tìm stack trace cụ thể: +``` +ERROR ... Exception: ... +at com.se1853_jv.service.impl.AdminServiceImpl.getAllUsers(...) +``` + +--- + +## 🛠️ Common Fixes + +### Fix 1: Query Syntax Error +Nếu lỗi về CONCAT, có thể SQL Server version không support: +```java +// Thay CONCAT bằng + operator (SQL Server) +LOWER(u.email) LIKE LOWER('%' + :search + '%') +``` + +### Fix 2: NullPointerException +Đảm bảo tất cả users đều có role: +```sql +-- Check và fix +UPDATE Users +SET Roleid = (SELECT TOP 1 id FROM Role WHERE name = 'RESEARCHER') +WHERE Roleid IS NULL; +``` + +### Fix 3: LazyInitializationException +Role đã có `FetchType.EAGER` nên không có vấn đề này. + +### Fix 4: Transaction Issue +Đảm bảo `@Transactional` được dùng đúng: +- `getAllUsers()` không cần `@Transactional` (read-only) +- Các methods modify cần `@Transactional` + +--- + +## 📋 Test Checklist + +- [ ] Database connection OK +- [ ] ADMIN role exists +- [ ] Admin user exists và có ADMIN role +- [ ] Tất cả users đều có role (không có NULL) +- [ ] Query test trực tiếp trong SQL Server OK +- [ ] Backend service đang chạy +- [ ] JWT token hợp lệ +- [ ] Authorization header đúng format + +--- + +## 🧪 Quick Test SQL + +```sql +-- 1. Check roles +SELECT * FROM Role; + +-- 2. Check admin user +SELECT u.email, r.name +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE r.name = 'ADMIN'; + +-- 3. Check users without role +SELECT COUNT(*) as users_without_role +FROM Users +WHERE Roleid IS NULL; + +-- 4. Test query +SELECT COUNT(*) +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE (NULL IS NULL OR NULL = '' OR 1=1) +AND (NULL IS NULL OR NULL = '' OR 1=1) +AND (NULL IS NULL OR u.is_active = NULL); +``` + diff --git "a/document/C\306\241 Ch\341\272\277 Float - N\303\272t Import Paper.md" "b/document/C\306\241 Ch\341\272\277 Float - N\303\272t Import Paper.md" new file mode 100644 index 00000000..5db6a757 --- /dev/null +++ "b/document/C\306\241 Ch\341\272\277 Float - N\303\272t Import Paper.md" @@ -0,0 +1,486 @@ +# Cơ Chế Float của Nút Import Paper (FAB) + +## Tổng Quan + +Nút Import Paper sử dụng **FloatingActionButton (FAB)** với tính năng **drag-and-drop** (kéo thả) để user có thể di chuyển nút đến vị trí mong muốn trên màn hình. Vị trí được lưu lại và khôi phục khi mở lại app. + +--- + +## 1. Cơ Chế Float - Drag and Drop + +### 1.1. Khái Niệm + +**Float** ở đây có nghĩa là: +- ✅ **Floating**: Nút nổi trên màn hình, không bị scroll theo content +- ✅ **Draggable**: User có thể kéo thả nút đến vị trí bất kỳ +- ✅ **Persistent**: Vị trí được lưu lại và khôi phục khi mở lại app + +### 1.2. Cách Hoạt Động + +``` +1. User touch vào FAB + ↓ +2. Di chuyển ngón tay (drag) + ↓ +3. FAB di chuyển theo ngón tay + ↓ +4. Thả ngón tay (drop) + ↓ +5. Lưu vị trí mới vào SharedPreferences + ↓ +6. Lần sau mở app → Load vị trí đã lưu +``` + +--- + +## 2. Implementation Chi Tiết + +### 2.1. XML Layout + +```xml + + +``` + +**Vị trí mặc định:** +- **Bottom Right**: Góc dưới bên phải +- **Margin**: 24dp từ cạnh phải, 80dp từ bottom navbar + +--- + +### 2.2. Setup Drag and Drop + +#### Bước 1: Remove Constraints + +```java +// Remove constraints để cho phép free movement +ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) fabImportPaper.getLayoutParams(); +params.leftToLeft = ConstraintLayout.LayoutParams.UNSET; +params.rightToRight = ConstraintLayout.LayoutParams.UNSET; +params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET; +params.topToTop = ConstraintLayout.LayoutParams.UNSET; +fabImportPaper.setLayoutParams(params); +``` + +**Mục đích**: Bỏ các ràng buộc của ConstraintLayout để có thể di chuyển tự do bằng `setX()` và `setY()`. + +#### Bước 2: Set Default Position + +```java +// Tính toán vị trí mặc định (bottom right) +float defaultX = screenWidth - fabWidth - dpToPx(24); // 24dp margin +float defaultY = screenHeight - fabHeight - bottomNavHeight - dpToPx(80); // 80dp margin + +// Set vị trí +fabImportPaper.setX(defaultX); +fabImportPaper.setY(defaultY); +``` + +--- + +### 2.3. Touch Event Handling + +#### ACTION_DOWN (Bắt đầu touch) + +```java +case MotionEvent.ACTION_DOWN: + // Lưu vị trí touch ban đầu + initialTouchX = event.getRawX(); + initialTouchY = event.getRawY(); + + // Tính offset giữa FAB và touch point + dX = view.getX() - event.getRawX(); + dY = view.getY() - event.getRawY(); + + isDragging = false; + return false; // Không consume để cho phép click hoạt động +``` + +**Mục đích**: +- Lưu vị trí touch ban đầu để tính toán khoảng cách di chuyển +- Tính offset để FAB di chuyển chính xác theo ngón tay + +#### ACTION_MOVE (Di chuyển) + +```java +case MotionEvent.ACTION_MOVE: + // Tính khoảng cách di chuyển + float deltaX = Math.abs(event.getRawX() - initialTouchX); + float deltaY = Math.abs(event.getRawY() - initialTouchY); + + // Chỉ bắt đầu drag nếu di chuyển > threshold (10dp) + if (deltaX > DRAG_THRESHOLD || deltaY > DRAG_THRESHOLD) { + isDragging = true; + + // Ngăn parent view intercept touch events + view.getParent().requestDisallowInterceptTouchEvent(true); + + // Cancel click event + view.setPressed(false); + view.cancelLongPress(); + + // Tính vị trí mới + float newX = event.getRawX() + dX; + float newY = event.getRawY() + dY; + + // Giới hạn trong màn hình + newX = Math.max(0, Math.min(newX, screenWidth - fabWidth)); + newY = Math.max(statusBarHeight, Math.min(newY, screenHeight - fabHeight - bottomNavHeight)); + + // Cập nhật vị trí FAB + view.setX(newX); + view.setY(newY); + + return true; // Consume event + } + return false; // Chưa đủ để drag, cho phép click +``` + +**Điểm quan trọng:** + +1. **DRAG_THRESHOLD = 10dp**: + - Chỉ bắt đầu drag khi di chuyển > 10dp + - Tránh nhầm lẫn giữa click và drag + +2. **Boundary Constraints**: + - Giới hạn FAB trong màn hình + - Không cho phép di chuyển ra ngoài màn hình + - Tránh che bottom navbar + +3. **Click vs Drag**: + - Nếu di chuyển < threshold → Click event + - Nếu di chuyển > threshold → Drag event (cancel click) + +#### ACTION_UP / ACTION_CANCEL (Kết thúc) + +```java +case MotionEvent.ACTION_UP: +case MotionEvent.ACTION_CANCEL: + // Cho phép parent intercept lại + view.getParent().requestDisallowInterceptTouchEvent(false); + + if (isDragging) { + // Lưu vị trí sau khi drag + saveFabPosition(view.getX(), view.getY()); + isDragging = false; + view.setPressed(false); + return true; // Consume để prevent click + } + + isDragging = false; + return false; // Cho phép click event +``` + +--- + +### 2.4. Lưu và Load Vị Trí + +#### Lưu Vị Trí + +```java +private void saveFabPosition(float x, float y) { + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putFloat(KEY_FAB_X, x); + editor.putFloat(KEY_FAB_Y, y); + editor.apply(); // Async save +} +``` + +**Lưu vào**: `SharedPreferences` với key `"FeedActivityPrefs"` + +#### Load Vị Trí + +```java +private void loadFabPosition() { + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + float savedX = prefs.getFloat(KEY_FAB_X, -1); + float savedY = prefs.getFloat(KEY_FAB_Y, -1); + + if (savedX >= 0 && savedY >= 0) { + // Validate và apply vị trí đã lưu + float x = Math.max(0, Math.min(savedX, screenWidth - fabWidth)); + float y = Math.max(statusBarHeight, Math.min(savedY, screenHeight - fabHeight - bottomNavHeight)); + + fabImportPaper.setX(x); + fabImportPaper.setY(y); + } else { + // Không có vị trí đã lưu → dùng default position + } +} +``` + +**Validation**: Đảm bảo vị trí đã lưu vẫn hợp lệ khi mở lại (màn hình có thể đã thay đổi kích thước). + +--- + +## 3. Click Event Handling + +### 3.1. Phân Biệt Click vs Drag + +```java +fabImportPaper.setOnClickListener(v -> { + // Chỉ trigger click nếu KHÔNG đang drag + if (!isDragging) { + Intent intent = new Intent(FeedActivity.this, ImportPaperManuallyActivity.class); + startActivityForResult(intent, REQUEST_CODE_IMPORT_PAPER); + } +}); +``` + +**Logic:** +- Nếu `isDragging = false` → Click event → Mở ImportPaperManuallyActivity +- Nếu `isDragging = true` → Không trigger click → Chỉ drag + +### 3.2. Click Event Flow + +``` +User tap FAB + ↓ +ACTION_DOWN → isDragging = false + ↓ +ACTION_MOVE → Check distance + ↓ + ├─ Distance < 10dp → Click (isDragging = false) + └─ Distance > 10dp → Drag (isDragging = true, cancel click) + ↓ +ACTION_UP + ↓ + ├─ isDragging = true → Save position, prevent click + └─ isDragging = false → Trigger onClick → Open ImportPaperManuallyActivity +``` + +--- + +## 4. Visibility Management + +### 4.1. Tab-Based Visibility + +```java +// Ẩn FAB khi ở tab Teams (position 2) +tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + int position = tab.getPosition(); + FloatingActionButton fab = findViewById(R.id.fabImportPaper); + if (fab != null) { + fab.setVisibility(position == 2 ? View.GONE : View.VISIBLE); + } + } +}); +``` + +**Logic:** +- **Tab 0 (Discovery)**: Hiển thị FAB +- **Tab 1 (My Papers)**: Hiển thị FAB +- **Tab 2 (Teams)**: Ẩn FAB + +**Lý do**: Tab Teams không cần import paper (chỉ quản lý teams). + +--- + +## 5. Boundary Constraints + +### 5.1. Screen Bounds + +```java +// Giới hạn trong màn hình +int screenWidth = rootLayout.getWidth(); +int screenHeight = rootLayout.getHeight(); +int statusBarHeight = displayRect.top; +int bottomNavHeight = bottomNav.getHeight(); + +// Constrain +newX = Math.max(0, Math.min(newX, screenWidth - fabWidth)); +newY = Math.max(statusBarHeight, Math.min(newY, screenHeight - fabHeight - bottomNavHeight)); +``` + +**Constraints:** +- **Left**: `x >= 0` +- **Right**: `x <= screenWidth - fabWidth` +- **Top**: `y >= statusBarHeight` +- **Bottom**: `y <= screenHeight - fabHeight - bottomNavHeight` + +--- + +## 6. Luồng Hoạt Động Tổng Thể + +### 6.1. Khi App Khởi Động + +``` +1. onCreate() → setupFabDragAndDrop() + ↓ +2. Wait for layout measurement + ↓ +3. Remove constraints + ↓ +4. Set default position (bottom right) + ↓ +5. Load saved position (nếu có) + ↓ +6. Apply position → FAB hiển thị +``` + +### 6.2. Khi User Drag FAB + +``` +1. Touch FAB → ACTION_DOWN + ↓ +2. Move finger → ACTION_MOVE + ↓ +3. Check distance > 10dp? + ├─ NO → Continue (allow click) + └─ YES → Start dragging + ↓ +4. Calculate new position + ↓ +5. Constrain within screen bounds + ↓ +6. Update FAB position (setX, setY) + ↓ +7. Release finger → ACTION_UP + ↓ +8. Save position to SharedPreferences +``` + +### 6.3. Khi User Click FAB + +``` +1. Touch FAB → ACTION_DOWN + ↓ +2. Move finger < 10dp → ACTION_MOVE (not dragging) + ↓ +3. Release finger → ACTION_UP + ↓ +4. isDragging = false → Trigger onClick + ↓ +5. Open ImportPaperManuallyActivity +``` + +--- + +## 7. Technical Details + +### 7.1. Coordinate System + +- **getRawX() / getRawY()**: Tọa độ tuyệt đối trên màn hình +- **getX() / getY()**: Tọa độ tương đối của view +- **setX() / setY()**: Set vị trí của view (sau khi remove constraints) + +### 7.2. Touch Event Consumption + +- **return true**: Consume event → Không propagate +- **return false**: Không consume → Cho phép click event + +### 7.3. Request Disallow Intercept + +```java +view.getParent().requestDisallowInterceptTouchEvent(true); +``` + +**Mục đích**: Ngăn parent view (ScrollView, RecyclerView) intercept touch events khi đang drag FAB. + +--- + +## 8. Use Cases + +### Use Case 1: User Drag FAB +``` +Scenario: User muốn di chuyển FAB sang trái để không che content + +Steps: +1. Touch và giữ FAB +2. Kéo sang trái +3. Thả tay +4. FAB ở vị trí mới +5. Lần sau mở app → FAB vẫn ở vị trí đó +``` + +### Use Case 2: User Click FAB +``` +Scenario: User muốn import paper mới + +Steps: +1. Tap FAB (không kéo) +2. Mở ImportPaperManuallyActivity +3. Upload PDF +4. Quay lại → FAB vẫn ở vị trí cũ +``` + +### Use Case 3: User Switch Tabs +``` +Scenario: User chuyển sang tab Teams + +Steps: +1. Click tab Teams +2. FAB tự động ẩn (View.GONE) +3. Click tab My Papers +4. FAB tự động hiện lại (View.VISIBLE) +``` + +--- + +## 9. Câu Trả Lời Ngắn Gọn + +**"Cơ chế float của nút import paper hoạt động như thế nào?"** + +**Trả lời:** + +"Nút Import Paper sử dụng FloatingActionButton (FAB) với tính năng drag-and-drop: + +1. **Floating**: Nút nổi trên màn hình, không bị scroll theo content + +2. **Draggable**: + - User có thể kéo thả nút đến vị trí bất kỳ + - Sử dụng TouchEvent handling (ACTION_DOWN, ACTION_MOVE, ACTION_UP) + - Có threshold 10dp để phân biệt click và drag + +3. **Persistent**: + - Vị trí được lưu vào SharedPreferences + - Khôi phục vị trí khi mở lại app + +4. **Boundary Constraints**: + - Giới hạn FAB trong màn hình + - Tránh che status bar và bottom navbar + +5. **Click vs Drag**: + - Di chuyển < 10dp → Click event → Mở ImportPaperManuallyActivity + - Di chuyển > 10dp → Drag event → Di chuyển FAB + +**Implementation**: Remove constraints của ConstraintLayout, sử dụng setX()/setY() để di chuyển tự do." + +--- + +## 10. Code Summary + +### Key Components: + +1. **FloatingActionButton**: Material Design component +2. **TouchEvent Listener**: Handle drag and drop +3. **SharedPreferences**: Lưu vị trí +4. **ConstraintLayout**: Layout container (constraints được remove để cho phép free movement) +5. **Coordinate Calculation**: Tính toán vị trí dựa trên screen bounds + +### Key Methods: + +- `setupFabDragAndDrop()`: Setup drag and drop functionality +- `saveFabPosition()`: Lưu vị trí vào SharedPreferences +- `loadFabPosition()`: Load vị trí từ SharedPreferences +- `onTouch()`: Handle touch events +- `updateFabVisibility()`: Show/hide FAB based on tab + +--- + +**Tài liệu này giải thích chi tiết cơ chế float/drag-and-drop của nút Import Paper trong Android app.** + diff --git "a/document/H\306\260\341\273\233ng D\341\272\253n Test Admin Endpoints - Postman.md" "b/document/H\306\260\341\273\233ng D\341\272\253n Test Admin Endpoints - Postman.md" new file mode 100644 index 00000000..c5e0e0e2 --- /dev/null +++ "b/document/H\306\260\341\273\233ng D\341\272\253n Test Admin Endpoints - Postman.md" @@ -0,0 +1,635 @@ +# Hướng Dẫn Test Admin Endpoints Qua Postman + +## 📋 Mục Lục +1. [Chuẩn Bị](#chuẩn-bị) +2. [Lấy JWT Token](#lấy-jwt-token) +3. [Test User Management Endpoints](#test-user-management-endpoints) +4. [Test Statistics Endpoints](#test-statistics-endpoints) +5. [Troubleshooting](#troubleshooting) + +--- + +## 🔧 Chuẩn Bị + +### 1. Base URL +``` +http://localhost:8080/account-service +``` + +### 2. Tạo Admin User +Trước khi test, đảm bảo bạn đã có admin user: +- Chạy script: `services/AccountService/src/main/resources/database/create_admin_user.sql` +- Hoặc đăng ký user và update role thành ADMIN trong database + +### 3. Collection Variables trong Postman +Tạo các variables trong Postman Collection: +- `base_url`: `http://localhost:8080/account-service` +- `token`: (sẽ được set sau khi login) +- `admin_user_id`: (sẽ được set sau khi lấy user details) + +--- + +## 🔑 Lấy JWT Token + +### Step 1: Login với Admin Account + +**Request:** +``` +POST {{base_url}}/v1/api/auth/login +Content-Type: application/json +``` + +**Body (JSON):** +```json +{ + "email": "admin@labverse.com", + "password": "admin123" +} +``` + +**Response:** +```json +{ + "status": 200, + "message": "Login successful", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "type": "Bearer", + "userId": "encoded-user-id", + "email": "admin@labverse.com", + "username": "admin", + "fullName": "System Administrator", + "role": "ADMIN" + } +} +``` + +### Step 2: Copy Token +- Copy giá trị `token` từ response +- Set vào variable `token` trong Postman +- Hoặc dùng Postman script để tự động set: + +```javascript +// Postman Test Script (sau khi login) +if (pm.response.code === 200) { + var jsonData = pm.response.json(); + pm.environment.set("token", jsonData.data.token); + pm.environment.set("admin_user_id", jsonData.data.userId); + console.log("Token saved:", jsonData.data.token); +} +``` + +--- + +## 👥 Test User Management Endpoints + +### 1. Get All Users + +**Request:** +``` +GET {{base_url}}/admin/users?page=0&size=20 +Authorization: Bearer {{token}} +``` + +**Query Parameters:** +- `page` (optional): Page number (default: 0) +- `size` (optional): Page size (default: 20) +- `search` (optional): Search by email, username, or fullName +- `role` (optional): Filter by role (ADMIN, PI, RESEARCHER, STUDENT) +- `isActive` (optional): Filter by status (true/false) + +**Example với filters:** +``` +GET {{base_url}}/admin/users?page=0&size=20&search=admin&role=ADMIN&isActive=true +Authorization: Bearer {{token}} +``` + +**Expected Response:** +```json +{ + "status": 200, + "message": "Success", + "data": { + "content": [ + { + "id": "encoded-id", + "email": "admin@labverse.com", + "username": "admin", + "fullName": "System Administrator", + "avatarUrl": null, + "role": "ADMIN", + "createdDate": "2024-01-01", + "updatedDate": "2024-01-01" + } + ], + "totalElements": 1, + "totalPages": 1, + "currentPage": 0, + "size": 20 + } +} +``` + +**Postman Test Script:** +```javascript +pm.test("Status code is 200", function () { + pm.response.to.have.status(200); +}); + +pm.test("Response has users data", function () { + var jsonData = pm.response.json(); + pm.expect(jsonData.data).to.have.property('content'); + pm.expect(jsonData.data.content).to.be.an('array'); +}); +``` + +--- + +### 2. Get User Details + +**Request:** +``` +GET {{base_url}}/admin/users/{{user_id}} +Authorization: Bearer {{token}} +``` + +**Path Variable:** +- `user_id`: Encoded user ID (lấy từ response của Get All Users) + +**Expected Response:** +```json +{ + "status": 200, + "message": "Success", + "data": { + "id": "encoded-id", + "email": "user@example.com", + "username": "username", + "fullName": "Full Name", + "avatarUrl": null, + "role": "RESEARCHER", + "isActive": true, + "createdDate": "2024-01-01", + "updatedDate": "2024-01-01", + "paperCount": 0, + "teamCount": 2, + "collectionCount": 5 + } +} +``` + +--- + +### 3. Activate User + +**Request:** +``` +PATCH {{base_url}}/admin/users/{{user_id}}/activate +Authorization: Bearer {{token}} +``` + +**Expected Response:** +```json +{ + "status": 200, + "message": "User activated successfully", + "data": null +} +``` + +--- + +### 4. Deactivate User + +**Request:** +``` +PATCH {{base_url}}/admin/users/{{user_id}}/deactivate +Authorization: Bearer {{token}} +``` + +**Expected Response:** +```json +{ + "status": 200, + "message": "User deactivated successfully", + "data": null +} +``` + +--- + +### 5. Change User Role + +**Request:** +``` +PATCH {{base_url}}/admin/users/{{user_id}}/role +Authorization: Bearer {{token}} +Content-Type: application/json +``` + +**Body (JSON):** +```json +{ + "roleId": "role-uuid-here" +} +``` + +**Lưu ý:** +- Cần lấy `roleId` từ database (Role table) +- Không thể đổi role của chính mình + +**Expected Response:** +```json +{ + "status": 200, + "message": "Success", + "data": { + "id": "encoded-id", + "email": "user@example.com", + "role": "PI" + } +} +``` + +**Cách lấy Role ID:** +```sql +SELECT id, name FROM Role; +``` + +--- + +### 6. Delete User + +**Request:** +``` +DELETE {{base_url}}/admin/users/{{user_id}} +Authorization: Bearer {{token}} +``` + +**Expected Response:** +```json +{ + "status": 200, + "message": "User deleted successfully", + "data": null +} +``` + +**Lưu ý:** +- Không thể delete chính mình +- Delete là soft delete (set isActive = false) + +--- + +## 📊 Test Statistics Endpoints + +### Get Overview Statistics + +**Request:** +``` +GET {{base_url}}/admin/statistics/overview +Authorization: Bearer {{token}} +``` + +**Expected Response:** +```json +{ + "status": 200, + "message": "Success", + "data": { + "totalUsers": 10, + "activeUsers": 8, + "inactiveUsers": 2, + "totalPapers": 0, + "papersThisMonth": 0, + "totalCollections": 0 + } +} +``` + +--- + +## 🔍 Troubleshooting + +### Lỗi 401 Unauthorized + +**Nguyên nhân:** +- Token không hợp lệ hoặc đã hết hạn +- Thiếu header Authorization + +**Giải pháp:** +1. Login lại để lấy token mới +2. Kiểm tra header: `Authorization: Bearer {{token}}` +3. Đảm bảo token không có khoảng trắng thừa + +--- + +### Lỗi 403 Forbidden + +**Nguyên nhân:** +- User không có role ADMIN +- Role không được set đúng trong database + +**Giải pháp:** +1. Kiểm tra role trong database: +```sql +SELECT u.email, r.name as role +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE u.email = 'admin@labverse.com'; +``` + +2. Đảm bảo role là `ADMIN` (không phải `ROLE_ADMIN`) + +3. Kiểm tra SecurityConfig có đúng không: +```java +hierarchy.setHierarchy("ROLE_ADMIN > ROLE_PI > ROLE_RESEARCHER > ROLE_INTERN"); +``` + +--- + +### Lỗi 500 Internal Server Error + +**Nguyên nhân phổ biến:** + +#### 1. Query Error - NULL handling +**Lỗi:** `NullPointerException` hoặc `QueryException` + +**Giải pháp:** +- Đã fix trong code: Query đã check `IS NULL OR = ''` +- Service đã normalize empty strings thành null + +**Test query trực tiếp:** +```sql +-- Test query với NULL +SELECT u.* FROM Users u +WHERE (NULL IS NULL OR LOWER(u.email) LIKE LOWER('%' + NULL + '%')) +AND (NULL IS NULL OR u.role.name = NULL) +AND (NULL IS NULL OR u.isActive = NULL); + +-- Test query với empty string +SELECT u.* FROM Users u +WHERE ('' IS NULL OR '' = '' OR LOWER(u.email) LIKE LOWER('%' + '' + '%')) +AND ('' IS NULL OR '' = '' OR u.role.name = '') +AND (NULL IS NULL OR u.isActive = NULL); +``` + +#### 2. Role không tồn tại +**Lỗi:** `ResourceNotFoundException: Role not found` + +**Giải pháp:** +```sql +-- Kiểm tra roles có trong database +SELECT * FROM Role; + +-- Nếu thiếu ADMIN role: +INSERT INTO Role (id, name) VALUES (NEWID(), 'ADMIN'); +``` + +#### 3. User không có Role +**Lỗi:** `NullPointerException` khi access `user.getRole().getName()` + +**Giải pháp:** +```sql +-- Kiểm tra users không có role +SELECT u.* FROM Users u WHERE u.Roleid IS NULL; + +-- Fix: Gán role cho user +UPDATE Users +SET Roleid = (SELECT id FROM Role WHERE name = 'RESEARCHER') +WHERE Roleid IS NULL; +``` + +#### 4. LazyInitializationException +**Lỗi:** `could not initialize proxy - no Session` + +**Giải pháp:** +- Đảm bảo `@Transactional` được dùng đúng +- Check `fetch = FetchType.EAGER` cho relationship cần thiết + +--- + +### Lỗi 400 Bad Request + +#### Invalid User ID Format +**Nguyên nhân:** ID không phải encoded format + +**Giải pháp:** +- Sử dụng encoded ID từ response của Get All Users +- Không dùng raw UUID + +#### Cannot change your own role +**Nguyên nhân:** Admin đang cố đổi role của chính mình + +**Giải pháp:** +- Dùng user ID khác để test +- Hoặc dùng admin khác để đổi role + +--- + +## 📝 Postman Collection Template + +### Collection Structure: +``` +LabVerse Admin API +├── Authentication +│ └── Login Admin +├── User Management +│ ├── Get All Users +│ ├── Get User Details +│ ├── Activate User +│ ├── Deactivate User +│ ├── Change User Role +│ └── Delete User +└── Statistics + └── Get Overview Statistics +``` + +### Pre-request Script (cho tất cả requests): +```javascript +// Auto-set token if available +if (pm.environment.get("token")) { + pm.request.headers.add({ + key: "Authorization", + value: "Bearer " + pm.environment.get("token") + }); +} +``` + +### Test Script (cho tất cả requests): +```javascript +// Check if response is successful +if (pm.response.code >= 200 && pm.response.code < 300) { + console.log("✅ Request successful"); +} else { + console.log("❌ Request failed:", pm.response.text()); +} +``` + +--- + +## 🧪 Test Cases + +### Test Case 1: Get All Users (No Filters) +``` +GET /admin/users?page=0&size=20 +Expected: 200 OK, returns paginated users +``` + +### Test Case 2: Get All Users (With Search) +``` +GET /admin/users?page=0&size=20&search=admin +Expected: 200 OK, returns filtered users +``` + +### Test Case 3: Get All Users (With Role Filter) +``` +GET /admin/users?page=0&size=20&role=ADMIN +Expected: 200 OK, returns only ADMIN users +``` + +### Test Case 4: Get All Users (With Status Filter) +``` +GET /admin/users?page=0&size=20&isActive=true +Expected: 200 OK, returns only active users +``` + +### Test Case 5: Activate User +``` +PATCH /admin/users/{id}/activate +Expected: 200 OK, user.isActive = true +``` + +### Test Case 6: Deactivate User +``` +PATCH /admin/users/{id}/deactivate +Expected: 200 OK, user.isActive = false +``` + +### Test Case 7: Get Statistics +``` +GET /admin/statistics/overview +Expected: 200 OK, returns statistics object +``` + +--- + +## 🔧 Debug Backend + +### 1. Check Logs +Xem logs trong console để tìm lỗi cụ thể: +``` +2025-11-14T12:34:11.266+07:00 ERROR ... Exception: ... +``` + +### 2. Test Query Trực Tiếp +```sql +-- Test query findAllWithFilters +DECLARE @search NVARCHAR(255) = NULL; +DECLARE @role NVARCHAR(255) = NULL; +DECLARE @isActive BIT = NULL; + +SELECT u.* +FROM Users u +WHERE (@search IS NULL OR @search = '' OR LOWER(u.email) LIKE LOWER('%' + @search + '%')) +AND (@role IS NULL OR @role = '' OR EXISTS ( + SELECT 1 FROM Role r WHERE r.id = u.Roleid AND r.name = @role +)) +AND (@isActive IS NULL OR u.isActive = @isActive); +``` + +### 3. Check Database State +```sql +-- Check users và roles +SELECT + u.id, + u.email, + u.username, + u.isActive, + r.name as role_name +FROM Users u +LEFT JOIN Role r ON u.Roleid = r.id; + +-- Check ADMIN role exists +SELECT * FROM Role WHERE name = 'ADMIN'; + +-- Check admin users +SELECT u.*, r.name as role +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE r.name = 'ADMIN'; +``` + +### 4. Enable SQL Logging +Thêm vào `application.yml`: +```yaml +spring: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true +``` + +--- + +## ✅ Checklist Trước Khi Test + +- [ ] Admin user đã được tạo trong database +- [ ] ADMIN role đã tồn tại trong Role table +- [ ] User đã được gán ADMIN role +- [ ] Backend service đang chạy (port 8080) +- [ ] Database connection OK +- [ ] JWT token đã được lấy và set vào Postman +- [ ] Authorization header đã được thêm vào requests + +--- + +## 📞 Common Issues & Solutions + +### Issue 1: "No static resource v1/api/v1/api/admin/users" +**Nguyên nhân:** URL bị duplicate prefix +**Giải pháp:** Đã fix - URL giờ là `/admin/users` không còn `/v1/api/admin/users` + +### Issue 2: "Access Denied" hoặc 403 +**Nguyên nhân:** Role không đúng +**Giải pháp:** +```sql +-- Verify role +SELECT u.email, r.name +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE u.email = 'your-admin-email@example.com'; +``` + +### Issue 3: Query returns empty result +**Nguyên nhân:** Query conditions quá strict +**Giải pháp:** Test với NULL values trước: +``` +GET /admin/users?page=0&size=20 +``` + +--- + +## 🎯 Quick Test Commands + +### Test với cURL: +```bash +# Login +curl -X POST http://localhost:8080/account-service/v1/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@labverse.com","password":"admin123"}' + +# Get Users (thay TOKEN bằng token từ login) +curl -X GET "http://localhost:8080/account-service/admin/users?page=0&size=20" \ + -H "Authorization: Bearer TOKEN" + +# Get Statistics +curl -X GET "http://localhost:8080/account-service/admin/statistics/overview" \ + -H "Authorization: Bearer TOKEN" +``` + +--- + +Chúc bạn test thành công! 🚀 + diff --git "a/document/H\306\260\341\273\233ng D\341\272\253n T\341\272\241o T\303\240i Kho\341\272\243n Admin.md" "b/document/H\306\260\341\273\233ng D\341\272\253n T\341\272\241o T\303\240i Kho\341\272\243n Admin.md" new file mode 100644 index 00000000..29460c05 --- /dev/null +++ "b/document/H\306\260\341\273\233ng D\341\272\253n T\341\272\241o T\303\240i Kho\341\272\243n Admin.md" @@ -0,0 +1,174 @@ +# Hướng Dẫn Tạo Tài Khoản Admin + +## Tài khoản Admin là gì? + +**Tài khoản Admin** là user có role `ADMIN` trong hệ thống LabVerse. Với role này, bạn có thể: + +- ✅ Quản lý tất cả users (xem, activate/deactivate, đổi role, xóa) +- ✅ Quản lý tất cả teams (xem, xóa) +- ✅ Xem thống kê tổng quan của hệ thống +- ✅ Truy cập Admin Dashboard tại `/admin` + +## Cách Tạo Tài Khoản Admin + +### ⚠️ Lưu ý quan trọng: +- **Không thể** đăng ký với role ADMIN qua form đăng ký thông thường +- Form đăng ký chỉ cho phép: `PI`, `RESEARCHER`, `STUDENT` +- Cần tạo admin user trực tiếp trong database hoặc qua API + +--- + +## Phương Pháp 1: Tạo Qua Database (Khuyến nghị cho lần đầu) + +### Bước 1: Tạo BCrypt Hash cho Password + +Bạn cần hash password bằng BCrypt. Có thể dùng: + +**Option A: Online BCrypt Generator** +- Truy cập: https://bcrypt-generator.com/ +- Nhập password (ví dụ: `admin123`) +- Copy hash được tạo (ví dụ: `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy`) + +**Option B: Dùng Spring Boot Application** +```java +// Tạo một test class hoặc dùng Spring Shell +@Autowired +private PasswordEncoder passwordEncoder; + +String hash = passwordEncoder.encode("admin123"); +System.out.println(hash); +``` + +### Bước 2: Chạy SQL Script + +1. Mở SQL Server Management Studio hoặc tool quản lý database +2. Kết nối đến database của LabVerse +3. Chạy script: `services/AccountService/src/main/resources/database/create_admin_user.sql` +4. **Nhớ thay đổi**: + - Email: `admin@labverse.com` → email bạn muốn + - Username: `admin` → username bạn muốn + - Password hash: Thay bằng BCrypt hash của password bạn muốn + +### Bước 3: Verify + +Sau khi chạy script, kiểm tra: +```sql +SELECT u.email, u.username, r.name as role +FROM Users u +JOIN Role r ON u.Roleid = r.id +WHERE r.name = 'ADMIN'; +``` + +### Bước 4: Đăng nhập + +- Email: Email bạn đã đặt trong script +- Password: Password bạn đã hash ở Bước 1 + +--- + +## Phương Pháp 2: Tạo Qua API (Nếu đã có Admin khác) + +Nếu bạn đã có một admin user, bạn có thể tạo admin mới qua Admin Dashboard: + +1. Đăng nhập với admin account hiện tại +2. Vào Admin Dashboard (`/admin`) +3. Tab "Users" → Tìm user cần nâng cấp +4. Click "Actions" → "Change Role" +5. Chọn role `ADMIN` + +**Hoặc** dùng API trực tiếp: +```bash +PATCH /account-service/v1/api/admin/users/{userId}/role +Authorization: Bearer {admin_token} +Content-Type: application/json + +{ + "roleId": "{ADMIN_ROLE_ID}" +} +``` + +--- + +## Phương Pháp 3: Tạo Admin User Đầu Tiên Qua Code (Tạm thời) + +Nếu bạn muốn tự động tạo admin user khi ứng dụng khởi động, có thể thêm vào `AccountServiceApplication.java`: + +```java +@PostConstruct +public void createAdminUser() { + // Check if admin exists + if (!userRepository.existsByEmail("admin@labverse.com")) { + Role adminRole = roleRepository.findByName("ADMIN") + .orElseThrow(() -> new RuntimeException("ADMIN role not found")); + + User admin = new User(); + admin.setEmail("admin@labverse.com"); + admin.setUsername("admin"); + admin.setFullName("System Administrator"); + admin.setPassword(passwordEncoder.encode("admin123")); // Change this! + admin.setRole(adminRole); + admin.setIsActive(true); + + userRepository.save(admin); + System.out.println("Admin user created: admin@labverse.com / admin123"); + } +} +``` + +⚠️ **Lưu ý**: Xóa code này sau khi đã tạo admin user để tránh security risk! + +--- + +## Kiểm Tra Role Hierarchy + +Hệ thống có role hierarchy như sau: +``` +ADMIN > PI > RESEARCHER > INTERN +``` + +Admin có quyền cao nhất và có thể: +- Quản lý tất cả users +- Xem tất cả teams (kể cả private) +- Xóa teams và users +- Xem thống kê hệ thống + +--- + +## Troubleshooting + +### Lỗi: "Role not found: ADMIN" +- **Giải pháp**: Chạy migration script `migration_add_admin_role.sql` trước + +### Lỗi: "Access Denied" khi vào `/admin` +- **Giải pháp**: Kiểm tra user có role `ADMIN` không: + ```sql + SELECT u.email, r.name as role + FROM Users u + JOIN Role r ON u.Roleid = r.id + WHERE u.email = 'your-email@example.com'; + ``` + +### Lỗi: "Cannot change your own role" +- **Giải pháp**: Admin không thể đổi role của chính mình. Cần admin khác thực hiện. + +--- + +## Security Best Practices + +1. ✅ **Đổi password mặc định** ngay sau khi tạo admin user +2. ✅ **Sử dụng email thật** để có thể reset password nếu cần +3. ✅ **Không chia sẻ** admin credentials +4. ✅ **Tạo nhiều admin users** để tránh single point of failure +5. ✅ **Xóa code tự động tạo admin** sau khi đã setup xong + +--- + +## Default Admin Credentials (Sau khi chạy script) + +Nếu dùng script mặc định: +- **Email**: `admin@labverse.com` +- **Username**: `admin` +- **Password**: `admin123` (hoặc password bạn đã hash) + +⚠️ **NHỚ ĐỔI PASSWORD NGAY SAU KHI ĐĂNG NHẬP LẦN ĐẦU!** + diff --git a/document/LabVerse_Admin_API.postman_collection.json b/document/LabVerse_Admin_API.postman_collection.json new file mode 100644 index 00000000..f783d8f2 --- /dev/null +++ b/document/LabVerse_Admin_API.postman_collection.json @@ -0,0 +1,346 @@ +{ + "info": { + "_postman_id": "labverse-admin-api", + "name": "LabVerse Admin API", + "description": "Collection để test các Admin endpoints của LabVerse", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Authentication", + "item": [ + { + "name": "Login Admin", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " var jsonData = pm.response.json();", + " pm.environment.set(\"token\", jsonData.data.token);", + " pm.environment.set(\"admin_user_id\", jsonData.data.userId);", + " console.log(\"✅ Token saved:\", jsonData.data.token);", + "}", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Response has token\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data).to.have.property('token');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@labverse.com\",\n \"password\": \"admin123\"\n}" + }, + "url": { + "raw": "{{base_url}}/v1/api/auth/login", + "host": [ + "{{base_url}}" + ], + "path": [ + "v1", + "api", + "auth", + "login" + ] + }, + "description": "Login với admin account để lấy JWT token" + }, + "response": [] + } + ] + }, + { + "name": "User Management", + "item": [ + { + "name": "Get All Users", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Response has users data\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data).to.have.property('content');", + " pm.expect(jsonData.data.content).to.be.an('array');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/users?page=0&size=20", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "size", + "value": "20" + }, + { + "key": "search", + "value": "", + "disabled": true + }, + { + "key": "role", + "value": "", + "disabled": true + }, + { + "key": "isActive", + "value": "", + "disabled": true + } + ] + }, + "description": "Lấy danh sách tất cả users với pagination và filters" + }, + "response": [] + }, + { + "name": "Get User Details", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/users/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users", + "{{user_id}}" + ] + }, + "description": "Lấy chi tiết của một user cụ thể" + }, + "response": [] + }, + { + "name": "Activate User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/users/{{user_id}}/activate", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users", + "{{user_id}}", + "activate" + ] + }, + "description": "Kích hoạt một user (set isActive = true)" + }, + "response": [] + }, + { + "name": "Deactivate User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/users/{{user_id}}/deactivate", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users", + "{{user_id}}", + "deactivate" + ] + }, + "description": "Vô hiệu hóa một user (set isActive = false)" + }, + "response": [] + }, + { + "name": "Change User Role", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"roleId\": \"role-uuid-here\"\n}" + }, + "url": { + "raw": "{{base_url}}/admin/users/{{user_id}}/role", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users", + "{{user_id}}", + "role" + ] + }, + "description": "Đổi role của user. Cần roleId từ database." + }, + "response": [] + }, + { + "name": "Delete User", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/users/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "users", + "{{user_id}}" + ] + }, + "description": "Xóa user (soft delete - set isActive = false)" + }, + "response": [] + } + ] + }, + { + "name": "Statistics", + "item": [ + { + "name": "Get Overview Statistics", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Response has statistics\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data).to.have.property('totalUsers');", + " pm.expect(jsonData.data).to.have.property('totalCollections');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/admin/statistics/overview", + "host": [ + "{{base_url}}" + ], + "path": [ + "admin", + "statistics", + "overview" + ] + }, + "description": "Lấy thống kê tổng quan của hệ thống" + }, + "response": [] + } + ] + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:8080/account-service", + "type": "string" + }, + { + "key": "token", + "value": "", + "type": "string" + }, + { + "key": "user_id", + "value": "", + "type": "string" + } + ] +} + diff --git "a/document/M\303\264 t\341\272\243 Lu\341\273\223ng Ho\341\272\241t \304\220\341\273\231ng - Team v\303\240 Reading List.md" "b/document/M\303\264 t\341\272\243 Lu\341\273\223ng Ho\341\272\241t \304\220\341\273\231ng - Team v\303\240 Reading List.md" new file mode 100644 index 00000000..74dfc225 --- /dev/null +++ "b/document/M\303\264 t\341\272\243 Lu\341\273\223ng Ho\341\272\241t \304\220\341\273\231ng - Team v\303\240 Reading List.md" @@ -0,0 +1,824 @@ +# Mô Tả Luồng Hoạt Động - Team và Reading List + +## Mục Lục +1. [Tổng Quan](#tổng-quan) +2. [Luồng Hoạt Động Team Management](#luồng-hoạt-động-team-management) +3. [Luồng Hoạt Động Reading List Management](#luồng-hoạt-động-reading-list-management) +4. [So Sánh với Core Functional.md](#so-sánh-với-core-functionalmd) +5. [Luồng Upload PDF lên S3 và Hiển Thị](#luồng-upload-pdf-lên-s3-và-hiển-thị) + +--- + +## Tổng Quan + +Tài liệu này mô tả chi tiết các luồng hoạt động của **Team Management** và **Reading List Management** trong hệ thống LabVerse, đồng thời so sánh với yêu cầu trong Core Functional.md và mô tả luồng upload PDF lên S3 và hiển thị. + +--- + +## Luồng Hoạt Động Team Management + +### 1. Tạo Team Mới (UC-TM-01) + +**Luồng chính:** +``` +1. User đăng nhập → Navigate đến trang Teams (/teams) +2. Click nút "Create Team" +3. Hệ thống hiển thị dialog tạo team +4. User điền form: + - Team Name (bắt buộc) + - Description (tùy chọn) + - Research Field (tùy chọn) + - Privacy: PUBLIC hoặc PRIVATE (bắt buộc) +5. Click "Create Team" +6. Frontend gửi POST request đến API: POST /v1/api/teams +7. Backend (AccountService) xử lý: + - Validate dữ liệu đầu vào + - Kiểm tra tên team đã tồn tại chưa + - Tạo team trong database với UUID + - Tự động thêm creator làm OWNER + - Encode ID trước khi trả về +8. Backend trả về TeamResponse với ID đã encode +9. Frontend hiển thị thông báo thành công +10. Refresh danh sách teams +11. Đóng dialog +``` + +**Business Rules:** +- Tên team phải unique trong hệ thống +- Creator tự động nhận role OWNER +- ID được encode bằng Base64 URL-safe trước khi trả về +- Privacy mặc định là PUBLIC nếu không chỉ định + +**API Endpoint:** +- `POST /account-service/v1/api/teams` +- Authentication: Required (Bearer Token) +- Authorization: `@PreAuthorize("isAuthenticated()")` + +--- + +### 2. Xem Danh Sách Teams (UC-TM-02) + +**Luồng chính:** +``` +1. User navigate đến trang Teams +2. Frontend gửi GET request: GET /v1/api/teams/?page=0&size=10-12 +3. Có thể thêm filters: + - search: Tìm kiếm theo tên team + - privacy: Lọc theo PUBLIC/PRIVATE + - researchField: Lọc theo lĩnh vực nghiên cứu +4. Backend trả về paginated response với danh sách teams +5. Frontend hiển thị team cards trong grid layout +6. User có thể: + - Scroll để xem thêm + - Search theo tên + - Filter theo privacy/research field + - Click pagination để chuyển trang + - Click vào team card để xem chi tiết +``` + +**Business Rules:** +- PUBLIC teams: Tất cả user đã đăng nhập đều thấy được +- PRIVATE teams: Chỉ members mới thấy được +- Pagination mặc định: 10-12 teams/trang +- Search không phân biệt hoa thường + +--- + +### 3. Xem Chi Tiết Team (UC-TM-03) + +**Luồng chính:** +``` +1. User click vào team card +2. Navigate đến /teams/:id +3. Frontend gửi GET request: GET /v1/api/teams/{teamId} +4. Backend decode teamId và lấy thông tin team +5. Frontend gửi GET request: GET /v1/api/teams/{teamId}/members +6. Backend trả về danh sách members với roles +7. Frontend hiển thị: + - Thông tin team (name, description, research field, privacy) + - Danh sách members với roles + - Paper count (nếu có) + - Action buttons (nếu user là OWNER/ADMIN): + * Add Member + * Edit Team + * Delete Team + * Update Member Role +``` + +**Business Rules:** +- Chỉ OWNER và ADMIN thấy action buttons +- MEMBER chỉ có thể xem thông tin +- PRIVATE teams chỉ members mới truy cập được + +--- + +### 4. Thêm Member vào Team (UC-TM-04) + +**Luồng chính:** +``` +1. OWNER/ADMIN navigate đến team detail page +2. Click "Add Member" +3. Hiển thị dialog thêm member +4. User nhập email hoặc username +5. User chọn role: ADMIN hoặc MEMBER +6. Click "Add" +7. Frontend gửi POST request: POST /v1/api/teams/{teamId}/members + Body: { "userId": "email-or-id", "role": "MEMBER" } +8. Backend xử lý: + - Tìm user theo email/username + - Kiểm tra user đã là member chưa + - Thêm user vào team với role đã chọn +9. Backend trả về TeamMemberResponse +10. Frontend refresh member list +11. Hiển thị thông báo thành công +``` + +**Business Rules:** +- OWNER không thể remove hoặc đổi role của chính mình +- Mỗi user chỉ có thể là member một lần mỗi team +- Role mặc định là MEMBER nếu không chỉ định +- OWNER role không thể assign qua action này (chỉ qua transfer) + +--- + +### 5. Xóa Member khỏi Team (UC-TM-05) + +**Luồng chính:** +``` +1. OWNER/ADMIN xem member list +2. Click "Remove" bên cạnh member +3. Hiển thị confirmation dialog +4. User xác nhận +5. Frontend gửi DELETE request: DELETE /v1/api/teams/{teamId}/members/{memberId} +6. Backend xử lý: + - Kiểm tra quyền (OWNER/ADMIN) + - Kiểm tra không phải OWNER + - Xóa member khỏi team + - Revoke access của member +7. Backend trả về success +8. Frontend refresh member list +9. Hiển thị thông báo thành công +``` + +**Business Rules:** +- OWNER không thể remove chính mình +- OWNER không thể bị remove bởi ADMIN +- Xóa là immediate và không thể undo + +--- + +### 6. Cập Nhật Role của Member (UC-TM-06) + +**Luồng chính:** +``` +1. OWNER xem member list +2. Click "Change Role" hoặc dropdown role +3. Chọn role mới: OWNER, ADMIN, hoặc MEMBER +4. Hiển thị confirmation (đặc biệt khi transfer OWNER) +5. User xác nhận +6. Frontend gửi PUT request: PUT /v1/api/teams/{teamId}/members/{memberId}/role + Body: { "role": "ADMIN" } +7. Backend xử lý: + - Kiểm tra quyền (chỉ OWNER) + - Nếu transfer OWNER: + * Cập nhật role của member mới thành OWNER + * Cập nhật role của OWNER cũ thành ADMIN + - Cập nhật permissions +8. Backend trả về updated TeamMemberResponse +9. Frontend refresh member list +``` + +**Business Rules:** +- Chỉ OWNER mới có thể đổi role +- Khi transfer OWNER, OWNER cũ tự động thành ADMIN +- Chỉ có một OWNER tại một thời điểm +- Role changes là immediate + +--- + +### 7. Cập Nhật Thông Tin Team (UC-TM-07) + +**Luồng chính:** +``` +1. OWNER/ADMIN navigate đến team detail +2. Click "Edit Team" +3. Hiển thị form với thông tin hiện tại +4. User chỉnh sửa: + - Team Name + - Description + - Research Field + - Privacy (PUBLIC/PRIVATE) +5. Click "Save" +6. Frontend gửi PUT request: PUT /v1/api/teams/{teamId} + Body: { "name": "...", "description": "...", ... } +7. Backend xử lý: + - Validate dữ liệu + - Kiểm tra tên team unique (nếu đổi tên) + - Cập nhật team trong database +8. Backend trả về updated TeamResponse +9. Frontend refresh thông tin team +10. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Tên team phải unique nếu đổi +- Privacy có thể đổi giữa PUBLIC và PRIVATE +- Updated timestamp tự động cập nhật + +--- + +### 8. Xóa Team (UC-TM-08) + +**Luồng chính:** +``` +1. OWNER navigate đến team detail +2. Click "Delete Team" +3. Hiển thị confirmation dialog với warning +4. User xác nhận +5. Frontend gửi DELETE request: DELETE /v1/api/teams/{teamId} +6. Backend xử lý: + - Kiểm tra quyền (chỉ OWNER) + - Xóa team khỏi database + - Xóa tất cả team-member relationships + - Xử lý team-paper relationships (papers không bị xóa) +7. Backend trả về success +8. Frontend navigate về team list +9. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Chỉ OWNER mới có thể xóa team +- Xóa là permanent và không thể undo +- Papers không bị xóa, chỉ mất association với team +- Tất cả members mất access ngay lập tức + +--- + +## Luồng Hoạt Động Reading List Management + +### 1. Tạo Reading List (UC-RL-01) + +**Luồng chính:** +``` +1. User đăng nhập → Navigate đến Reading Lists page (/reading-lists) +2. Click "Create List" +3. Hiển thị dialog tạo reading list +4. User điền form: + - List Name (bắt buộc) + - Description (tùy chọn) +5. Click "Create List" +6. Frontend gửi POST request: POST /v1/api/reading-lists + Body: { + "name": "...", + "description": "...", + "userId": "encoded-user-id", + "userIdsList": ["encoded-user-id"], + "paperIdsList": [] + } +7. Backend (ReadingService) xử lý: + - Validate dữ liệu (name required) + - Tạo reading list trong database + - Tự động thêm creator vào userIds array + - Encode ID trước khi trả về +8. Backend trả về ReadingListResponse +9. Frontend refresh danh sách reading lists +10. Hiển thị thông báo thành công +``` + +**Business Rules:** +- List name không cần unique (user có thể có nhiều list cùng tên) +- Creator tự động được thêm vào userIds array +- List bắt đầu với paperIds array rỗng +- Created và updated timestamps tự động set + +--- + +### 2. Xem Danh Sách Reading Lists (UC-RL-02) + +**Luồng chính:** +``` +1. User navigate đến Reading Lists page +2. Frontend gửi GET request: GET /v1/api/reading-lists/user/{userId} +3. Backend trả về danh sách reading lists mà user là member +4. Frontend hiển thị reading list cards trong grid layout +5. Mỗi card hiển thị: + - List name + - Description (truncated 2 dòng) + - Paper count + - Member count + - Created date +6. User có thể click vào card để xem chi tiết +``` + +**Business Rules:** +- Chỉ hiển thị lists mà user là member +- Lists được sort theo created date (mới nhất trước) +- Empty lists vẫn được hiển thị +- Paper count và member count hiển thị 0 nếu rỗng + +--- + +### 3. Xem Chi Tiết Reading List (UC-RL-03) + +**Luồng chính:** +``` +1. User click vào reading list card +2. Navigate đến /reading-lists/:id +3. Frontend gửi GET request: GET /v1/api/reading-lists/{listId} +4. Backend decode listId và lấy thông tin list +5. Frontend hiển thị: + - List name và description + - Paper count + - Member count + - Danh sách papers (title, authors, journal) + - Danh sách members (name, email, avatar) + - Created date, Updated date +6. Hiển thị action buttons: + - Add Paper + - Add Member + - Delete List +``` + +**Business Rules:** +- Chỉ members mới có thể xem chi tiết +- Tất cả members có quyền ngang nhau (không có role hierarchy) +- Papers hiển thị với thông tin cơ bản + +--- + +### 4. Thêm Papers vào Reading List (UC-RL-04) + +**Luồng chính:** +``` +1. Member navigate đến reading list detail +2. Click "Add Paper" +3. Hiển thị paper selection interface: + - Search bar để tìm papers + - Danh sách papers từ library của user + - Hoặc paper picker dialog +4. User chọn một hoặc nhiều papers +5. Click "Add" hoặc "Add Selected" +6. Frontend gửi PUT request: PUT /v1/api/reading-lists/{listId}/papers + Body: { "paperIds": ["encoded-paper-id-1", "encoded-paper-id-2"] } +7. Backend xử lý: + - Validate papers tồn tại + - Kiểm tra papers đã có trong list chưa (optional) + - Thêm papers vào paperIds array + - Cập nhật reading list trong database +8. Backend trả về updated ReadingListResponse +9. Frontend refresh paper list +10. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Một paper có thể tồn tại trong nhiều reading lists +- Papers không bị duplicate trong cùng một list +- Thêm papers không xóa papers khỏi user's library +- Paper count tự động cập nhật + +--- + +### 5. Xóa Papers khỏi Reading List (UC-RL-05) + +**Luồng chính:** +``` +1. Member xem paper list trong reading list detail +2. Click "Remove" trên paper card hoặc swipe to delete (mobile) +3. Hiển thị confirmation (optional) +4. User xác nhận +5. Frontend gửi PUT request: PUT /v1/api/reading-lists/{listId}/papers + Body: { "paperIds": [remaining-paper-ids] } // Không bao gồm paper bị xóa +6. Backend cập nhật paperIds array +7. Frontend refresh paper list +8. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Xóa paper khỏi list không xóa paper khỏi system +- Paper vẫn ở trong user's library +- Paper vẫn ở trong các reading lists khác (nếu có) +- Xóa là immediate + +--- + +### 6. Thêm Members vào Reading List (UC-RL-06) + +**Luồng chính:** +``` +1. Member navigate đến reading list detail +2. Click "Add Member" +3. Hiển thị dialog thêm member +4. User nhập email hoặc username +5. Click "Add" +6. Frontend gửi PUT request: PUT /v1/api/reading-lists/{listId}/users + Body: { "userIds": ["encoded-user-id-1", "encoded-user-id-2"] } +7. Backend xử lý: + - Tìm user theo email/username + - Validate user tồn tại + - Kiểm tra user đã là member chưa + - Thêm user vào userIds array + - Cập nhật reading list trong database +8. Backend trả về updated ReadingListResponse +9. Frontend refresh member list +10. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Mỗi user chỉ có thể là member một lần mỗi list +- Tất cả members có quyền ngang nhau +- Thêm members cho phép collaborative list management +- Members có thể truy cập list ngay lập tức + +--- + +### 7. Xóa Members khỏi Reading List (UC-RL-07) + +**Luồng chính:** +``` +1. Member xem member list trong reading list detail +2. Click "Remove" bên cạnh user +3. Hiển thị confirmation dialog +4. User xác nhận +5. Frontend gửi PUT request: PUT /v1/api/reading-lists/{listId}/users + Body: { "userIds": [remaining-user-ids] } // Không bao gồm user bị xóa +6. Backend cập nhật userIds array +7. Frontend refresh member list +8. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Users có thể remove chính mình khỏi list +- Xóa member không xóa papers +- Papers vẫn accessible cho remaining members +- Xóa là immediate + +--- + +### 8. Xóa Reading List (UC-RL-08) + +**Luồng chính:** +``` +1. Member navigate đến Reading Lists page hoặc detail page +2. Click "Delete" (dropdown menu hoặc action button) +3. Hiển thị confirmation dialog với warning +4. User xác nhận +5. Frontend gửi DELETE request: DELETE /v1/api/reading-lists/{listId} +6. Backend xử lý: + - Xóa reading list khỏi database + - Xóa tất cả list-user relationships + - Xử lý list-paper relationships (papers không bị xóa) +7. Backend trả về success +8. Frontend navigate về reading lists page +9. Hiển thị thông báo thành công +``` + +**Business Rules:** +- Bất kỳ member nào cũng có thể xóa list (không cần OWNER) +- Xóa là permanent và không thể undo +- Papers trong list không bị xóa khỏi system +- Papers vẫn ở trong user libraries và các lists khác +- Tất cả list data bị xóa + +--- + +## So Sánh với Core Functional.md + +### 1. Team Management + +**Core Functional.md - Activity 6: Create and Manage Shared Collections (Projects)** + +**Yêu cầu trong Core Functional.md:** +- Purpose: Cho phép teams nhóm papers liên quan đến một project hoặc topic cụ thể +- UI Components: + - Tab "Teams" hoặc "Collections" + - RecyclerView liệt kê tất cả shared collections mà user là thành viên + - Button cho PIs để "Create New Collection" và "Invite Members" qua email +- Core Logic: Collections là backend feature. App fetch danh sách collections mà user có access. Thêm paper vào collection link nó với collection's ID. + +**Ánh xạ với Use Case Specification:** + +✅ **Đã ánh xạ đầy đủ:** + +1. **Create Team (UC-TM-01)** ↔ **Create New Collection** + - ✅ User có thể tạo team mới với thông tin cơ bản + - ✅ Creator tự động trở thành OWNER (tương đương PI) + - ✅ Team có thể PUBLIC hoặc PRIVATE + +2. **View Team List (UC-TM-02)** ↔ **RecyclerView của Collections** + - ✅ Hiển thị danh sách teams mà user là member + - ✅ Hỗ trợ search và filter + - ✅ Pagination support + +3. **Add Team Member (UC-TM-04)** ↔ **Invite Members** + - ✅ OWNER/ADMIN có thể thêm members + - ✅ Có thể thêm bằng email/username + - ⚠️ Chưa có email invitation workflow (được đề cập trong Future Enhancements) + +4. **Team-Paper Relationship** + - ✅ Papers có thể được link với team (thông qua collections) + - ✅ Papers không bị xóa khi team bị xóa + +**Khác biệt và Bổ sung:** + +- ✅ **Role Hierarchy**: Use Case Specification chi tiết hơn với OWNER > ADMIN > MEMBER +- ✅ **Privacy Settings**: PUBLIC/PRIVATE teams +- ✅ **Research Field**: Thêm field để categorize teams +- ✅ **Member Role Management**: Chi tiết hơn với transfer ownership +- ⚠️ **Email Invitations**: Chưa implement (trong Future Enhancements) + +--- + +### 2. Reading List Management + +**Core Functional.md - Activity 15: Create Reading Lists & Journal Clubs** + +**Yêu cầu trong Core Functional.md:** +- Purpose: Cho phép users manually curate và share themed collections của papers cho specific projects hoặc discussion groups +- UI Components: + - Tab "Reading Lists" với RecyclerView của user-created lists + - FAB để "Create New List" + - Interface đơn giản để add papers từ main library vào list + - Share button để invite other users để view hoặc collaborate trên list +- Core Logic: Feature dựa trên database relationships đơn giản (list has many papers; user has many lists). Tất cả actions là CRUD operations qua API. + +**Ánh xạ với Use Case Specification:** + +✅ **Đã ánh xạ đầy đủ:** + +1. **Create Reading List (UC-RL-01)** ↔ **Create New List** + - ✅ User có thể tạo reading list mới + - ✅ Creator tự động được thêm vào members + - ✅ List bắt đầu với empty papers + +2. **View Reading Lists (UC-RL-02)** ↔ **RecyclerView của Lists** + - ✅ Hiển thị danh sách reading lists của user + - ✅ Hiển thị paper count và member count + +3. **Add Papers to Reading List (UC-RL-04)** ↔ **Add Papers từ Library** + - ✅ Members có thể thêm papers vào list + - ✅ Papers được lấy từ user's library + - ✅ Hỗ trợ multiple papers selection + +4. **Add Members to Reading List (UC-RL-06)** ↔ **Share Button** + - ✅ Members có thể thêm users khác vào list + - ✅ Hỗ trợ collaborative management + - ⚠️ Chưa có share link/public link (trong Future Enhancements) + +5. **Database Relationships** + - ✅ List has many papers (paperIds array) + - ✅ User has many lists (userIds array) + - ✅ Tất cả operations là CRUD qua API + +**Khác biệt và Bổ sung:** + +- ✅ **Equal Permissions**: Tất cả members có quyền ngang nhau (không có role hierarchy như teams) +- ✅ **Multiple Lists per User**: User có thể có nhiều lists với cùng tên +- ✅ **Paper Sharing**: Papers có thể ở nhiều lists cùng lúc +- ⚠️ **Journal Clubs**: Chưa có feature riêng cho journal clubs (có thể implement như reading list với discussion features) + +--- + +## Luồng Upload PDF lên S3 và Hiển Thị + +### 1. Luồng Upload PDF lên S3 + +**Tổng quan:** +Hệ thống sử dụng AWS S3 để lưu trữ PDF files. Khi user upload PDF, file được upload lên S3 bucket và URL được lưu trong database. + +**Luồng chi tiết:** + +``` +1. User chọn PDF file từ device + - Android: Sử dụng Intent để mở file picker + - Web: Sử dụng file input element + +2. Frontend gửi request upload + - Endpoint: POST /paper-service/v1/api/papers/pdf/upload-with-file + - Method: POST + - Content-Type: multipart/form-data + - Headers: + * X-User-Id: encoded-user-id (optional) + - Body (form-data): + * file: PDF file (MultipartFile) + * title: String (required) + * authors: String (required) + * journal: String (required) + * publicationYear: Int (required) + * doi: String (required) + * description: String (optional) + * keywords: String (optional, comma-separated) + * tags: String (optional, comma-separated) + +3. Backend Controller (PaperController.uploadPdfWithFile) + - Validate file: + * Kiểm tra file không rỗng + * Kiểm tra content-type là "application/pdf" + - Kiểm tra S3Service có available không + - Gọi S3Service.uploadPdf() + +4. S3Service.uploadPdf() + - Tạo unique filename: "papers/{UUID}.pdf" + - Đọc file từ InputStream vào ByteArray + - Build PutObjectRequest: + * bucket: bucketName từ config + * key: fileName + * contentType: "application/pdf" + * acl: PUBLIC_READ (public-read) + - Upload file lên S3: + * Sử dụng S3Client.putObject() + * RequestBody từ ByteArray + - Generate S3 URL: + * Nếu region = "us-east-1": + "https://{bucketName}.s3.amazonaws.com/{fileName}" + * Nếu region khác: + "https://{bucketName}.s3.{region}.amazonaws.com/{fileName}" + - Return S3 URL + +5. Backend Controller tiếp tục xử lý + - Parse keywords và tags (nếu có) + - Tạo UploadPdfRequest với: + * dataUrl: S3 URL từ bước 4 + * title, authors, journal, publicationYear, doi + * description, keywords, tags + - Gọi PaperService.createNewPaper() + +6. PaperService.createNewPaper() + - Generate DOI nếu không có hoặc empty + - Process tags: Tìm hoặc tạo tags trong database + - Build Paper entity: + * id: UUID.randomUUID() + * dataUrl: S3 URL + * metadata: Metadata(title, authors, journal, publicationYear, doi) + * keywords, description, tags + * createdBy: userId + - Save Paper vào database (SQL Server) + - Sync với Firebase (nếu configured) + +7. Backend trả về response + - Status: 200 OK + - Message: "Upload paper successfully" + - Data: null + +8. Frontend xử lý response + - Hiển thị thông báo thành công + - Refresh paper list + - Navigate về library page (optional) +``` + +**Cấu hình S3:** + +```kotlin +// S3Config.kt +@Configuration +@ConditionalOnProperty(prefix = "aws.s3", name = ["access-key"]) +class S3Config( + @Value("\${aws.s3.access-key}") private val accessKey: String, + @Value("\${aws.s3.secret-key}") private val secretKey: String, + @Value("\${aws.s3.region}") private val region: String, + @Value("\${aws.s3.bucket}") private val bucketName: String +) { + @Bean + fun s3Client(): S3Client { + val credentials = AwsBasicCredentials.create(accessKey, secretKey) + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .build() + } +} +``` + +**S3 Bucket Settings:** +- **ACL**: PUBLIC_READ được set khi upload +- **Object Ownership**: Phải là "Bucket owner preferred" hoặc "ACLs enabled" +- **Block Public ACLs**: Phải disabled để cho phép public-read +- **Region**: Có thể config bất kỳ AWS region nào + +**Error Handling:** +- Nếu S3Service không available: Trả về 503 Service Unavailable +- Nếu file không phải PDF: Trả về 400 Bad Request +- Nếu upload S3 fail: Log error và throw RuntimeException +- Nếu ACL fail: Log warning về bucket settings + +--- + +### 2. Luồng Hiển Thị PDF + +**Tổng quan:** +Sau khi PDF được upload lên S3, URL được lưu trong database. Khi user muốn xem PDF, app fetch URL từ API và hiển thị PDF từ S3 URL. + +**Luồng chi tiết:** + +``` +1. User click vào paper trong library/list + - Android: Navigate đến PaperDetailsActivity + - Web: Navigate đến paper detail page + +2. Frontend fetch paper details + - Endpoint: GET /paper-service/v1/api/papers/details?id={encoded-paper-id} + - Backend trả về PaperResponse: + { + "id": "encoded-id", + "dataUrl": "https://bucket.s3.region.amazonaws.com/papers/uuid.pdf", + "title": "...", + "authors": "...", + "journal": "...", + "publicationYear": 2024, + "doi": "...", + "keywords": [...], + "description": "..." + } + +3. Frontend lấy dataUrl từ response + - dataUrl là S3 URL của PDF file + - URL có format: https://{bucket}.s3.{region}.amazonaws.com/papers/{uuid}.pdf + +4. Android App hiển thị PDF + - Sử dụng PDF viewer library (AndroidPdfViewer hoặc tương tự) + - Load PDF từ URL: + * Download PDF từ S3 URL + * Cache PDF locally (Room database hoặc file system) + * Render PDF trong PDFView component + - Features: + * Zoom in/out + * Scroll pages + * Annotations (highlights, notes) + * Page navigation + +5. Web App hiển thị PDF + - Sử dụng PDF.js hoặc iframe + - Load PDF từ S3 URL: + * Fetch PDF từ S3 URL + * Render trong