| name | member-module |
| description | Member system NestJS module (會員系統 NestJS 模組). Use when working with user authentication (用戶認證), JWT tokens, OAuth2 integration (Google/Facebook), RBAC/ABAC permission control (權限控制), password policies (密碼策略), or Casbin integration. Keywords: 會員, 用戶, 認證, 授權, 權限, 登入, JWT, OAuth, Casbin, RBAC, ABAC, member, user, authentication, authorization, login, password
|
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,
refreshTokenExpiration: 7776000,
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,
};
}
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 模型
await enforcer.addPolicy('admin-role', 'articles', 'article', 'create');
await enforcer.addPolicy('admin-role', 'articles', 'article', 'delete');
await enforcer.addGroupingPolicy(memberId, 'admin-role', 'articles');
@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,
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';
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(),
};
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'],
};
const customProvider: CustomOAuth2Provider = {
channel: 'line',
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) => {
const response = await fetch('https://api.line.me/oauth2/v2.1/token', { });
return response.access_token;
},
getAccountFromAccessToken: async (accessToken) => {
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 依賴注入使用。
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class AuthController {
constructor(private readonly oauthService: OAuthService) {}
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);
}
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 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,
});
return member;
}
}
密碼重設流程
@Injectable()
export class PasswordResetService {
constructor(private readonly memberService: MemberBaseService) {}
async requestReset(account: string) {
const token = await this.memberService.getResetPasswordToken(account);
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
) {
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,
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 {
RESOLVED_MEMBER_REPO,
RESOLVED_MEMBER_REPOSITORY,
BASE_MEMBER_REPOSITORY,
CASBIN_ENFORCER,
MEMBER_BASE_MODULE_OPTIONS,
ACCESS_TOKEN_SECRET,
ACCESS_TOKEN_EXPIRATION,
REFRESH_TOKEN_SECRET,
REFRESH_TOKEN_EXPIRATION,
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';
Additional Exported Entities
import {
BaseMemberEntity,
MemberLoginLogEntity,
MemberPasswordHistoryEntity,
MemberOAuthRecordEntity,
MemberLoginLogRepo,
} from '@rytass/member-base-nestjs-module';
API Reference
詳細 API 文件請參閱 reference.md。
Data Types
TokenPairDto
登入成功後的回傳結構:
interface TokenPairDto {
accessToken: string;
refreshToken: string;
shouldUpdatePassword?: boolean;
passwordChangedAt?: string;
}
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('會員不存在');
}
注意: PasswordInHistoryError 目前未包含在 Errors 物件中,需直接從 ./constants/errors/base.error 導入使用。
Troubleshooting
Token 驗證失敗
- 確認
accessTokenSecret 一致
- 檢查 Token 是否過期
- 驗證 Bearer 格式正確
Casbin 權限不生效
- 確認
casbinAdapterOptions 配置正確
- 檢查策略是否正確添加
- 確認 domain 參數匹配
密碼策略錯誤
錯誤代碼 101: PasswordDoesNotMeetPolicyError
- 檢查密碼長度、大小寫、數字、特殊字符要求
- 使用
PasswordValidatorService.validatePassword() 測試