member-module
SKILL.md
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()測試
Weekly Installs
6
Repository
rytass/utilsGitHub Stars
6
First Seen
Feb 5, 2026
Security Audits
Installed on
amp6
github-copilot6
replit6
codex6
kimi-cli6
gemini-cli6