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

11 KiB
Raw Permalink Blame History

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 必須連結一個 MemberMemberId 不可為 null
  • 一個 Member 可以不有對應的 AppUser(教友可無登入帳號)

2. 資料模型

2.1 基礎類別

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

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
}

直接注入 AppDbContextGetPagedAsync 支援:

  • search:比對 FirstName_en, LastName_en, NickName, FirstName_zh, LastName_zh, Email
  • status:篩選 Member.Status
  • hasUserLEFT JOIN AspNetUsers ON MemberId = Member.Id

3.4 UserManagementService

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

{ "memberId": 42, "email": "john@example.com", "roles": ["member"], "languagePreference": "en" }

POST /api/users response只回傳一次):

{ "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 欄位:

  • 姓名(顯示優先順序:NickNameFirstName_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

建立帳號 DialogCreateUserDialogComponent):

  • 欄位: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 分區:

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_adminMember 管理 API 允許 secretarypastor 讀取
  • 臨時密碼僅在 API response 中回傳一次,不存入資料庫明文
  • MemberId 建立 UNIQUE INDEX(避免同一 Member 對應多個 User

6. EF Migration

新增實體後需執行一次 Migration:

Add-Migration AddMemberAndFamilyUnit
Update-Database

Migration 需包含:

  • Members 表(含所有欄位 + soft delete + audit fields
  • FamilyUnits
  • AspNetUsers.MemberIdUNIQUE INDEXnullable 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(後端已支援,前端先實作)