member-module
Member Base NestJS Module (會員系統模組)
Overview
@rytass/member-base-nestjs-module 提供完整的 NestJS 會員管理系統,包含 JWT 認證、OAuth2 整合、RBAC+Domain 權限控制(Casbin)和密碼策略管理。
Quick Start
安裝
npm install @rytass/member-base-nestjs-module
基本設定
import { MemberBaseModule } from '@rytass/member-base-nestjs-module';
@Module({
imports: [
TypeOrmModule.forRoot({ /* 資料庫配置 */ }),
MemberBaseModule.forRoot({
accessTokenExpiration: 900, // 15 分鐘
refreshTokenExpiration: 7776000, // 90 天
passwordMinLength: 12,
casbinAdapterOptions: {
type: 'postgres',
host: 'localhost',
// ...
},
}),
],
})
export class AppModule {}
會員登入
import { MemberBaseService } from '@rytass/member-base-nestjs-module';
@Injectable()
export class AuthService {
constructor(private readonly memberService: MemberBaseService) {}
// 基本登入
async login(account: string, password: string, ip?: string) {
const tokens = await this.memberService.login(account, password, ip);
return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
};
}
// 登入並指定 domain(用於多租戶場景)
async loginWithDomain(account: string, password: string, domain: string, ip?: string) {
const tokens = await this.memberService.login(account, password, { domain, ip });
return tokens;
}
}
Core Concepts
JWT 認證流程
Client → [Bearer Token] → CasbinGuard → Verify JWT → Extract Payload → Route Handler
↓
Check Decorators
↓
@IsPublic / @Authenticated / @AllowActions
權限裝飾器
| 裝飾器 | 用途 | 範例 |
|---|---|---|
@IsPublic() |
完全公開 | 登入、註冊頁面 |
@Authenticated() |
僅需有效 Token | 個人資料頁面 |
@AllowActions([...]) |
RBAC 權限檢查 | 管理功能 |
@HasPermission([subject, action]) |
動態權限檢查 | 參數裝飾器,用於 Field Resolver |
@MemberId() |
注入會員 ID | 參數裝飾器 |
@Account() |
注入會員帳號 | 參數裝飾器 |
RBAC+Domain 模型
// Casbin 策略格式: [subject, domain, object, action]
// 定義權限: [subject, domain, object, action]
await enforcer.addPolicy('admin-role', 'articles', 'article', 'create');
await enforcer.addPolicy('admin-role', 'articles', 'article', 'delete');
// 指派角色: [member, role, domain]
await enforcer.addGroupingPolicy(memberId, 'admin-role', 'articles');
// 路由保護: @AllowActions 接受 [Subject, Action][] 二元組陣列
@AllowActions([['article', 'create']])
async createArticle() { }
Common Patterns
完整認證設定
MemberBaseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
accessTokenSecret: config.get('JWT_ACCESS_SECRET'),
accessTokenExpiration: 900,
refreshTokenSecret: config.get('JWT_REFRESH_SECRET'),
refreshTokenExpiration: 7776000,
// 密碼策略
passwordMinLength: 12,
passwordShouldIncludeUppercase: true,
passwordShouldIncludeLowercase: true,
passwordShouldIncludeDigit: true,
passwordShouldIncludeSpecialCharacters: true,
passwordHistoryLimit: 5,
passwordAgeLimitInDays: 90,
// 登入安全
loginFailedBanThreshold: 5,
loginFailedAutoUnlockSeconds: 1800,
// Casbin
casbinAdapterOptions: {
type: 'postgres',
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
},
}),
})
OAuth2 整合
支援三種 OAuth2 Provider 類型:
import { OAuth2Provider, GoogleOAuth2Provider, FacebookOAuth2Provider, CustomOAuth2Provider } from '@rytass/member-base-nestjs-module';
// Google Provider
const googleProvider: GoogleOAuth2Provider = {
channel: 'google',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'https://app.com/auth/callbacks/google',
scope: ['profile', 'email'], // 可選
getState: () => crypto.randomUUID(), // 可選
};
// Facebook Provider
const facebookProvider: FacebookOAuth2Provider = {
channel: 'facebook',
clientId: process.env.FACEBOOK_APP_ID!,
clientSecret: process.env.FACEBOOK_APP_SECRET!,
redirectUri: 'https://app.com/auth/callbacks/facebook',
scope: ['email', 'public_profile'],
};
// Custom OAuth2 Provider(自訂 OAuth2 服務)
const customProvider: CustomOAuth2Provider = {
channel: 'line', // 自訂名稱(非 google/facebook)
clientId: process.env.LINE_CLIENT_ID!,
clientSecret: process.env.LINE_CLIENT_SECRET!,
redirectUri: 'https://app.com/auth/callbacks/line',
scope: ['profile', 'openid'], // 必填
requestUrl: 'https://access.line.me/oauth2/v2.1/authorize', // 必填
getAccessTokenFromCode: async (code) => {
// 實作從 code 取得 access token 的邏輯
const response = await fetch('https://api.line.me/oauth2/v2.1/token', { /* ... */ });
return response.access_token;
},
getAccountFromAccessToken: async (accessToken) => {
// 實作從 access token 取得使用者識別的邏輯
const profile = await fetch('https://api.line.me/v2/profile', { /* ... */ });
return profile.userId; // 回傳唯一識別碼
},
};
// 模組配置
MemberBaseModule.forRoot({
oauth2Providers: [googleProvider, facebookProvider, customProvider],
oauth2ClientDestUrl: '/dashboard',
});
內建 OAuth2 Controller 路由:
GET /auth/login/google // 重導向到 Google OAuth
GET /auth/callbacks/google // 處理 Google 回調
GET /auth/login/facebook // 重導向到 Facebook OAuth
GET /auth/callbacks/facebook // 處理 Facebook 回調
GET /auth/login/:channel // 自訂 OAuth provider
GET /auth/callbacks/:channel // 自訂 provider 回調
OAuthService 方法:
注意:
OAuthService未透過index.ts導出,僅能透過 NestJS 依賴注入使用。
// OAuthService 透過 DI 注入(不能直接 import)
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AuthController {
constructor(private readonly oauthService: OAuthService) {}
// 取得 OAuth 登入 URL
async getGoogleUrl(): Promise<string> {
return this.oauthService.getGoogleOAuthLoginUrl();
}
async getFacebookUrl(): Promise<string> {
return this.oauthService.getFacebookOAuthLoginUrl();
}
async getCustomUrl(channel: string): Promise<string> {
return this.oauthService.getCustomOAuthLoginUrl(channel);
}
// 處理 OAuth callback(回傳 TokenPairDto & { state?: string })
async googleCallback(code: string, state?: string) {
return this.oauthService.loginWithGoogleOAuth2Code(code, state);
}
async facebookCallback(code: string, state?: string) {
return this.oauthService.loginWithFacebookOAuth2Code(code, state);
}
async customCallback(channel: string, code: string, state?: string) {
return this.oauthService.loginWithCustomOAuth2Code(channel, code, state);
}
}
OAuth2Provider Type Guards:
注意: 這些 Type Guards 目前未透過
index.ts導出,若需要使用請在專案中自行定義:
// 自行定義 Type Guards(因未從 package 導出)
type OAuth2Provider = GoogleOAuth2Provider | FacebookOAuth2Provider | CustomOAuth2Provider;
function isGoogleProvider(p: OAuth2Provider): p is GoogleOAuth2Provider {
return p.channel === 'google';
}
function isFacebookProvider(p: OAuth2Provider): p is FacebookOAuth2Provider {
return p.channel === 'facebook';
}
function isCustomProvider(p: OAuth2Provider): p is CustomOAuth2Provider {
return p.channel !== 'google' && p.channel !== 'facebook';
}
自訂會員實體
import { BaseMemberEntity } from '@rytass/member-base-nestjs-module';
@Entity('members')
@ChildEntity()
export class CustomMember extends BaseMemberEntity {
@Column({ nullable: true })
displayName?: string;
@Column({ default: false })
isVerified: boolean;
@Column({ type: 'jsonb', nullable: true })
profile?: Record<string, any>;
}
// 模組配置
MemberBaseModule.forRoot({
memberEntity: CustomMember,
})
自訂 JWT Payload
MemberBaseModule.forRoot({
customizedJwtPayload: (member) => ({
id: member.id,
account: member.account,
displayName: member.displayName,
roles: member.roles,
}),
})
會員註冊
@Injectable()
export class RegistrationService {
constructor(private readonly memberService: MemberBaseService) {}
// 一般註冊
async register(account: string, password: string) {
return this.memberService.register(account, password, {
displayName: 'New User', // 可傳入自訂欄位
});
}
// 無密碼註冊(系統產生密碼)
async registerWithoutPassword(account: string) {
const [member, generatedPassword] = await this.memberService.registerWithoutPassword(account, {
shouldUpdatePassword: true, // 預設 true
});
// 發送 email 包含臨時密碼
return member;
}
}
密碼重設流程
@Injectable()
export class PasswordResetService {
constructor(private readonly memberService: MemberBaseService) {}
async requestReset(account: string) {
const token = await this.memberService.getResetPasswordToken(account);
// 發送 email 包含 token
return { message: 'Reset email sent' };
}
async resetPassword(token: string, newPassword: string) {
await this.memberService.changePasswordWithToken(token, newPassword);
return { message: 'Password changed' };
}
// 變更密碼(需舊密碼)
async changePassword(memberId: string, oldPassword: string, newPassword: string) {
await this.memberService.changePassword(memberId, oldPassword, newPassword);
return { message: 'Password changed' };
}
}
Token 刷新
@Injectable()
export class TokenService {
constructor(private readonly memberService: MemberBaseService) {}
async refresh(refreshToken: string, domain?: string) {
return this.memberService.refreshToken(refreshToken, { domain });
}
}
解鎖帳號
@Injectable()
export class AdminService {
constructor(private readonly memberService: MemberBaseService) {}
// 重設登入失敗次數(解鎖帳號)
async unlockMember(memberId: string) {
return this.memberService.resetLoginFailedCounter(memberId);
}
}
管理員操作 (MemberBaseAdminService)
import { MemberBaseAdminService } from '@rytass/member-base-nestjs-module';
@Injectable()
export class AdminManagementService {
constructor(private readonly adminService: MemberBaseAdminService) {}
// 軟刪除會員
async archiveMember(memberId: string): Promise<void> {
return this.adminService.archiveMember(memberId);
}
// 管理員重設密碼(可跳過密碼策略檢查)
async resetMemberPassword(
memberId: string,
newPassword: string,
ignorePasswordPolicy?: boolean // 預設 false
) {
return this.adminService.resetMemberPassword(memberId, newPassword, ignorePasswordPolicy);
}
}
密碼驗證工具 (PasswordValidatorService)
import { PasswordValidatorService } from '@rytass/member-base-nestjs-module';
@Injectable()
export class PasswordService {
constructor(private readonly passwordValidator: PasswordValidatorService) {}
// 產生符合策略的隨機密碼
generatePassword(): string {
return this.passwordValidator.generateValidPassword();
}
// 檢查密碼是否過期
checkPasswordExpiry(member: MemberEntity): boolean {
return this.passwordValidator.shouldUpdatePassword(member);
}
// 驗證密碼是否符合策略(支援歷史檢查)
async validatePassword(password: string, memberId?: string): Promise<boolean> {
return this.passwordValidator.validatePassword(password, memberId);
}
}
GraphQL 整合
import { GraphQLModule } from '@nestjs/graphql';
import { GraphQLContextTokenResolver } from '@rytass/member-base-nestjs-module';
@Module({
imports: [
GraphQLModule.forRoot({
context: GraphQLContextTokenResolver, // 自動提取 Token
fieldResolverEnhancers: ['guards'],
}),
MemberBaseModule.forRoot({ /* ... */ }),
],
})
export class AppModule {}
Module Options
認證選項
| 選項 | 預設 | 說明 |
|---|---|---|
accessTokenSecret |
隨機 | JWT Access Token 簽署密鑰 |
accessTokenExpiration |
900 | Access Token 過期時間(秒) |
refreshTokenSecret |
隨機 | Refresh Token 密鑰 |
refreshTokenExpiration |
7776000 | Refresh Token 過期時間(秒) |
onlyResetRefreshTokenExpirationByPassword |
false | 僅在密碼變更時重設 Refresh Token 過期時間 |
cookieMode |
false | 使用 HTTP-only Cookie(分兩個 cookie: ACCESS_TOKEN 和 REFRESH_TOKEN) |
accessTokenCookieName |
'ACCESS_TOKEN' | Access Token Cookie 名稱(cookieMode 時有效) |
refreshTokenCookieName |
'REFRESH_TOKEN' | Refresh Token Cookie 名稱(cookieMode 時有效) |
密碼重設
| 選項 | 預設 | 說明 |
|---|---|---|
resetPasswordTokenSecret |
隨機 | 密碼重設 Token 簽署密鑰 |
resetPasswordTokenExpiration |
3600 | 密碼重設 Token 過期時間(秒,預設 1 小時) |
密碼策略
| 選項 | 預設 | 說明 |
|---|---|---|
passwordMinLength |
8 | 最小長度 |
passwordShouldIncludeUppercase |
true | 需含大寫 |
passwordShouldIncludeLowercase |
true | 需含小寫 |
passwordShouldIncludeDigit |
true | 需含數字 |
passwordShouldIncludeSpecialCharacters |
false | 需含特殊字符 |
passwordPolicyRegExp |
undefined | 自訂密碼驗證 RegExp(設定後覆蓋上述選項) |
passwordHistoryLimit |
undefined | 密碼歷史限制(禁止重複使用最近 N 組密碼) |
passwordAgeLimitInDays |
undefined | 密碼有效天數 |
登入安全
| 選項 | 預設 | 說明 |
|---|---|---|
loginFailedBanThreshold |
5 | 失敗次數上限 |
loginFailedAutoUnlockSeconds |
null | 自動解鎖時間(秒) |
forceRejectLoginOnPasswordExpired |
false | 密碼過期時拒絕登入 |
Casbin 權限控制
| 選項 | 預設 | 說明 |
|---|---|---|
enableGlobalGuard |
true | 啟用全域 Guard |
casbinAdapterOptions |
- | TypeORM Adapter 配置 |
casbinModelString |
RBAC with domains | Casbin Model 定義 |
casbinPermissionDecorator |
- | 自訂權限裝飾器 |
casbinPermissionChecker |
- | 自訂權限檢查函式 |
實體與 Payload
| 選項 | 預設 | 說明 |
|---|---|---|
memberEntity |
BaseMemberEntity | 自訂會員實體類別 |
customizedJwtPayload |
- | 自訂 JWT Payload 產生函式 |
OAuth2
| 選項 | 預設 | 說明 |
|---|---|---|
oauth2Providers |
- | OAuth2 Provider 陣列 |
oauth2ClientDestUrl |
'/login' | OAuth2 登入後重導向 URL |
Symbol Tokens
可用於依賴注入的 Symbol Tokens:
import {
// Repository Token
RESOLVED_MEMBER_REPO, // Repository<BaseMemberEntity>
RESOLVED_MEMBER_REPOSITORY, // 別名,等同 RESOLVED_MEMBER_REPO
BASE_MEMBER_REPOSITORY, // BaseMemberRepo Symbol
// Casbin
CASBIN_ENFORCER, // Casbin Enforcer 實例
// Module Options
MEMBER_BASE_MODULE_OPTIONS, // 完整模組配置
// Token Settings
ACCESS_TOKEN_SECRET,
ACCESS_TOKEN_EXPIRATION,
REFRESH_TOKEN_SECRET,
REFRESH_TOKEN_EXPIRATION,
// Feature Flags
ENABLE_GLOBAL_GUARD,
ONLY_RESET_REFRESH_TOKEN_EXPIRATION_BY_PASSWORD,
COOKIE_MODE,
ACCESS_TOKEN_COOKIE_NAME,
REFRESH_TOKEN_COOKIE_NAME,
} from '@rytass/member-base-nestjs-module';
// 使用範例
@Injectable()
export class CustomService {
constructor(
@Inject(RESOLVED_MEMBER_REPO)
private readonly memberRepo: Repository<BaseMemberEntity>,
@Inject(CASBIN_ENFORCER)
private readonly enforcer: Enforcer,
) {}
}
Constants
DEFAULT_CASBIN_DOMAIN
預設的 Casbin Domain 值:
import { DEFAULT_CASBIN_DOMAIN } from '@rytass/member-base-nestjs-module';
// 值: '::DEFAULT::'
// 用於未指定 domain 時的預設 domain
Additional Exported Entities
import {
// 實體
BaseMemberEntity,
MemberLoginLogEntity,
MemberPasswordHistoryEntity,
MemberOAuthRecordEntity,
// 實體 Repository Symbols
MemberLoginLogRepo, // Symbol('MemberLoginLogRepo')
} from '@rytass/member-base-nestjs-module';
API Reference
詳細 API 文件請參閱 reference.md。
Data Types
TokenPairDto
登入成功後的回傳結構:
interface TokenPairDto {
accessToken: string; // JWT Access Token
refreshToken: string; // Refresh Token
shouldUpdatePassword?: boolean; // 密碼是否需要更新(僅啟用密碼過期檢查時返回)
passwordChangedAt?: string; // ISO8601 格式,上次密碼更改時間
}
Error Codes
| 代碼 | 錯誤類別 | 說明 |
|---|---|---|
| 100 | MemberNotFoundError |
找不到會員 |
| 101 | PasswordDoesNotMeetPolicyError |
密碼不符合策略 |
| 102 | InvalidPasswordError |
密碼錯誤 |
| 103 | PasswordValidationError |
密碼驗證失敗 |
| 104 | InvalidToken |
Token 無效 |
| 105 | MemberAlreadyExistedError |
會員已存在 |
| 106 | PasswordChangedError |
密碼已變更 |
| 107 | MemberBannedError |
會員已被停權 |
| 108 | PasswordExpiredError |
密碼已過期 |
| 109 | PasswordShouldUpdatePasswordError |
需要更新密碼 |
| 110 | PasswordInHistoryError |
密碼在歷史記錄中(不能重複使用) |
Errors 物件導出
所有錯誤類別透過 Errors 物件統一導出:
import { Errors } from '@rytass/member-base-nestjs-module';
// 使用範例
if (error instanceof Errors.MemberNotFoundError) {
console.log('會員不存在');
}
// Errors 包含:
// - MemberNotFoundError
// - PasswordDoesNotMeetPolicyError
// - InvalidPasswordError
// - PasswordValidationError
// - InvalidToken
// - MemberAlreadyExistedError
// - PasswordChangedError
// - MemberBannedError
// - PasswordExpiredError
// - PasswordShouldUpdatePasswordError
注意:
PasswordInHistoryError目前未包含在Errors物件中,需直接從./constants/errors/base.error導入使用。
Troubleshooting
Token 驗證失敗
- 確認
accessTokenSecret一致 - 檢查 Token 是否過期
- 驗證 Bearer 格式正確
Casbin 權限不生效
- 確認
casbinAdapterOptions配置正確 - 檢查策略是否正確添加
- 確認 domain 參數匹配
密碼策略錯誤
錯誤代碼 101: PasswordDoesNotMeetPolicyError
- 檢查密碼長度、大小寫、數字、特殊字符要求
- 使用
PasswordValidatorService.validatePassword()測試
More from rytass/utils
wms-module
|
24logistics-development
|
13logistics-adapters
|
12invoice-adapters
Taiwan e-invoice integration (台灣電子發票整合). Use when working with ECPay (綠界), EZPay (藍新), BankPro (金財通), or Amego (光貿) invoice services. Covers issuing invoices (開立發票), voiding (作廢), allowances (折讓), and querying invoice data.
8quadrats-module
|
7storage-development
Development guide for @rytass/storages base package (儲存基底套件開發指南). Use when creating new storage adapters (新增儲存 adapter), understanding base interfaces, or extending storage functionality. Covers StorageInterface, Storage class, file converters (檔案轉換器), hash algorithms (雜湊演算法), and implementation patterns.
7