diff --git a/docs/superpowers/specs/2026-05-27-aspnetusers-member-management-design.md b/docs/superpowers/specs/2026-05-27-aspnetusers-member-management-design.md new file mode 100644 index 0000000..d6d9564 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-aspnetusers-member-management-design.md @@ -0,0 +1,318 @@ +# Design Spec: AspNetUsers CRUD & Member Management + +**日期:** 2026-05-27 +**作者:** Chris Chen +**狀態:** 已核准,待實作 + +--- + +## 1. 範疇 + +Phase 1 的兩個管理頁面: + +1. **Member Management** — 教友資料 CRUD(含軟刪除) +2. **User Management** — AspNetUsers 帳號 CRUD(透過 ASP.NET Identity `UserManager`) + +**業務規則:** +- 每個 `AppUser` 必須連結一個 `Member`(`MemberId` 不可為 null) +- 一個 `Member` 可以不有對應的 `AppUser`(教友可無登入帳號) + +--- + +## 2. 資料模型 + +### 2.1 基礎類別 + +```csharp +public abstract class AuditableEntity +{ + public DateTime CreatedAt { get; set; } + public string CreatedBy { get; set; } = null!; // FK → AspNetUsers.Id + public DateTime UpdatedAt { get; set; } + public string UpdatedBy { get; set; } = null!; // FK → AspNetUsers.Id +} + +public abstract class SoftDeleteEntity : AuditableEntity +{ + public bool IsDeleted { get; set; } = false; + public DateTime? DeletedAt { get; set; } + public string? DeletedBy { get; set; } // FK → AspNetUsers.Id +} +``` + +### 2.2 Member(繼承 SoftDeleteEntity) + +完整欄位見 `docs/DB_SCHEMA.md §4`。重點欄位: + +| 欄位 | 型別 | 說明 | +|------|------|------| +| Id | int PK | auto-increment | +| FirstName_en | varchar(100) NOT NULL | 英文名 | +| LastName_en | varchar(100) NOT NULL | 英文姓 | +| FirstName_zh | varchar(100)? | 中文名 | +| LastName_zh | varchar(100)? | 中文姓 | +| Gender | varchar(10)? | 'M' \| 'F' \| 'Other' | +| DateOfBirth | date? | | +| BaptismDate | date? | | +| BaptismChurch | varchar(200)? | | +| Email | varchar(200)? | | +| PhoneCell | varchar(30)? | | +| PhoneHome | varchar(30)? | | +| Address / City / State / ZipCode | varchar | | +| Country | varchar(100) DEFAULT 'USA' | | +| PhotoBlobPath | varchar(500)? | Azure Blob | +| Status | varchar(20) DEFAULT 'Member' | 'Member' \| 'Visitor' \| 'Inactive' \| 'Former' | +| LanguagePreference | varchar(10) DEFAULT 'en' | | +| JoinDate | date? | | +| Notes | text? | | +| FamilyUnitId | int? | FK → FamilyUnits.Id(Phase 1 建 FK,不做 UI)| + +### 2.3 FamilyUnit(繼承 AuditableEntity) + +Phase 1:建 Entity + Migration,不實作 CRUD UI。 +欄位:`Id`, `FamilyName_en`, `FamilyName_zh`, `Notes` + audit fields。 + +### 2.4 AppUser 更新 + +`AppUser.MemberId`(已存在,nullable int)在建立 User 時強制填入。 + +--- + +## 3. 後端架構 + +### 3.1 分層結構 + +``` +API/ROLAC.API/ +├── Entities/ +│ ├── Base/ +│ │ ├── AuditableEntity.cs +│ │ └── SoftDeleteEntity.cs +│ ├── Member.cs +│ └── FamilyUnit.cs +├── Data/ +│ ├── AppDbContext.cs (新增 DbSets + QueryFilter) +│ └── Interceptors/ +│ └── AuditSaveChangesInterceptor.cs +├── DTOs/ +│ ├── Members/ +│ │ ├── MemberDto.cs +│ │ ├── MemberListItemDto.cs +│ │ ├── CreateMemberRequest.cs +│ │ └── UpdateMemberRequest.cs +│ ├── Users/ +│ │ ├── UserDto.cs +│ │ ├── UserListItemDto.cs +│ │ ├── CreateUserRequest.cs +│ │ └── UpdateUserRequest.cs +│ └── Shared/ +│ └── PagedResult.cs +├── Services/ +│ ├── IMemberService.cs / MemberService.cs +│ └── IUserManagementService.cs / UserManagementService.cs +└── Controllers/ + ├── MembersController.cs + └── UsersController.cs +``` + +### 3.2 AuditSaveChangesInterceptor + +繼承 `SaveChangesInterceptor`,在 `SavingChangesAsync` 前: + +- `EntityState.Added` + 繼承 `AuditableEntity` → 填 `CreatedAt = UtcNow`, `CreatedBy = currentUserId`, `UpdatedAt = UtcNow`, `UpdatedBy = currentUserId` +- `EntityState.Modified` + 繼承 `AuditableEntity` → 填 `UpdatedAt = UtcNow`, `UpdatedBy = currentUserId` +- `EntityState.Deleted` + 繼承 `SoftDeleteEntity` → 攔截,改成 `Modified`,設 `IsDeleted=true`, `DeletedAt=UtcNow`, `DeletedBy=currentUserId` +- `currentUserId` 透過 `IHttpContextAccessor` 取 JWT claim `sub` + +### 3.3 MemberService + +```csharp +public interface IMemberService +{ + Task> GetPagedAsync(int page, int pageSize, string? search, string? status, bool? hasUser); + Task GetByIdAsync(int id); + Task CreateAsync(CreateMemberRequest request, string createdBy); + Task UpdateAsync(int id, UpdateMemberRequest request); + Task DeleteAsync(int id); // soft delete via interceptor +} +``` + +直接注入 `AppDbContext`。`GetPagedAsync` 支援: +- `search`:比對 `FirstName_en`, `LastName_en`, `FirstName_zh`, `LastName_zh`, `Email` +- `status`:篩選 `Member.Status` +- `hasUser`:LEFT JOIN `AspNetUsers` ON `MemberId = Member.Id` + +### 3.4 UserManagementService + +```csharp +public interface IUserManagementService +{ + Task> GetPagedAsync(int page, int pageSize, string? search); + Task GetByIdAsync(string id); + Task CreateAsync(CreateUserRequest request); + // CreateUserResult: { UserId, TempPassword } + Task UpdateAsync(string id, UpdateUserRequest request); + Task DeactivateAsync(string id); // IsActive=false + LockoutEnd=MaxValue + Task ResetPasswordAsync(string id); // 回傳新臨時密碼 +} +``` + +注入 `UserManager`, `RoleManager`, `AppDbContext`。 + +**CreateAsync 邏輯:** +1. 驗證 `MemberId` 存在且未刪除 +2. 驗證 `MemberId` 未被其他 User 使用(UNIQUE 約束) +3. 驗證 email 未重複 +4. 生成 12 字臨時密碼(大小寫+數字+特殊符號) +5. `userManager.CreateAsync(user, tempPassword)` +6. `userManager.AddToRolesAsync(user, request.Roles)` +7. 回傳 `{ UserId, TempPassword }` + +### 3.5 API Endpoints + +#### Members + +| Method | Route | 說明 | 角色 | +|--------|-------|------|------| +| GET | `/api/members` | 分頁列表 | super_admin, secretary, pastor | +| GET | `/api/members/{id}` | 單筆詳情 | super_admin, secretary, pastor | +| POST | `/api/members` | 新增 | super_admin, secretary | +| PUT | `/api/members/{id}` | 更新 | super_admin, secretary | +| DELETE | `/api/members/{id}` | 軟刪除 | super_admin, secretary | + +GET 查詢參數:`?page=1&pageSize=20&search=Chen&status=Member&hasUser=true` +回傳:`PagedResult` + +#### Users + +| Method | Route | 說明 | 角色 | +|--------|-------|------|------| +| GET | `/api/users` | 分頁列表 | super_admin | +| GET | `/api/users/{id}` | 單筆詳情 | super_admin | +| POST | `/api/users` | 建立帳號(需 MemberId) | super_admin | +| PUT | `/api/users/{id}` | 更新(email/roles/IsActive) | super_admin | +| DELETE | `/api/users/{id}` | 停用帳號 | super_admin | +| POST | `/api/users/{id}/reset-password` | 重設密碼 | super_admin | + +POST /api/users request: +```json +{ "memberId": 42, "email": "john@example.com", "roles": ["member"], "languagePreference": "en" } +``` +POST /api/users response(**只回傳一次**): +```json +{ "userId": "guid...", "tempPassword": "Xk9#mP2qLv8!" } +``` + +--- + +## 4. 前端架構 + +### 4.1 路由 + +``` +/user-portal/admin/members → MembersPageComponent +/user-portal/admin/users → UsersPageComponent +``` + +### 4.2 新增 Angular Services + +``` +APP/src/app/ +└── features/ + ├── members/ + │ ├── models/ + │ │ └── member.model.ts + │ ├── services/ + │ │ └── member-api.service.ts (extends CrudBaseApiService) + │ ├── components/ + │ │ ├── member-form-dialog/ (新增/編輯 Member 表單) + │ │ └── create-user-dialog/ (從 Member 建立帳號) + │ └── pages/ + │ └── members-page/ + └── users/ + ├── models/ + │ └── user.model.ts + ├── services/ + │ └── user-api.service.ts (custom,非標準 CRUD) + ├── components/ + │ └── edit-user-dialog/ (編輯角色/狀態) + └── pages/ + └── users-page/ +``` + +### 4.3 Members 頁面 + +**Kendo Grid 欄位:** +- 姓名(`LastName_en, FirstName_en` / `LastName_zh FirstName_zh`) +- Status badge(顏色碼:Member=綠、Visitor=藍、Inactive=灰、Former=橙) +- Email +- PhoneCell +- JoinDate +- 帳號狀態(有帳號 → 綠色 ✓ badge;無 → 灰色 —) +- 操作:編輯 / 刪除 / **建立帳號**(只在無帳號時顯示) + +**新增/編輯 Dialog:** 全欄位,分為 3 個 tab: +- **基本資料**:姓名、性別、生日、Status、語言 +- **聯絡資訊**:Email、電話、地址 +- **教會資訊**:JoinDate、BaptismDate、BaptismChurch、Notes + +**建立帳號 Dialog(`CreateUserDialogComponent`):** +- 欄位:Email、角色(multi-select Kendo DropDownList)、語言偏好 +- 送出後顯示**臨時密碼**畫面(加「複製」按鈕),關閉前提醒「此密碼不會再次顯示」 + +### 4.4 Users 頁面 + +**Kendo Grid 欄位:** +- Email +- 連結 Member 姓名(可點擊跳到 Member 頁面) +- 角色 badges +- IsActive toggle +- 最後登入時間 +- 操作:編輯 / 停用 / 重設密碼 + +**重設密碼:** confirm dialog → 成功後顯示新臨時密碼(同 Member 頁建立帳號的結果畫面) + +### 4.5 側邊欄更新 + +`user-navbar.component.ts` 新增 Administration 分區: +```ts +adminNavItems: NavItem[] = [ + { text: 'Members', icon: peopleIcon, path: '/user-portal/admin/members' }, + { text: 'Users', icon: userIcon, path: '/user-portal/admin/users' }, +] +``` +只有 `super_admin` / `secretary` 角色才顯示此分區(透過 `AuthService.currentUser$` 的 roles 判斷)。 + +--- + +## 5. 安全性考量 + +- 所有 `/api/members` 和 `/api/users` 端點均需 `[Authorize]` +- User 管理 API 僅限 `super_admin`;Member 管理 API 允許 `secretary` 和 `pastor` 讀取 +- 臨時密碼僅在 API response 中回傳一次,不存入資料庫明文 +- `MemberId` 建立 UNIQUE INDEX(避免同一 Member 對應多個 User) + +--- + +## 6. EF Migration + +新增實體後需執行一次 Migration: +``` +Add-Migration AddMemberAndFamilyUnit +Update-Database +``` +Migration 需包含: +- `Members` 表(含所有欄位 + soft delete + audit fields) +- `FamilyUnits` 表 +- `AspNetUsers.MemberId` 加 `UNIQUE INDEX`(nullable unique — 允許多個 null,但非 null 值唯一) +- `idx_members_status`, `idx_members_email` indexes(參見 DB_SCHEMA.md §17) + +--- + +## 7. 不在本次範疇 + +- FamilyUnit CRUD UI(Phase 1 建 Entity,UI 暫緩) +- MemberTag / MemberMinistry 管理 UI +- 大頭照上傳(PhotoBlobPath) +- Email 邀請連結 +- Member 頁面的 Pagination(後端已支援,前端先實作)