# 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 | 英文姓 | | NickName | varchar(100)? | 常用名/英文小名(如 Chris)| | 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`, `NickName`, `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 欄位:** - 姓名(顯示優先順序:`NickName` → `FirstName_en`,搭配 `LastName_en`;中文名若有則顯示在括號內) - Status badge(顏色碼:Member=綠、Visitor=藍、Inactive=灰、Former=橙) - Email - PhoneCell - JoinDate - 帳號狀態(有帳號 → 綠色 ✓ badge;無 → 灰色 —) - 操作:編輯 / 刪除 / **建立帳號**(只在無帳號時顯示) **新增/編輯 Dialog:** 全欄位,分為 3 個 tab: - **基本資料**:法定英文姓名(FirstName_en / LastName_en)、NickName、中文姓名、性別、生日、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(後端已支援,前端先實作)