docs: add AspNetUsers CRUD and Member Management design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-05-27 08:12:07 -07:00
parent 60405ef0aa
commit adad5cb7e9
@@ -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.IdPhase 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<PagedResult<MemberListItemDto>> GetPagedAsync(int page, int pageSize, string? search, string? status, bool? hasUser);
Task<MemberDto?> GetByIdAsync(int id);
Task<int> 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<PagedResult<UserListItemDto>> GetPagedAsync(int page, int pageSize, string? search);
Task<UserDto?> GetByIdAsync(string id);
Task<CreateUserResult> CreateAsync(CreateUserRequest request);
// CreateUserResult: { UserId, TempPassword }
Task UpdateAsync(string id, UpdateUserRequest request);
Task DeactivateAsync(string id); // IsActive=false + LockoutEnd=MaxValue
Task<string> ResetPasswordAsync(string id); // 回傳新臨時密碼
}
```
注入 `UserManager<AppUser>`, `RoleManager<AppRole>`, `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<MemberListItemDto>`
#### 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<MemberDto>)
│ ├── 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 UIPhase 1 建 EntityUI 暫緩)
- MemberTag / MemberMinistry 管理 UI
- 大頭照上傳(PhotoBlobPath
- Email 邀請連結
- Member 頁面的 Pagination(後端已支援,前端先實作)