5d556b882d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
320 lines
11 KiB
Markdown
320 lines
11 KiB
Markdown
# 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<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 UI(Phase 1 建 Entity,UI 暫緩)
|
||
- MemberTag / MemberMinistry 管理 UI
|
||
- 大頭照上傳(PhotoBlobPath)
|
||
- Email 邀請連結
|
||
- Member 頁面的 Pagination(後端已支援,前端先實作)
|