Files
ROLAC/docs/superpowers/specs/2026-05-27-aspnetusers-member-management-design.md
Chris Chen 5d556b882d docs: add NickName field to Member spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 08:14:07 -07:00

320 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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`, `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<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 欄位:**
- 姓名(顯示優先順序:`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 UIPhase 1 建 EntityUI 暫緩)
- MemberTag / MemberMinistry 管理 UI
- 大頭照上傳(PhotoBlobPath
- Email 邀請連結
- Member 頁面的 Pagination(後端已支援,前端先實作)