# 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（後端已支援，前端先實作）
